Oliver's Blog

August 2017 - Posts

  • Localizing DevExtreme

    We often hear questions around topics of localization and globalization for our DevExtreme widgets. This is not surprising, since we’re talking about a complex topic area — more complex than many developers realize, especially at the beginning of new projects!

    In this post, I’m going to describe what our products do out of the box and the available options to extend the functionality for advanced globalization and localization scenarios.

    Note: This post has now been updated to reflect the news from the v17.2 release.

    I expect that my scenarios may be incomplete! If you have a problem in this area that remains a mystery to you after reading this post, please comment and let me know!

    Areas of interest

    Globalization is a term that describes the process of making software aware of the locale it is running in, or even of the fact that there is more than one locale to consider. I hope this is not a common thing these days, but I remember seeing many applications in my life that worked with lots of hard-coded strings, clever date parsing functions specific to the author’s locale and other… well… tricks that make your life difficult when the application is supposed to run in a different country.

    Globalizing an application could be called a mindset adjustment, at least if the process starts before, or at least while, code is being written. It is mostly about guidelines like “don’t use string literals without thinking about translation”, “leave enough room in UI design for labels in other languages” or “don’t assume format details for numeric data”.

    Typical globalization tasks are these:

    • Provide a mechanism for end users to select their locale, and/or auto-detect it
    • Ensure that string literals are wrapped in a call to a translation retrieval function, loaded from an external translatable resource, or similar
    • Check that automated tests run with different locale settings to find potential issues
    • Enable the UI to adapt to changing space requirements

    Localization on the other hand involves steps to make an application work according to the preferences or requirements of a certain locale. These are typical tasks for localization:

    • Obtain translated content for each piece of text in the application
    • Configure display and data entry formats, specifically for numeric and date/time data
    • Switch the UI to right-to-left layout

    What’s in the box

    Non-English languages

    The DevExtreme distribution supplies localized strings for all widgets in four languages: English (which is the default), Russian, Japanese and German. The strings are contained in dictionary files, which are JavaScript or JSON files, depending on the packaging you use. There is documentation for this mechanism, but all it amounts to is that you need to include or load one or more of the language-specific dictionaries in your application. For instance, you could use a script tag to load one of the files like this:

    <script type="text/javascript" src="js/localization/dx.messages.de.js"></script>
    

    CDN URLs also work with this loading mechanism:

    <script type="text/javascript" 
      src="https://cdn3.devexpress.com/jslib/17.2.3/js/localization/dx.messages.de.js"></script>
    

    When using the npm packages, for instance, you will need to load a JSON file with the messages and call DevExpress.localization.loadMessages yourself. The section below titled Globalize/Using npm includes sample code for this process.

    Further examples can be found in the docs. It is possible to create custom dictionaries simply by copying one of the existing files, naming it according to a new locale, and changing the content depending on your needs.

    To use the localized strings from one of the dictionaries, all you need to do is tell the DevExtreme widgets which locale they should observe:

    DevExpress.localization.locale("de")
    

    In order to use the language requested by a user’s browser, we recommend the following call. Note however: you must be sure, when loading a localization library (see below), that the library either directly supports all the possible locales returned by this logic, or falls back gracefully to a supported locale. Especially with Globalize this can be quite hard, and you may want to map the browser language to a supported locale explicitly before calling locale(...).

    DevExpress.localization.locale(navigator.language || navigator.browserLanguage);
    

    Again, for a few more details check the documentation linked above.

    Right-to-left

    Right-to-left (RTL) layouts are supported by the widgets directly. When you set up a widget, you can use the rtlEnabled property for that widget:

    $("editor").dxDateBox({
      rtlEnabled: true
    });
    

    However, usually you want to set RTL for your entire application and this is possible by passing the same flag to the config function:

    DevExpress.config({ rtlEnabled: true });
    

    Here is the documentation page for config, in case you want to have a closer look.

    Value formatting

    Widgets support formatting of values via many different configuration options, and here is the general documentation page for format specifications. However, it is important to understand that automatic locale-specific formatting is only supported if you use a separate library – read on for that!

    Starting with v17.2, the formats supported by the built-in localization functionality are much more powerful than they were before, supporting custom date and number format strings.

    This structure definition is shown for an Object-type format block in our documentation:

    format: {
        type: String, // one of the predefined formats
        precision: Number, // the precision of values
        currency: String // a specific 3-letter code for the "currency" format
    }
    

    It is important to understand that the currency part of this specification will only be interpreted if a localization library is loaded, while type and precision work out of the box. The standard currency is usually USD (unless you change it via config()) and currency values will be displayed with a preceding $ (dollar sign).

    In the same way, using format: "currency" will only work with a separate localization library loaded.

    Using globalization/localization libraries

    There is support for two different globalization/localization libraries for DevExtreme. The main documentation refers to Globalize, which was supported first and remains the standard catch-all solution. However, I’m going to talk about Intl first, because it is the simpler of the two approaches.

    DevExtreme-Intl

    Intl is the short name used to refer to a particular object of the ECMAScript Internationalization API. To support this API for DevExtreme, we have created DevExtreme-Intl. If you follow this last link, you will see some documentation for the module in the project README file.

    Loading the library is very simple, since there is just one JavaScript file to include via a script tag or an npm call (and in the latter case you’d have to add a require or import statement to load the library). Once this is done, two things happen automatically:

    • Date and number formats in DevExtreme widgets observe the locale configured via localization.locale() as described above.
    • Currencies are interpreted where they are configured (globally or in format definitions) and values defined as currencies are displayed with the correct currency symbols.

    In addition, you can take advantage of extended formatting functionality for date and number fields by passing structures compatible with the Intl API. Follow this link for the README section that links to Intl documentation on these capabilities.

    To illustrate, here is one of the examples also shown in the README. The format structure uses the properties year, month and day, which are described here.

    $("#grid").dxDataGrid({
      dataSource: dataSource,
      columns: [
        {
          dataField: "OrderDate",
          format: { year: "2-digit", month: "narrow", day: "2-digit" }
        }, 
        ...
      ]
    });
    

    Starting with v17.2, you can achieve the same formatting using a simple format string: format: "dd. MMMMM yy".

    Using DevExtreme-Intl is easy (and you’ll see how complicated it is, in comparison, to set up Globalize), which is its main advantage. There is only one drawback: you may need to sort out additional mechanisms for the full-scale localization of your entire application, for instance for loading translations of arbitrary strings.

    Prior to v17.2, extra steps were required to enable date parsing for manual edits in dxDateBox editors or embedded date editors in the dxDataGrid, and formatting was restricted. These restrictions do not apply to versions starting with v17.2.

    Globalize

    The activation of Globalize for your project is much more complicated than that of Intl. Detailed instructions are available as part of our DevExtreme documentation. We recommend loading eight JavaScript files, and eight more JSON files with CLDR data. Globalize and the CLDR repository have even more modules than that, if you need them! Here is a link to the Globalize docs on CLDR, and this link is about distinguishing which modules you may require.

    Please note that the instructions on this documentation page assume certain paths to access the Globalize JavaScript files and the CLDR data. These paths are valid if you are using our DevExtreme installation from a zip file or the Windows installer. If you are using a different installation method, your paths will be different. We have other documentation, but it isn’t perfect at this time.

    Using a CDN

    The page CDN Services shows CDN URLs for the JavaScript files. You might want to check for more current versions of the two main packages cldrjs and globalize, and it is also possible to load Globalize from cdnjs instead of aspnetcdn. Further, the page doesn’t show any URLs to load CLDR data from in this scenario. To help you out, here is a version of the CLDR loading logic from the Use Globalize documentation linked above, using unpkg URLs to access CLDR data:

    $.when(
      $.getJSON('https://unpkg.com/cldr-dates-full/main/en/ca-gregorian.json'),
      $.getJSON('https://unpkg.com/cldr-numbers-full/main/en/numbers.json'),
      $.getJSON('https://unpkg.com/cldr-numbers-full/main/en/currencies.json'),
      $.getJSON('https://unpkg.com/cldr-dates-full/main/de/ca-gregorian.json'),
      $.getJSON('https://unpkg.com/cldr-numbers-full/main/de/numbers.json'),
      $.getJSON('https://unpkg.com/cldr-numbers-full/main/de/currencies.json'),
      $.getJSON('https://unpkg.com/cldr-core/supplemental/likelySubtags.json'),
      $.getJSON('https://unpkg.com/cldr-core/supplemental/timeData.json'),
      $.getJSON('https://unpkg.com/cldr-core/supplemental/weekData.json'),
      $.getJSON('https://unpkg.com/cldr-core/supplemental/currencyData.json'),
      $.getJSON('https://unpkg.com/cldr-core/supplemental/numberingSystems.json')
    ).then(function () {
    // ... continue like the example in the docs
    

    Using npm

    The basic use of npm is described in this page of the documentation. However, this page assumes that you will be loading Globalize files via script tags, accessing them directly from the node_modules path. In reality I think it is more likely that you’ll be using a packaging approach once you start installing modules via npm.

    The documentation page Modularity describes how to use Webpack and other packagers with your DevExtreme project. The description is pretty good, but also complex. Fortunately we have a set of sample projects, and I recommend you have a look at those in this github repository.

    Unfortunately the page on Modularity doesn’t refer to Globalize integration, and only few of the sample projects address this topic. One more thing for us to improve – meanwhile, here are the steps necessary to activate Globalize for your project.

    1 — Let’s say you start out from a project that has the simple structure shown in the Webpack/jQuery sample.

    2 — Install a few extra packages:

    npm install --save-dev cldr-data globalize
    

    3 — Edit index.js and add these lines at the top:

    require('devextreme/localization/globalize/message');
    require('devextreme/localization/globalize/number');
    require('devextreme/localization/globalize/currency');
    require('devextreme/localization/globalize/date');
    
    const Globalize = require('globalize');
    Globalize.load(
      // language specific files, loading English and German here
      require('cldr-data/main/en/ca-gregorian.json'),
      require('cldr-data/main/en/numbers.json'),
      require('cldr-data/main/en/currencies.json'),
      require('cldr-data/main/de/ca-gregorian.json'),
      require('cldr-data/main/de/numbers.json'),
      require('cldr-data/main/de/currencies.json'),
    
      require('cldr-data/supplemental/likelySubtags.json'),
      require('cldr-data/supplemental/timeData.json'),
      require('cldr-data/supplemental/weekData.json'),
      require('cldr-data/supplemental/currencyData.json'),
      require('cldr-data/supplemental/numberingSystems.json')
    );
    
    // German messages - English ones are included by default
    const deMessages = require('devextreme/localization/messages/de.json');
    
    const localization = require('devextreme/localization');
    
    // This loading instruction is not required when using CDN paths
    localization.loadMessages(deMessages);
    localization.locale('de');
    

    The first block loads the Globalize integration modules that come with the DevExtreme npm distribution. You might wonder why we don’t appear to be loading any of the cldrjs and globalize package files that were included in the CDN based approach. The reason is that when using this modularized approach, the required references are pulled in automatically by Webpack (once you’ve added something to the Webpack config, see below).

    Note that in some of our samples, you will find code that uses Globalize functions directly to load the messages and set the locale:

    Globalize.loadMessages(deMessages);
    Globalize.locale('de');
    

    There is no practical difference between these approaches for the task at hand. You may prefer using the DevExtreme “versions” of the functions for consistency (since that will work with or without Globalize loaded, or even with or without DevExtreme-Intl loaded). On the other hand, in a larger application you may be using Globalize independently to localize other parts of your application than just the DevExtreme widgets, and in that case it may seem more familiar to use Globalize API calls directly.

    4 — Edit webpack.config.js and add this block to the module.exports object:

    module.exports = {
      entry: ...
      ...
      resolve: {
        alias: {
          globalize$: path.resolve(
            __dirname,
            'node_modules/globalize/dist/globalize.js'
          ),
          globalize: path.resolve(
            __dirname,
            'node_modules/globalize/dist/globalize'
          ),
          cldr$: path.resolve(__dirname, 'node_modules/cldrjs/dist/cldr.js'),
          cldr: path.resolve(__dirname, 'node_modules/cldrjs/dist/cldr')
        }
      }
      ...
    }
    

    This configuration ensures that references to globalize and cldr inside some of the packages can be resolved correctly.

    5 — Build the bundle by running webpack (or node_modules/.bin/webpack if you don’t have Webpack installed globally). Open index.html in a browser and everything should come up correctly and without errors.

    6 — To see localization in action, I recommend adding a dxDateBox to your project. Add this to index.html, right after the myButton div that’s already in there:

    <div id="datebox"></div>
    

    Then add this code to your index.js:

    require('devextreme/ui/date_box');
    $('#datebox').dxDateBox();
    

    Don’t forget to run webpack again, and test the German localization by entering invalid text in the datebox. You should see a German language error message coming up. When you select a date, you should also see the German style format (dd.mm.yyyy).

    What Globalize does

    Once Globalize has been loaded, whichever mechanism you use, it performs the same tasks as Intl did in the section above. It enables localized date and number formats and the use of currency symbols.

    The infrastructure provided by Globalize is very complex, but also very powerful, and you’ll find it a versatile solution for application-wide localization tasks. An API overview for Globalize can be found here, and of course lots of third-party content is available about it.

    Final thoughts

    I hope this post was able to shed some light on the options for localization with the DevExtreme widgets, and the technical details of working with Globalize and Intl. As I said in my introduction, there are many different use cases and scenarios and I’m sure I’m not covering them all. Please let me know if you have questions about your own situation and I’ll help you answer them, and extend this post accordingly!

  • ClojureScript for the DevExtreme React Grid

    I had a question recently about ClojureScript and how easy it would be to use the DevExtreme React Grid with that language and environment. ClojureScript is a functional language, which is a concept that works very well with React.

    The CLJSJS repository provides an easy way for ClojureScript developers to depend on JavaScript libraries (in their own words), but JavaScript libraries need to be packaged and maintained in a specific format for this to work. I’m leaving this approach for later, since right now the React Grid has not reached a full release yet.

    Meanwhile I found a nice recipe in this post by Tomer Weller, on the topic of including arbitrary JavaScript libraries in a ClojureScript project. I created a demo project, which is available here: https://github.com/oliversturm/demo-dx-react-grid-clojurescript

    In case you have never heard about Clojure or ClojureScript, I recommend reading Danial Higginbotham’s book Clojure for the Brave and True and this very nice ClojureScript tutorial.

    Requirements to build or try the demo

    You will need both a working Node environment and an installation of Leiningen. Follow the two links in the previous sentence for installation files and instructions – both are quick and very straight-forward and you shouldn’t run into trouble.

    How to build the demo yourself

    Of course you can skip this part and just get the whole thing from github – in that case scroll down to find the instructions for running the demo. If you would like to do it yourself, read on.

    The steps I followed are based on the post linked above, but I will also describe the demo code itself.

    Project setup and library import

    1 — Set up the project

    Use this command to create a new project (feel free to modify the name of course):

    lein new reagent-frontend demo-dx-react-grid-clojurescript
    

    Now initialize the project with a Node package.json file (it’s okay to accept all the defaults):

    npm init
    

    Install the required JavaScript packages:

    npm install --save-dev bootstrap webpack react react-dom react-bootstrap @devexpress/dx-react-core @devexpress/dx-react-grid @devexpress/dx-react-grid-bootstrap3
    

    2 — Create the JavaScript bundle

    Create the file webpack.config.js with the following content (there should be no need to modify anything in here):

    const webpack = require('webpack');
    const path = require('path');
    
    const BUILD_DIR = path.resolve(__dirname, 'public', 'js');
    const APP_DIR = path.resolve(__dirname, 'src', 'js');
    
    const config = {
      entry: `${APP_DIR}/main.js`,
      output: {
        path: BUILD_DIR,
        filename: 'bundle.js'
      }
    };
    
    module.exports = config;
    

    Create the file src/js/main.js with the content below. This is the main file used by webpack, and it configures all the JavaScript dependencies to be made available in the window context. It also means that webpack finds the actual requirements and includes the correct packages in the bundle.

    window.deps = {
      react: require('react'),
      'react-dom': require('react-dom'),
      'dx-react-core': require('@devexpress/dx-react-core'),
      'dx-react-grid': require('@devexpress/dx-react-grid'),
      'dx-react-grid-bootstrap3': require('@devexpress/dx-react-grid-bootstrap3')
    };
    
    window.React = window.deps['react'];
    window.ReactDOM = window.deps['react-dom'];
    

    For simplicity, add a build entry to the scripts section of package.json and remove the test entry. Your scripts section should look like this:

    ...
      "scripts": {
        "build": "webpack -p"
      },
    ...
    

    Now run the build script to generate the bundle:

    npm run build
    

    3 — Modify project.clj for the bundle

    Edit the file project.clj. First, change the dependencies block (within the first few lines) to the following to make sure that Reagent uses React and ReactDOM from the bundle in place of its own versions.

    :dependencies [[org.clojure/clojure "1.8.0" :scope "provided"]
                   [org.clojure/clojurescript "1.9.908" :scope "provided"]
                   [reagent "0.7.0" :exclusions [cljsjs/react cljsjs/react-dom]]]
    

    Second, insert the following block right behind both lines starting with :optimizations. This changes both compiler profiles to include the bundle instead of the standard CLJSJS libraries for React and ReactDOM. (Note that it is correct for the ReactDOM name to read cljsjs.react.dom, i.e. with a . (dot) instead of a dash.)

    :foreign-libs [{:file "public/js/bundle.js"
                    :provides ["cljsjs.react" "cljsjs.react.dom" "webpack.bundle"]}]
    

    If you have trouble finding the right place to insert the last block, please double-check with the complete version from my demo.

    4 — Copy stylesheets and fonts

    The React Grid requires some Bootstrap stylesheets and font files to work correctly. It should be possible to include these in the bundle, but I went the easier way here to include them separately, not least because the Grid also supports Material UI as an alternative UI platform.

    Copy the stylesheets and font files to the public directory (use Windows copy andmd commands if you are indeed on Windows):

    cp node_modules/bootstrap/dist/css/bootstrap*min.css public/css
    mkdir public/fonts
    cp node_modules/bootstrap/dist/fonts/* public/fonts
    

    Edit public/index.html and add the lines to load the Bootstrap stylesheets. Your head section should look like this:

    <head>
      <meta charset="utf-8">
      <meta content="width=device-width, initial-scale=1" name="viewport">
      <link href="css/bootstrap.min.css" rel="stylesheet" type="text/css">
      <link href="css/bootstrap-theme.min.css" rel="stylesheet" type="text/css">
      <link href="css/site.css" rel="stylesheet" type="text/css">
    </head>
    

    Demo source code

    What remains is to implement the file src/demo-dx-react-grid-clojurescript/core.cljs to render a React Grid. First, add the bundle to the :require part of the namespace declaration:

    (ns demo-dx-react-grid-clojurescript.core
      (:require
       [reagent.core :as r]
       [webpack.bundle]))
    

    Here is my complete function home-page:

    (defn home-page []
      (let [g (aget js/window "deps" "dx-react-grid")
            sorting-state (aget g "SortingState")
            local-sorting (aget g "LocalSorting")
    
            bs3 (aget js/window "deps" "dx-react-grid-bootstrap3")
            grid (aget bs3 "Grid")
            table-view (aget bs3 "TableView")
            table-header-row (aget bs3 "TableHeaderRow")
    
            columns [{:name "name" :title "Name"}
                     {:name "age" :title "Age"}]
            rows [{:name "Oliver" :age 37}
                  {:name "Bert" :age 52}
                  {:name "Jill" :age 31}]]
        [:div
         [:h2 "DevExtreme React Grid"]
         [:> grid {:columns columns :rows rows }
          [:> sorting-state
           {:onSortingChange (fn [sorting] (.log js/console
                                                 "sorting changed" sorting))}]
          [:> local-sorting]
          [:> table-view ]
          [:> table-header-row {:allowSorting true}]]]))
    

    The first block of code in the function defines a few local values using let. This call retrieves the object deps.dx-react-grid from the window context (where the webpack main.js put it previously):

    (aget js/window "deps" "dx-react-grid")
    

    The same thing is done with the line that assigns the bs3 value. From these two top level JavaScript objects, the SortingState, LocalSorting, Grid, TableView and TableHeaderRow objects can be retrieved, and they are stored in Clojure values.

    As the final part of the let instruction, columns and rows are populated for demo purposes.

    The home-page function renders a result using the slightly extended Hiccup syntax implemented by the Reagent interface library for React. For example, this line begins rendering the Grid component, passing the previously arranged values as columns and rows React properties:

    [:> grid {:columns columns :rows rows}
    ...
    

    The nested elements are rendered using similar syntax and follow the normal structure of the React Grid. I have included an event handler on the SortingState (or sorting-state) for illustration purposes.

    Running the demo (mine or yours)

    If you cloned my project, you will need to build the JavaScript bundle before running the demo. If you were following the steps to do it yourself, you should have already done this above. These are the required commands:

    npm install
    npm run build
    

    With the bundle available, you can then run the demo by executing this:

    lein figwheel
    

    The command instructs Leiningen to run the project using the figwheel environment. On the console, you will see a few downloads of required components, a compilation step, hopefully no error messages (please check, especially in case things go wrong!) and finally this line:

    Prompt will show when Figwheel connects to your application
    

    At the same time, Figwheel tries to open the main page of the application in the browser. This works fine for me, but just in case you don’t see a browser page coming up, you can open it manually by connecting to http://localhost:3449 or opening the file public/index.html.

    When the browser page loads, Figwheel changes its prompt to read app:cljs.user=>, which means that the built-in REPL is ready for interaction.

    The browser should now show the working React Grid with three rows of demo data. Functionality is limited because I included only a few basic plugins - feel free to play around and extend, and let me know if you encounter any issues!

  • DevExtreme React Grid Remote Data Loading - Package and Fiddle

    In a recent post, I described a plugin for the purpose of loading data from a remote service.

    As of right now, that plugin is available as an npm package, so you can easily use it in your own applications. Here is the repository, where you can find usage details in the README shown at the bottom of the page: https://github.com/oliversturm/dx-react-devextreme-data-server

    Using the approach described in my post MongoDB and DevExtreme News – Data Servers Made Easy, I set up a demonstration data server running in the cloud. This makes it possible to show you this demo Fiddle, which utilizes the DevExtremeDataServer plugin:

    Please try the plugin and let me know what you think, or publish any issues or questions you have directly to github!

  • MongoDB and DevExtreme News - Data Servers Made Easy

    Several months ago, I announced the library devextreme-query-mongodb, which can be used to query MongoDB with parameters in the format supported by our DevExtreme UI widgets. You can find the original announcement here, and the library has its home on github here.

    Recently I have released version 2 of this library. I chose to use major version 2 for the release because of some structural changes I made. Existing projects that were using v1 should work with v2 directly, as soon as you take one extra requirement into account. As the project wiki shows:

    In v2, the library is published with babel-compiled files (in the dist) folder, which are used by default. This provides broader compatibility, but it introduces a requirement for babel-polyfill. To satisfy this, you should add a dependency to babel-polyfill to your project (npm install –save babel-polyfill) and initialize the polyfill before you load devextreme-query-mongodb:

    require('babel-polyfill');
    const query = require('devextreme-query-mongodb');
    

    The options API

    To make it easier to create data services on the basis of devextreme-query-mongodb, I have included an API in v2 that validates query parameters. You can find a page of documentation in the wiki, but the gist of it is that you can retrieve a valid loadOptions object like this:

    const getOptions = require('devextreme-query-mongodb/options').getOptions;
    
    ...
    
    // request object in req
    const loadOptions = getOptions(req.query);
    

    A sample data server

    The page DemoDataServer in the wiki has the full source code for a data server. I decided not to publish the server as a github project, because the source needs a few changes depending on your own scenarios and a github project wouldn’t have worked out of the box (or grown more complicated again).

    Here are the basic steps implemented in the sample.

    1 — Connect to MongoDB. Of course you need to use your own MongoDB connection URI here.

    mongodb.MongoClient.connect(mongoUri, function(err, database) {
    ...
    

    2 — Set up an Express web server. Modify the URL part and collection name as necessary.

    const app = express();
    ...
    app.get('/countries', function(req, res) {
      getData(database.collection('countries'), req, res);
    });
    

    3 — Load data, taking advantage of the new options API. Adapt the schema for your collection.

    function getQueryOptions(req) {
      return getOptions(req.query, {
        areaKM2: 'int',
        population: 'int'
      });
    }
    
    async function getData(coll, req, res) {
      try {
        const options = getQueryOptions(req);
    
        const results = await query(
          coll,
          options.loadOptions,
          options.processingOptions
        );
        res.status(200).jsonp(results);
      } catch (err) {
        handleError(res, err, 'Failed to retrieve data');
      }
    }
    

    4 — Run the web server

    const httpServer = http.createServer(app);
    httpServer.listen(process.env.HTTPPORT || 8080, function() {
      const port = httpServer.address().port;
      console.log('HTTP server running on port', port);
    });
    

    Try it!

    Please give the library a spin if you can use it in your projects, and report any issues or questions directly on github!

LIVE CHAT

Chat is one of the many ways you can contact members of the DevExpress Team.
We are available Monday-Friday between 7:30am and 4:30pm Pacific Time.

If you need additional product information, write to us at info@devexpress.com or call us at +1 (818) 844-3383

FOLLOW US

DevExpress engineers feature-complete Presentation Controls, IDE Productivity Tools, Business Application Frameworks, and Reporting Systems for Visual Studio, along with high-performance HTML JS Mobile Frameworks for developers targeting iOS, Android and Windows Phone. Whether using WPF, ASP.NET, WinForms, HTML5 or Windows 10, DevExpress tools help you build and deliver your best in the shortest time possible.

Copyright © 1998-2017 Developer Express Inc.
All trademarks or registered trademarks are property of their respective owners