Legacy: This topic discusses extending operation views created using the Windows version Operations Dashboard for ArcGIS. This version of the application has been moved to Mature Support status. To learn more about the latest web-based version, see Operations Dashboard for ArcGIS.
The basic steps to creating a widget are as follows:
A widget in an operation view is an informational display that is used to provide quick access to the information you need. Widget types include bar or pie charts, lists or indicators, and so on. In this topic, you'll create an attribute table widget that shows the attribute information of the features in the form of a grid from the data source used by the widget, the name of the data source, and the number of features in the data source.
View tutorial filesBefore you begin, you must set up a development environment to get Windows Operations Dashboard, and prepare it to run in developer mode.
In the extensions folder that you created in the Set up a development environment topic, create a folder called tableWidget, and create the following files in that folder:
Note: You can assign different names to the above files.
Open the table.json file, or the manifest file, in your IDE and add the following text:
{ "type": "Widget", "title": "Table Widget", "description": "A widget that shows the attributes of the features from a data source", "thumbnailPath": "table.png", "useDataFromView": true, "runtime": { "path": "table.html" }, "configuration": { "requireDataSource": true }, "credits": "Esri" }
Keep the following details in mind when configuring this file:
Embedded in the configuration object is a requireDataSource property. When it is set to true, the widget's Settings dialog box will include a data source drop-down menu for the author of the operation view to choose a data source for the widget. This kind of widget is considered a data source consumer.
The configuration object is covered in more detail in the Create a configurable extension topic.
To learn more about the manifest file properties, see Manifest file.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <link rel="stylesheet" type="text/css" href="style.css"/> </head> <body> </body> </html>
<body>
tags, create a <div>
and add a data-dojo-type
attribute with the value "extension/TableWidget" as shown below. This attribute will be used to instantiate
an extension/TableWidget object that comes from the table widget dijit you'll create in the tableWidget.js. file.
<body> <div data-dojo-type="extension/TableWidget"></div> </body>
<div> <!-- Display data source name and the number of features --> <label class="dataSourceInfoLabel" data-dojo-attach-point="infoLabel"></label> <!-- Show the features on an attribute table--> <!-- Add the dgrid-autoheight class to the table so that its height will auto-adjust based on the contents --> <table class="dgrid-autoheight" data-dojo-attach-point="gridDiv"></table> </div>
This defines the UI of the widget. The HTML
fragment has a label
element that displays the name and feature count of the data source used by this widget.
Under that is a Dojo grid, or dgrid, that shows the attribute information of the features.
<body>
tag, add the
<script>
block with the dojoConfig object shown below.
The dojoConfig object defines which module (that is, the table widget dijit) loads. The async properties are set to true. To learn more about dojoConfig, see the Dojo Toolkit documentation.
<script> var dojoConfig = { async: true, paths: { "extension": location.pathname.replace(/\/[^/]+$/, '') } }; </script>
Note: Within the paths object, the URI of the extension folder is referenced as "extension".
This is used to construct the relative path to the table widget dijit (that is, "extension/TableWidget") in the
dojo-data-type attribute of a <div>
element in this file.
Note: If you want to pass the culture to the extension, you can do so here in the dojoConfig using locale: location.search.match(/locale=([\w-]+)/) ? RegExp.$1 : "en-us"
Then, you can access the locale by using window.dojoConfig.locale
.
<script>
block from the previous step, add another <script>
block referencing the ArcGIS API for JavaScript.
You must load the dojoConfig object before loading the ArcGIS API for JavaScript, otherwise the dojoConfig object will not load.
<script src="//js.arcgis.com/3.26/"></script>
<script>
block, add the Dojo parser:
<script> require([ "dojo/parser", "dojo/domReady!" ], function(parser){ parser.parse(); }); </script>
define
function:
define([ "dojo/_base/declare", "dojo/_base/lang", "dijit/_WidgetBase", "dijit/_TemplatedMixin", "esri/opsdashboard/WidgetProxy", "dojo/store/Memory", "dojo/store/Observable", "esri/tasks/query", "dgrid/OnDemandGrid", "dojo/text!./TableWidgetTemplate.html" ], function (declare, lang, _WidgetBase, _TemplatedMixin, WidgetProxy, Memory, Observable, Query, Grid, templateString) { });
This function will be used to define the table widget dijit. Load the dependent modules, and include a callback function with the modules as parameters.
The second parameter is an array of super classes that are inherited by the table widget class. The third parameter is an object that will contain the methods that will be called throughout the lifecycle of the class.
... "dojo/text!./TableWidgetTemplate.html" ], function (declare, lang, _WidgetBase, _TemplatedMixin, WidgetProxy, Memory, Observable, Query, Grid, templateString) { return declare("TableWidget", [_WidgetBase, _TemplatedMixin, WidgetProxy], { templateString: templateString, debugName: "TableWidget", }); });
return declare("TableWidget", [_WidgetBase, _TemplatedMixin, WidgetProxy], { templateString: templateString, debugName: "TableWidget", templateString: templateString, });
hostReady
callback function.
This callback will be invoked when Operations Dashboard signals to the extension that their relationship is established.
hostReady: function () { // Create the store we will use to display the features in the grid this.store = new Observable(new Memory()); // Get from the data source and the associated data source config // The dataSourceConfig stores the fields selected by the operation view publisher during configuration var dataSource = this.dataSourceProxies[0]; var dataSourceConfig = this.getDataSourceConfig(dataSource); // Build a collection of fields that we can display var fieldsToQuery = []; var columns = []; dataSource.fields.forEach(function (field) { switch (field.type) { case "esriFieldTypeString": case "esriFieldTypeSmallInteger": case "esriFieldTypeInteger": case "esriFieldTypeSingle": case "esriFieldTypeDouble": fieldsToQuery.push(field.name); columns.push({field: field.name}); return; } }); // Create the grid this.grid = new Grid({ store: this.store, cleanEmptyObservers: false, columns: columns }, this.gridDiv); this.grid.startup(); // Create the query object fieldsToQuery.push(dataSource.objectIdFieldName); this.query = new Query(); this.query.outFields = fieldsToQuery; this.query.returnGeometry = false; },
At this point, you should have completed the following tasks inside this callback:
hostReady
callback, add a dataSourceExpired
callback.
This callback is called when the widget is initialized. The widget queries for the features from the data source and displays the attribute information of the features in the table.
dataSourceExpired: function (dataSource, dataSourceConfig) { // Execute the query. A request will be sent to the server // to query for the features. // The results are in the featureSet dataSource.executeQuery(this.query).then(lang.hitch(this, function (featureSet) { // Show the name of the data source and the number of features // returned from the query this.updateDataSourceInfoLabel(dataSource.name, featureSet); // Show the features in the table this.updateAttributeTable(featureSet, dataSource); })); },
Note: This callback will also be called when:
dataSourceExpired
callback, add the updateDataSourceInfoLabel
helper method to display the
name and feature count of the data source.
updateDataSourceInfoLabel: function (dataSourceName, featureSet) { // Compose the correct string to display var dataSourceInfo = dataSourceName; if(!featureSet.features || featureSet.features.length == 0) dataSourceInfo += " has no feature"; else dataSourceInfo += " has " + featureSet.features.length + " features"; this.infoLabel.innerHTML = dataSourceInfo; },
updateDataSourceInfoLabel
method, add the updateAttributeTable
helper method to display
the attribute information of the features on the table.
updateAttributeTable: function (featureSet, dataSource) { // For each feature put them in the store and overwrite any existing // Potential improvement: Remove from the store the features that were not part of this update. if (this.store.data.length > 0) { this.store.query().forEach(function (item) { this.store.remove(item.id); }.bind(this)); } if (featureSet.features) { featureSet.features.forEach(function (feature) { this.store.put(feature.attributes, {overwrite: true, id: feature.attributes[dataSource.objectIdFieldName]}); }.bind(this)); } }
Your code for the TableWidget.js file should look like this:
define([ "dojo/_base/declare", "dojo/_base/lang", "dijit/_WidgetBase", "dijit/_TemplatedMixin", "esri/opsdashboard/WidgetProxy", "dojo/store/Memory", "dojo/store/Observable", "esri/tasks/query", "dgrid/OnDemandGrid", "dojo/text!./TableWidgetTemplate.html" ], function (declare, lang, _WidgetBase, _TemplatedMixin, WidgetProxy, Memory, Observable, Query, Grid, templateString) { return declare("TableWidget", [_WidgetBase, _TemplatedMixin, WidgetProxy], { templateString: templateString, debugName: "TableWidget", hostReady: function () { // Create the store we will use to display the features in the grid this.store = new Observable(new Memory()); // Get from the data source and the associated data source config // The dataSourceConfig stores the fields selected by the operation view publisher during configuration var dataSource = this.dataSourceProxies[0]; var dataSourceConfig = this.getDataSourceConfig(dataSource); // Build a collection of fields that we can display var fieldsToQuery = []; var columns = []; dataSource.fields.forEach(function (field) { switch (field.type) { case "esriFieldTypeString": case "esriFieldTypeSmallInteger": case "esriFieldTypeInteger": case "esriFieldTypeSingle": case "esriFieldTypeDouble": fieldsToQuery.push(field.name); columns.push({field: field.name}); return; } }); // Create the grid this.grid = new Grid({ store: this.store, cleanEmptyObservers: false, columns: columns }, this.gridDiv); this.grid.startup(); // Create the query object fieldsToQuery.push(dataSource.objectIdFieldName); this.query = new Query(); this.query.outFields = fieldsToQuery; this.query.returnGeometry = false; }, dataSourceExpired: function (dataSource, dataSourceConfig) { // Execute the query. A request will be sent to the server to query for the features. // The results are in the featureSet dataSource.executeQuery(this.query).then(function (featureSet) { // Show the name of the data source and the number of features returned from the query this.updateDataSourceInfoLabel(dataSource.name, featureSet); // Show the features in the table this.updateAttributeTable(featureSet, dataSource); }.bind(this)); }, updateDataSourceInfoLabel: function (dataSourceName, featureSet) { // Compose the correct string to display var dataSourceInfo = dataSourceName; if(!featureSet.features || featureSet.features.length == 0) dataSourceInfo += " has no feature"; else dataSourceInfo += " has " + featureSet.features.length + " features"; this.infoLabel.innerHTML = dataSourceInfo; }, updateAttributeTable: function (featureSet, dataSource) { // For each feature put them in the store and overwrite any existing // Potential improvement: Remove from the store the features that were not part of this update. if (this.store.data.length > 0) { this.store.query().forEach(function (item) { this.store.remove(item.id); }.bind(this)); } if (featureSet.features) { featureSet.features.forEach(function (feature) { this.store.put(feature.attributes, {overwrite: true, id: feature.attributes[dataSource.objectIdFieldName]}); }.bind(this)); } } }); });
Now that the extension UI and logic is in place, open the style.css file and add the following code to update the style of the UI elements:
html, body { width: 100%; height: 100%; margin: 0; padding: 0; overflow: hidden; } body { font: 13px sans-serif; font-family: "Segoe UI"; } label{ font-weight: bold; } .table { display: table; height: 100%; width: 100%; } .table-row { display: table-row; } .table-row.full { height: 100%; }
Your extension is now ready for testing in Operations Dashboard.
Note: To learn more about creating an operation view, see Create an operation view in the PDF of the operation view help.
The table widget should be listed under the Custom category. Double-click the Table widget to add it to the view.
Tip: When an extension is loaded in developer mode, it will have a red bounding box to help you distinguish it from the extensions created for the production environment.
With the widget in place, you can start debugging with the Operations Dashboard extension debugger.
Tip: You can also click the Dev Mode text at the upper right corner of the app, and click the Open Debug Tool to launch the debugger.
The URL to the table.HTML page shown on the debugger starts from localhost:<port number> because the extension is running from a local server.
Tip: If you make a change to the HTML or JS file during debugging, you can click the Refresh button at the upper left corner of the debugger to get the update. However, if you make any changes to the manifest file, you will need to save the operation view, close it, and reopen it.
To debug your extension using the web browser, complete the following steps:
Note: Since you're hosting your extension on the local server, you need to keep the Windows app as well as the local server running.