Saturday, November 10, 2012

Using AMD pattern to create modular ArcGis JavaScript Applications

1.     Introduction

As from Dojo 1.7, the pattern of using modular JavaScript classes was introduced to load JavaScript files asynchronous, no longer requiring loading the files at the retrieve of a html files or at least create a much simpler approach of this pattern. With the version 3.2 of the ArcGis JavaScript library ESRI introduced AMD for its portal API. ESRI will extend the use of AMD in other modules with later releases.

In this document I will illustrate how we can use this pattern to refactor the GIS framework described in earlier documents.  You will also see how popular other libraries can fit into this pattern.
2.     The modular approach

In modular languages  the last years new design patterns has been introduced to create modular and maintainable applications. Two patterns that emerged are separation of concerns and loose coupling.  In the picture below I outlined the flow of the different modules with the dependencies between them.  The way Dojo  AMD works can at a certain stage be compared to the use of PRISM for  Silverlight/WPF applications which is a library that implements  the two patterns separation of concerns and loose coupling .

Dojo AMD is responsible to create classes and inject there dependencies into the modules which helps us to create the modular concept. One of the goals of AMD is to restrict the use of global variables and encapsulate as much as possible into the created classes this covers the separation of concerns.

 In the previous version of my framework I heavenly relied on global variables that were created at the startup. With AMD this will no longer be the case, as they will be maintained by Dojo and can be compared to the pattern of loose coupling.


In PRISM on Silverlight/WPF there is component container that contains all objects which can be defined  with a single or multiple occurrence.

Most of the modules we will need does not  require multiple instances,  but rather requires to be a singleton.  Dojo AMD does not have a definition singletons. The map module, legend module and others requires only a single instance. Below you will find the blue print of a module definition that we can use to implement  a singleton class in Dojo AMD . You can find more information on this pattern at

http://unscriptable.com/2011/09/22/amd-module-patterns-singleton/

A typical singleton module has the structure outlook below.

define ([…],function () {
                var instance;
                // Add here the local variables and the local functions
                function Singleton() {

                }
                Singleton.prototype = {   
                // Add here your  public methods   
                 }
                return (instance || (instance || new Singleton()));
});

When the require function create the class it always returns the same object. In our application all component  modules as map, legend, toolbars are created as a singleton. This is also the case for the viewmodels in the MVVM pattern, except the main viewmodel , that I will explain later.

In a future document I will illustrate a regular AMD class that will create a new type of layer derived from the graphic layer that I called ‘sqlLayer’ and can have multiple instances. This class will make it possible to add SQL tables that have a geometry element and that are not managed by an ArcSde server. A REST service is responsible for doing the CRUDQ  actions.  A typical example is the use of a Microsoft SqlGeometry attribute in a table.

3.     Before and after AMD illustrated


In the diagram illustration of the different modules,  you can see the different components of a typical GIS application. With the help of KnockoutJS the demo application gives us the possibility to make a separation between the GUI and the business logic. I wanted also separate the GIS modules from the Business modules. The best solutions is when you can also separate the business components from the ArcGis JavaScript API.
First look at the situation before using AMD.
<script type="text/javascript" src="http://serverapi.arcgisonline.com/jsapi/arcgis/?v=3.2" ></script>
<script type="text/javascript">
/*------------------------------------*/
// REQUIRE
/*------------------------------------*/
         dojo.require("dijit.dijit");
         dojo.require("dijit.form.CheckBox");
         dojo.require("dijit.layout.BorderContainer");
         dojo.require("dijit.layout.ContentPane");
         dojo.require("dijit.layout.StackContainer");
         dojo.require("dijit.layout.StackController");
         dojo.require("dijit.layout.TabContainer");
         dojo.require("dijit.layout.AccordionContainer");
         dojo.require("dijit.popup");
         dojo.require("dijit._base.popup");
         dojo.require("dijit.DropDownMenu");
         dojo.require("dijit.MenuItem");
         dojo.require("dijit.MenuBar");
         dojo.require("dijit.PopupMenuBarItem");
         dojo.require("dijit.Toolbar");
         dojo.require("dijit.registry");
         // Esri dijits
         dojo.require("esri.arcgis.utils");
         dojo.require("esri.dijit.InfoWindow");
         dojo.require("esri.dijit.Legend");
         dojo.require("esri.dijit.Measurement");
         dojo.require("esri.dijit.Scalebar");
         dojo.require("esri.geometry");
         dojo.require("esri.map");
         dojo.require("esri.dijit.editing.Editor-all");
         dojo.require("esri.dijit.editing.TemplatePicker-all");
         dojo.require("esri.dijit.AttributeInspector-all");
         dojo.require("esri.layers.wms");
         dojo.require("esri.layers.wmts");
         dojo.require("esri.SnappingManager");
         dojo.require("esri.symbol");
         dojo.require("esri.renderer");
         dojo.require("esri.utils"); 
         dojo.require("esri.tasks.query");
         dojo.require("esri.toolbars.navigation");
</script>
<script type="text/javascript" src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.7.2.min.js"></script>
<script type="text/javascript" src="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.14/jquery-ui.min.js"></script>
<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?sensor=false"></script>
<script type="text/javascript" src="scripts/jquery.tmpl.js"></script>
<script type="text/javascript" src="scripts/jquery.livequery.js"></script>
<script type="text/javascript" src="scripts/jquery.xml2json.js"></script>
<script type="text/javascript" src="scripts/Modules/jqueryAgsLegendPlugIn.js"></script>
<script type="text/javascript" src="scripts/Modules/jqueryAgsSelectPlugIn.js"></script>
<script type="text/javascript" src="scripts/knockout-2.0.0.js"></script>
<script type="text/javascript" src="scripts/globalVars.js"></script>
<!-- Supporting JavaScript objects -->
<script type="text/javascript" src="scripts/Helper/eventAggregator.js"></script>
<script type="text/javascript" src="scripts/Helper/dataMapping.js"></script>
<script type="text/javascript" src="scripts/Helper/logUtil.js"></script>
<script type="text/javascript" src="scripts/Helper/genUtil.js"></script>
<!-- Gis JavaScript framework -->
<script type="text/javascript" src="scripts/Gis/gisOperation.js"></script>
<script type="text/javascript" src="scripts/Gis/gisCommonTasks.js"></script>
<script type="text/javascript" src="scripts/Gis/gisEditing.js"></script>
<script type="text/javascript" src="scripts/Gis/gisGeoProcessing.js"></script>
<script type="text/javascript" src="scripts/Gis/gisExtra.js"></script>
<!-- JavaScript GIS Modules -->
<script type="text/javascript" src="scripts/Modules/legendModule.js"></script>
<script type="text/javascript" src="scripts/Modules/layerSelectionModule.js"></script>
<script type="text/javascript" src="Scripts/Modules/commonToolbarModule.js"></script> 
<script type="text/javascript" src="scripts/Modules/editToolbarGeneralModule.js"></script>
<script type="text/javascript" src="scripts/Modules/mapModule.js"></script>
<!-- JavaScript View models -->
<script type="text/javascript" src="scripts/ViewModels/mainViewModel.js"></script>
<script type="text/javascript" src="scripts/ViewModels/commonToolbarVM.js"></script>
<script type="text/javascript" src="scripts/ViewModels/editToolbarGeneralVM.js"></script>
<script type="text/javascript" src="scripts/ViewModels/layerSelectionVM.js"></script>
<!-- JavaScript bootstrapper -->
<script type="text/javascript" src="scripts/startup.js"></script>
<!-- END JAVASCRIPT -->

As  you can see, this looks horrible if you not try to group the JavaScript files into more global files, but then you lose the modularity when you try to replace one module by another version. The AMD is clearly a solution to this issue.  After transferring to AMD, the same code now looks like this:
<script type="text/javascript">
  var dojoConfig = {
    async: true,
    tlmSiblingOfDojo: false,
    parseOnLoad: true,
    packages: [
         "name"'jtSoftware'"location""/Scripts/Gis" },
         "name"'helper'"location""/Scripts/Helper" },
         "name"'viewModels'"location""/Scripts/ViewModels" },
         "name"'modules'"location""/Scripts/Modules" },
         "name"'common'"location""/Scripts", main: "globalDefs" },
   "name"'google'"location"'https://maps.googleapis.com/maps/api/js?
sensor=false'},
        "name"'jquery'"location"'http://ajax.googleapis.com/ajax/libs/jquery/1.8.2', main: 'jquery.min' },
        "name"'jqueryui'"location"'http://ajax.googleapis.com/ajax/libs/jqueryui/1.9.0', main: 'jquery-ui.min' },
        "name"'jquerytmpl'"location""/Scripts", main: 'jquery.tmpl'},
        { "name"'jqueryxml'"location""/Scripts", main: 'jquery.xml2json'},
        "name"'ko'"location"'http://ajax.aspnetcdn.com/ajax/knockout', main: 'knockout-2.1.0' }]
  };
</script>
<script type="text/javascript" src="http://serverapi.arcgisonline.com/jsapi/arcgis/?v=3.2"></script>
<!-- JavaScript bootstrapper -->
<script type="text/javascript" src="scripts/bootstrap.js"></script>
<!-- END JAVASCRIPT -->

As you can see, all dependencies defined by require before AMD are gone and are now defined in the different modules as happens in high level programming languages.
With the use of packages, you now can group different JavaScript files together like you use libraries into high level programming languages.

The result is now a HTML page with little JavaScript code embedded. This makes a more clear separation between code behind (JavaScript embedded) and the business logic. Adapting the GUI for different environments (Mobile, Pad, Desktop) is no longer requiring adapting the different JavaScript definitions but can be done at module loading.
The loading of external libraries is defined also with the packages option. Popular libraries as ‘jquery’ and ‘knockoutJS’  can make use of AMD and are ether AMD compliant or are either support AMD loading as is the case of ‘jquery’. For jquery you must explicit define a property.

                define.amd.jQuery = true;

 4.     The flow of processing
In the HTML besides the loading of the ArcGis JavaScript API library, only one other JavaScript file is included in the HTML page, the ‘bootstrapper.js’. All will start with this module which after loading the configuration file kicks of the whole loading process of the different components of the HTML page. In the picture shows earlier, I illustrate how all different modules interact with each other. The following order is executing :

·         Document ready
·         Get configurations
·         Create viewmodel(s
·         Map module  initialization
·         Legend Module
·         Layer Selection module
·         Toolbar modules

During the loading of the modules, the needed GIS modules are loaded through the AMD pattern. Another important aspect is the  asynchronous starting of the initialization of the modules. To manage this asynchronous behavior the observer pattern is needed in pour application. For this I use a custom class ‘EventAggregator’ explained in an earlier document
http://jpenet.blogspot.be/2012/03/arcgis-javascript-api-creation-of.html

The use of publish and subscribe can also be used with the Dojo library, but for now I stick to this simpler solution.  It is very important, at least in the development stage, to add a lot of logging information for tracking the asynchronous process. With AMD, you now have two dimensions into the asynchronous process :

·         Asynchronous processing of GIS operations

·         Asynchronous loading of the AMD classes

In the first version of the  JavaScript implementation  I  mostly used the closure pattern .The refactoring of the modules for AMD (Singleton pattern) is not a hard job,  there is already a separation of local variables, only some renaming is required.
5.     Separation of concern

In the  flowchart of the modules, you see there are three main parts

·         GUI interface
·         Business logic
·         GIS  modules
o   GIS Framework
o   ESRI ArcGis JavaScript API         
With the use of MVVM through the library ‘knockoutJS’ I already separated the GUI interface from the business logic implemented with the map module, legend module, select layer module and the different viewmodel modules.
It is also useful to separate the Business logic from the ESRI ArcGis JavaScript API.  In the current implementation this Is not the case, but it is not a hard work to encapsulate all the direct use of ESRI components into the GIS framework. Although it was not the purpose in this study to replace the ESRI GIS solution by another GIS solution, by doing a clear separation of the business logic from the ArcGIS JavaScript library you could create an application based on another GIS implementation (OpenLayers , GeoServer) by replacing the GIS framework modules.
For the moment  there are a limited number of modules in the GIS framework. A split into core functionality and separate functional modules is a next step in implementing AMD. You can by example put all Google functionality into one separate module. Editing could also be split into base editing and more advance edit functionality.
6.     A first look at the AMD modules

To illustrate what the look is from the singleton modules created for the AMD version, I will show what I did with the map module. The creation of the map module is at the heart of the bootstrap process. All modules depends on the creation of a map module.
Before we had this as

var mapModule = (function () {
Now the header this looks as:

define(["helper/logging""helper/eventAggregator",
        "jtSoftware/configuration""jtSoftware/gisOperation""jtSoftware/gisEditing""modules/legendModule"],
    function (logging, eventAggregator, configuration, gisOperation, gisEditing, legendModule) {

The header shows now the dependency of the different classes. Through the parameters the Dojo library performs the dependency injection.
The method functions defined in the map module in the closure pattern is coded as

 return {
    init: function (divElement, mapLayersInitialized) {
       try {
          esri.config.defaults.map.slider = { left: "30px", top: "30px", width: null,
                          height: "200px" };
          var mapControl = new esri.Map(divElement, {});

 In AMD with the singleton pattern looks now like

MapModule.prototype = {
   init: function () {
      try {
            gisOperation.setSlider("30px""30px"null"200px");
            gisOperation.setMap(commonData.MapRegion);
            mapControl = gisOperation.getMap();

In the code above you can see how de dependency of the ArcGis library in the earlier version was removed by encapsulating the creation of the map object within the GIS framework.  This is the job to be done for making the business layer independed of the GIS architecture choosen. 
The loading of the ESRI components will be kicked off by the GisOperation  module of the GIS framework through AMD pattern.

In a next document I will illustrate more of the practical aspects in detail to move towards AMD implementation.

2 comments: