Using the userToken

One of the most common questions I get asked when working with the Esri Silverlight API will be along the lines "I’m calling this task for a collection of graphics but how do I relate the results to the original values?". The answer is right there in the documentation but is often overlooked so here is a simple explanation of how to do it. Looking at the API reference for tasks you will see that most of the methods have an overload that takes an object parameter called userToken. As it is an object you can pass in your own data. The key to utilizing the userToken is that you pass it back to the calling code when the task completes. This way you have a reference to the original data as well as the task result data.

To see why this is useful let’s look at what happens when we do things the wrong way. In this example I’ll be running a buffer operation over a graphic collection. To demonstrate the problem we’ll create a Guid as our userToken value but try and reference it without using the overloaded buffer method. The code looks like.

string userToken;
private void Button_Click(object sender, RoutedEventArgs e)
{
    userToken = Guid.NewGuid().ToString();

    var geometryService = new GeometryService(@"http://sampleserver3.arcgisonline.com/ArcGIS/rest/services/Geometry/GeometryServer");

    geometryService.BufferCompleted += (sender2, e2) =>
    {
        System.Threading.Thread.Sleep(2000);
         
        TextResults.Items.Add(userToken);
    };

    var spatialReference = ((FeatureLayer)MyMap.Layers["CitiesFeatureLayer"]).Graphics.First().Geometry.SpatialReference;

    var bufferParams = new BufferParameters
    {
        BufferSpatialReference = spatialReference,
        OutSpatialReference = spatialReference,
        Unit = LinearUnit.Kilometer
    };

    bufferParams.Features.AddRange(((FeatureLayer)MyMap.Layers["CitiesFeatureLayer"]).Graphics);
    bufferParams.Distances.Add(100);

    geometryService.BufferAsync(bufferParams);
}

The userToken value is set at the start of the method and used again when we have a result in the BufferCompleted event handler. The output looks like this if we invoke the event handler in quick succession.

incorrect

See how the value for the Guid is duplicated due to our code being incorrect in this simple example so you can imagine the threading hell you can get into with more complex scenarios.

Thankfully it’s easy to resolve the situation. Here’s the updated code and output

private void Button_Click(object sender, RoutedEventArgs e)
{
    string userToken = Guid.NewGuid().ToString();

    var geometryService = new GeometryService(@"http://sampleserver3.arcgisonline.com/ArcGIS/rest/services/Geometry/GeometryServer");

    geometryService.BufferCompleted += (sender2, e2) =>
    {
        System.Threading.Thread.Sleep(2000);
        // The userToken is in the GraphicEventArgs 
        // We need to cast it back to the type that we passed in
        TextResults.Items.Add(e2.UserState.ToString());
    };

    var spatialReference = ((FeatureLayer)MyMap.Layers["CitiesFeatureLayer"]).Graphics.First().Geometry.SpatialReference;

    var bufferParams = new BufferParameters
    {
        BufferSpatialReference = spatialReference,
        OutSpatialReference = spatialReference,
        Unit = LinearUnit.Kilometer
    };

    bufferParams.Features.AddRange(((FeatureLayer)MyMap.Layers["CitiesFeatureLayer"]).Graphics);
    bufferParams.Distances.Add(100);

    geometryService.BufferAsync(bufferParams, userToken);
}

correct

Now the output is as expected as we are passing the userToken when we call the BufferAsync method and we use the UserState value from the passed in arguments in the BufferCompleted handler.

Now we know the problem and solution lets apply it to the original question of how to relate a graphic collection to the original values. The reason we need to do this is so that we have access to the attribute data for each graphic as well as the geometry. The markup and code look like.

<Grid x:Name="LayoutRoot" Background="White">
    <Grid.Resources>
        <esri:SimpleRenderer x:Key="MySimplePolygonRenderer">
            <esri:SimpleFillSymbol Fill="#01FFFFFF" BorderBrush="#88000000" BorderThickness="2" />
        </esri:SimpleRenderer>
        <esri:SimpleRenderer x:Key="MySimplePointRenderer">
            <esri:SimpleMarkerSymbol Color="Red" Size="6" Style="Diamond" />
        </esri:SimpleRenderer>
    </Grid.Resources>
        
    <Grid.ColumnDefinitions>
        <ColumnDefinition />
        <ColumnDefinition />
    </Grid.ColumnDefinitions>

    <esri:Map x:Name="MyMap" Grid.Column="0" Grid.ColumnSpan="2" WrapAround="True" Extent="-15000000,2000000,-7000000,8000000">
        <esri:ArcGISTiledMapServiceLayer ID="StreetMapLayer"
                Url="http://services.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer"/>

        <esri:GraphicsLayer ID="BufferedCities" Renderer="{StaticResource MySimplePolygonRenderer}" RendererTakesPrecedence="True">
            <esri:GraphicsLayer.MapTip>
                <Border CornerRadius="10" BorderBrush="Black" Background="White" BorderThickness="1" Margin="0,0,15,15">
                    <StackPanel Margin="7">
                        <TextBlock Text="Text from buffered geometry" FontWeight="Bold" Foreground="Black"  />
                        <TextBlock Text="{Binding [CITY_NAME]}" FontWeight="Bold" Foreground="Black"  />
                    </StackPanel>
                </Border>
            </esri:GraphicsLayer.MapTip>
        </esri:GraphicsLayer>

        <esri:FeatureLayer ID="CitiesFeatureLayer"
                Url="http://sampleserver1.arcgisonline.com/ArcGIS/rest/services/Specialty/ESRI_StatesCitiesRivers_USA/MapServer/0" 
                Where="POP1990 > 500000"                                
                Renderer="{StaticResource MySimplePointRenderer}" 
                OutFields="CITY_NAME,POP1990">
            <esri:FeatureLayer.MapTip>
                <Border CornerRadius="10" BorderBrush="Black" Background="White" BorderThickness="1" Margin="0,0,15,15">
                    <StackPanel Margin="7">
                        <TextBlock Text="{Binding [CITY_NAME]}" FontWeight="Bold" Foreground="Black"  />
                    </StackPanel>
                </Border>
            </esri:FeatureLayer.MapTip>
        </esri:FeatureLayer>
    </esri:Map>
        
    <Button Grid.Column="1" VerticalAlignment="Top" HorizontalAlignment="Right" Click="Button_Click">Buffer</Button>
</Grid>
public partial class MainPage : UserControl
{
    public MainPage()
    {
        InitializeComponent();
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        // We want a reference to the graphics from the FeatureLayer, this will be our userToken
        GraphicCollection userToken = ((FeatureLayer)MyMap.Layers["CitiesFeatureLayer"]).Graphics;

        var geometryService = new GeometryService(@"http://sampleserver3.arcgisonline.com/ArcGIS/rest/services/Geometry/GeometryServer");

        geometryService.BufferCompleted += (sender2, e2) =>
        {
            // The userToken is in the GraphicEventArgs 
            // We need to cast it back to the type that we passed in
            if (e2.Results.Any())
                BufferComplete(e2.Results, (IEnumerable<Graphic>) e2.UserState);
        };

        var spatialReference = userToken.First().Geometry.SpatialReference;

        var bufferParams = new BufferParameters
        {
            BufferSpatialReference = spatialReference,
            OutSpatialReference = spatialReference,
            Unit = LinearUnit.Kilometer
        };

        bufferParams.Features.AddRange(userToken);
        bufferParams.Distances.Add(100);

        geometryService.BufferAsync(bufferParams, userToken);
    }

    /// <summary>
    /// Called from a successful buffer operation
    /// </summary>
    /// <param name="bufferedGeometries">The result of the buffer. Graphic collection but only contains the geometries.</param>
    /// <param name="userToken">the original graphic collection which has the point geometry and the attribute data.</param>
    private void BufferComplete(IList<Graphic> bufferedGeometries, IEnumerable<Graphic> userToken)
    {
        var citiesGraphicsLayers = (GraphicsLayer)MyMap.Layers["BufferedCities"];

        // We need to iterate through the results and copy the attribute values from the original data 
        for (int i = 0; i < bufferedGeometries.Count(); i++)
        {
            bufferedGeometries[i].Attributes.AddRange(userToken.ElementAt(i).Attributes);

            citiesGraphicsLayers.Graphics.Add(bufferedGeometries[i]);
        }
    }
}

public static class Collection
{
    public static void AddRange<T>(this ICollection<T> collection, IEnumerable<T> items)
    {
        var list = collection as List<T>;

        if (list == null)
            foreach (var item in items)
                collection.Add(item);
        else
            // Optimization for case when collection is or derives from List<T>.
            list.AddRange(items);
    }
}

This time the userToken is the graphic collection from the feature layer and we buffer the original point data by 100km before adding the resultant geometries back onto our map display. As both layers use maptips we can see the data by hovering over the geometry.

1

2

This technique has a plethora of applications in code so it’s worth getting familiar with using it and seeing how you can apply it.

Mixing the Silverlight and JavaScript Esri Web APIs

OK so you may have read the title and thought why would anyone want to do this. Well I can think of a couple of reasons

  • You are more comfortable working with the Esri Silverlight API for creating your GIS workflow as it has better debugging support, may perform better, easier to test complex operations etc.
  • You prefer to design your layout using html
  • You want separation of your main map UI and peripheral controls
  • Because you can! Smile

Communicating between Silverlight and JavaScript is pretty trivial. To go from JavaScript –> Silverlight you need to register the object and define the methods that can be accessed via JavaScript.

// In the constructor
HtmlPage.RegisterScriptableObject("Page", this);            

[ScriptableMember]        
public void DoSomething() 
{     
    // Do something here   
}

To go from Silverlight –> JavaScript you call HtmlPage.Window.Invoke and pass in the relevant parameters. For this example I’ll be concentrating on Silverlight –> JavaScript calls to create a scalebar and legend widget using the Esri JS API with the Silverlight map control.

First we’ll create our Silverlight project and add the map control with some layers and toolkit controls

<UserControl x:Class="SampleSlwithJs.Silverlight.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:esri="http://schemas.esri.com/arcgis/client/2009">
    <Grid x:Name="LayoutRoot" >
    <esri:Map WrapAround="True" x:Name="MyMap">
        <esri:ArcGISTiledMapServiceLayer ID="TiledLayer"
                Url="http://services.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer" />
        <esri:ArcGISDynamicMapServiceLayer ID="DynamicLayer" 
                Url="http://sampleserver3.arcgisonline.com/ArcGIS/rest/services/Earthquakes/RecentEarthquakesRendered/MapServer" />    
    </esri:Map>

    <esri:Navigation Margin="5" HorizontalAlignment="Left" VerticalAlignment="Bottom"
                         Map="{Binding ElementName=MyMap}"></esri:Navigation>

    <esri:MagnifyingGlass x:Name="MyMagnifyingGlass" Visibility="Visible" ZoomFactor="3"
                              Map="{Binding ElementName=MyMap}" >
        <esri:MagnifyingGlass.Layer>
        <esri:ArcGISTiledMapServiceLayer ID="StreetMapLayer" 
                    Url="http://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer"/>
        </esri:MagnifyingGlass.Layer>
    </esri:MagnifyingGlass>
    </Grid>
</UserControl>

In the code behind we can now wire up when we want to make the calls to the JavaScript functions. As we’re going to have a scale bar and legend we need to know what the map extent is going to be and what layers we are interested in.

using System.Windows.Browser;
using System.Linq;
using System.Windows.Controls;
using ESRI.ArcGIS.Client;

namespace SampleSlwithJs.Silverlight
{
    public partial class MainPage : UserControl
    {
        public MainPage()
        {
            InitializeComponent();
            MyMap.Layers.LayersInitialized += LayersInitialized;
        }

        void LayersInitialized(object sender, System.EventArgs args)
        {
            HtmlPage.Window.Invoke("Initialise", MyMap.Extent.XMin, MyMap.Extent.YMin, MyMap.Extent.XMax, MyMap.Extent.YMax,
                MyMap.SpatialReference.WKID,
                MyMap.Layers.OfType<ArcGISDynamicMapServiceLayer>().First().Url);

            MyMap.ExtentChanged += MapExtentChanged;
            // Call this to create the scalebar for the inital view
            HtmlPage.Window.Invoke("MapExtentChanged", MyMap.Extent.XMin, MyMap.Extent.YMin, MyMap.Extent.XMax, MyMap.Extent.YMax);
        }

        void MapExtentChanged(object sender, ExtentEventArgs e)
        {
            HtmlPage.Window.Invoke("MapExtentChanged", e.NewExtent.XMin, e.NewExtent.YMin, e.NewExtent.XMax, e.NewExtent.YMax);
        }
    }
}

From this code you can see that we are calling two JavaScript functions, one when the map layers are initialised and one each time the map extent changes.

Now you need to add the JavaScript code. The content of your html page should be similar to

<script type="text/javascript">    djConfig = { parseOnLoad: true }; </script>
<script type="text/javascript" src="http://serverapi.arcgisonline.com/jsapi/arcgis/?v=2.4compact"></script>
<script type="text/javascript">
    dojo.require("esri.map");
    dojo.require("esri.dijit.Scalebar");
    dojo.require("esri.dijit.Legend"); 
</script>
<script type="text/javascript">
    var scalebar;
    var map;
    function Initialise(xmin, ymin, xmax, ymax, wkid, layerUrl) {
        var initExtent = new esri.geometry.Extent({ "xmin": xmin, "ymin": ymin, "xmax": xmax, "ymax": ymax, "spatialReference": { "wkid": wkid} });
        map = new esri.Map("map", { extent: initExtent });
        esri.hide(dojo.byId("map"));

        var dynamicLayer = new esri.layers.ArcGISDynamicMapServiceLayer(layerUrl, { "opacity": 0.5 });
        map.addLayer(dynamicLayer);
        var legendDijit = new esri.dijit.Legend({ map: map }, "legend");
        legendDijit.startup();
    }
    function MapExtentChanged(xmin, ymin, xmax, ymax) {

        if (map == 'undefined') return;
        var extent = new esri.geometry.Extent({ "xmin": xmin, "ymin": ymin, "xmax": xmax, "ymax": ymax });
        map.setExtent(extent);

        scalebar = new esri.dijit.Scalebar({
        map: map,
        scalebarUnit: "metric"
        }, dojo.byId("scalebar"));
    }
</script>
<div style="height: 600px;">
<div id="silverlightControlHost" style="width: 600px; height: 400px; float: left; position: relative;">
    <object data="data:application/x-silverlight-2," type="application/x-silverlight-2" width="100%" height="100%">
    <param name="source" value="ClientBin/SampleSlwithJs.Silverlight.xap" />
    <param name="onError" value="onSilverlightError" />
    <param name="background" value="white" />
    <param name="minRuntimeVersion" value="4.0.60310.0" />
    <param name="autoUpgrade" value="true" />
    <a href="http://go.microsoft.com/fwlink/?LinkID=149156&v=4.0.60310.0" style="text-decoration: none">
    <img src="http://go.microsoft.com/fwlink/?LinkId=161376" alt="Get Microsoft Silverlight" style="border-style: none" />
    </a>
    </object>
    <iframe id="_sl_historyFrame" style="visibility: hidden; height: 0px; width: 0px; border: 0px"></iframe>
    <div id="scalebar"></div>
    <div id="map"></div>
</div>
<div id="legend" style="float: right; width: 240px; height: 400px; overflow: hidden; overflow-y: auto;"></div>    
</div>

Now when we run the application we see our Silverlight map control and our JavaScript scale bar and legend controls (the screenshot is showing an ASP.NET MVC template site but you can run it in any html page).

image

Each time the map extent changes the scale bar control is automatically updated to reflect it.

image

Now this code isn’t production quality (brownie points for whoever spots the error and knows what is causing it) and I’m not saying you should go out and use this approach for the next application you work on but its fun to have a play around yourself. Let me know if you find this information useful.

Getting Started with the ArcGIS for Windows Mobile SDK

My initial thoughts on working with this SDK reminded me of the first time I worked with ArcObjects; lots of power but it can be daunting knowing where to begin. I’ve put together this post to aid developers looking to utilise this SDK and hit the ground running when building applications for Windows Mobile 6 on rugged devices.

Version Control

The starting point for a lot of people will be creating a new project which raises the question of what version of Visual Studio and the .NET Compact Framework to use? Well unfortunately VS2010 isn’t supported so that leaves us with VS2008. For the compact framework you get the choice of either 2.0 or 3.5. The ArcGIS Mobile SDK is designed for working with 2.0 but you can use 3.5, the downside being that the Visual Studio design mode won’t show the ArcGIS integration. This means that you can’t use the toolbox and designer to create your UI, not that big a deal really as I find the VS UI tooling pretty horrible anyway. If you use 3.5 you will need to create your controls using code when your form or user control is initialised. For the map you may do something like

public static class MapHelper 
{ 
    private const double MinScale = 1000; 
 
    public static Map BuildMapControl(int width, int height) 
    { 
        if (width < 1) 
            throw new ArgumentOutOfRangeException("width", "Width must be greater than zero.");  
 
        if (height < 1) 
            throw new ArgumentOutOfRangeException("height", "Height must be greater than zero."); 
 
        var map = new Map
        { 
            Location = new Point(0, 0), 
            Size = new Size(width, height), 
            Anchor = 
              System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right | 
              System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Top, 
            TabIndex = 0 
        }; 
 
        new ScaleBar 
        { 
            Map = map, 
            DisplayPosition = ScaleBarDisplayPosition.TopLeft, 
            UnitSystem = ScaleBarUnitSystem.Metric, 
            Width = 200, 
            BarColor = Color.Black, 
            HaloColor = Color.White 
        };
 
        // prevent the map zooming in too far 
        map.ExtentChanged += (sender, e) => 
        { 
            var mapCtrl = sender as Map;  
            if (mapCtrl == null) 
                return; 
 
            if (mapCtrl.Scale < MinScale) 
                mapCtrl.Scale = MinScale; 
        }; 
 
        return map; 
    } 
}

Then in your form constructor you would call

ESRI.ArcGIS.Mobile.Map map = MapHelper.BuildMapControl(480, 500); 
this.Controls.Add(map); 
 
// Define the map actions that can be used to interact with the map control 
var panMapAction = new PanMapAction(); 
map.MapActions.Add(panMapAction);
 
map.CurrentMapAction = panMapAction;

Working with Data

The most common data format that you will be working with is the MobileCache. When we work with the newer web APIs we think of the term cache as meaning pre cooked image tiles commonly used for fast rendering of complex symbology but in the Windows Mobile SDK a MobileCache can be thought of as dynamic data, kind of like working with data from a dynamic map service or fgdb. The MobileCache is a SQLite database that consists of a schema file containing the map document metadata i.e. what layers there are, how to draw them etc. and a .db file that contains the actual feature and attribute data. There is also a db-journal file created when the MobileCache is open. To use a MobileCache in your application you must first open it from it’s location on disk (this code assumes the data already exists).

private MobileCache OpenCache(string cachePath) 
{ 
    // Set the cache storage location on disk 
    var mobileCache = new MobileCache(cachePath); 
 
    if (!mobileCache.IsValid) return null; 
 
    try 
    { 
        mobileCache.Open(); 
    } 
    catch (Exception ex) 
    { 
        // do something with this 
    } 
 
    return mobileCache.IsEmpty ? null : mobileCache; 
}

If you want to use a traditional cache then you can use the ESRI.ArcGIS.Mobile.DataProducts.RasterData TileCacheMapLayer type. This works from an existing set of tiled images and a tiling scheme file that can be copied onto the device. Be careful when opening data from multiple places in code as you are responsible for opening and closing the data and ensuring it isn’t locked.

public TileCacheMapLayer LoadBaseMap(string path) 
{ 
    var basemap = new TileCacheMapLayer(path); 
    basemap.TileRequestError += (sender, e) => { throw e.TileRequestException; }; 
    basemap.Open(); 
 
    return basemap; 
}

Once you have the data you can add it to your map control as a layer. The draw order is last one added gets drawn first i.e. looks the same order as ArcMap but opposite to Web APIs. Each datasource that is added to the map has to be in the same projection, you can’t mix and match spatial references as there is no built in projection on the fly.

Syncing Data with ArcGIS Server

Whilst it is useful to be able to work with data copied locally onto a device a much more powerful option is being able to sync data between ArcGIS Server and a device. This can be done against a service that has been published with the Mobile Access enabled (note that in order for a service to be enabled for mobile access it must have an explicit extent defined in its underlying map document). The following code shows how to download data filtered by extent and a definition expression, this is to restrict the number of features that are downloaded as we don’t want to get the complete dataset if it is large.

public void Download(string serviceEndpoint, string cachePath, IDictionary<string, IEnumerable<string>> layerQueries) 
{ 
    // Code for downloading features from an ArcGIS service 
    var mobileCache = new MobileCache(cachePath); 
 
    // If the cache already exists then close it. 
    if (mobileCache.CacheExists && mobileCache.IsOpen) 
        mobileCache.Close(); 
 
    try 
    { 
        var serviceConnection = new MobileServiceConnection { Url = serviceEndpoint };
 
        // Create the local cache files on the device and open the connection to the service 
        serviceConnection.CreateCache(mobileCache);
 
        mobileCache.Open(); 
 
        foreach (var layer in layerQueries) 
        { 
            if (layer.Value == null || !layer.Value.Any()) 
                continue;
  
            FeatureLayer featureLayer = mobileCache.FeatureLayers.Single(fl => fl.Name == layer.Key); 
 
            var agent = new FeatureLayerSyncAgent(featureLayer, serviceConnection) 
            { 
                SynchronizationDirection = SyncDirection.DownloadOnly, 
                DownloadFilter = MakeLayerQuery(featureLayer, layer.Value.ToArray()) 
            };
 
            agent.Synchronize(); 
        } 
    } 
    finally 
    { 
        if (mobileCache != null && mobileCache.CacheExists && mobileCache.IsOpen) 
            mobileCache.Close(); 
    } 
} 
 
internal static QueryFilter MakeLayerQuery(FeatureLayer layer, string[] ids) 
{ 
    if (ids == null || !ids.Any()) 
        return new QueryFilter(new Envelope(771809.111674345, 4675666.8666, 2407100.82832565, 6266536.3974), 
                               GeometricRelationshipType.Within); 
 
    if (ids.Count() == 1) 
        return new QueryFilter( 
            new Envelope(771809.111674345, 4675666.8666, 2407100.82832565, 6266536.3974), 
            GeometricRelationshipType.Within, 
            string.Format("{0} = '{1}'", layer.DisplayColumnName, ids.First())); 
 
    // Here we are assuming that the service is configured to have the DisplayColumnName as the correct value 
    return new QueryFilter( 
        new Envelope(771809.111674345, 4675666.8666, 2407100.82832565, 6266536.3974), 
        GeometricRelationshipType.Within, 
        string.Format("{0} in ('{1}')", layer.DisplayColumnName, string.Join("','", ids))); 
}

Looking deeper into the code, the call to serviceConnection.CreateCache(mobileCache); connects to the specified mobile enabled ArcGIS Server service url and downloads the service schema, creating the MapSchema.bin file under the directory location specified. The subsequent call to mobileCache.Open(); creates the MobileCache.db (this is the actual SQLite feature database) and MobileCache.db-journal files. As these file names are the same for each MobileCache it is worth creating each set of data in a different folder location on disk.

If you want to provide some feedback to the user of your application you can hook into the agent.StateChanged and agent.ProgressChanged events. The sequence for a download is

State Ready

State Synchronizing

SyncPhase Downloading, SyncStep – DecomposingExtent

SyncPhase Downloading, SyncStep – ExtentDecomposed, also has the number of features to synchronise

SyncPhase Downloading, SyncStep – DownloadingData

SyncPhase Downloading, SyncStep – DataCached

State Ready

The code required for doing an upload is almost identical, but usually without the need to filter the upload as we want to make sure all our captured data is sent back to the server. The differences are

FeatureLayer featureLayer = mobileCache.FeatureLayers.Single(fl => fl.Name == layerName);
 
if (!featureLayer.HasEdits) 
    continue; 
 
var agent = new FeatureLayerSyncAgent(featureLayer, serviceConnection) 
{ 
    SynchronizationDirection = SyncDirection.UploadOnly 
};
 
agent .Synchronize();

Note that we are only interested in layers that have edits by checking the HasEdits property. If you do want to be able to edit data you must ensure that the underlying feature class has the GlobalID column added to it.

Custom Layers

After you have data on a device another common scenario would be a custom way of visualising it. It’s fairly easy to knock up your own MapActions and Layers so I won’t go into this in detail but a quick tip is that there are some helpers in the SDK for rendering the data so that it looks nice. When you create a custom layer that extends MapGraphicLayer you have to override the Draw method. Rather than using the Graphics to draw your features you can create your own symbol and then use that. For example

public class SampleLayer : MapGraphicLayer 
{ 
    private Font _font; 
    private Symbol _pointSymbol; 
 
    public SampleLayer() : base("Sample layer") 
    { 
        _font = new Font("Tahoma", 24, FontStyle.Bold); 
        _pointSymbol = new Symbol(
            new PointPaintOperation(Color.Red, 1, 0, Color.Red, 0, 6, PointPaintStyle.Circle)); 
    } 
 
    protected override void Dispose(bool disposing) 
    { 
        try 
        { 
            if (disposing) 
            { 
                if (_pointSymbol != null) 
                    _pointSymbol.Dispose(); 
                _pointSymbol = null; 
 
                if (_font != null) 
                    _font.Dispose(); 
                _font = null; 
 
                Location = null; 
            } 
        } 
        finally 
        { 
            base.Dispose(disposing); 
        } 
    } 
 
    private Coordinate _location; 
    public Coordinate Location 
    { 
        get { return _location; } 
        set 
        { 
            _location = value; 
            // notify the map control that you want to redraw this layer 
            OnDataChanged(new DataChangedEventArgs(Map.GetExtent()));                
        } 
    } 
 
    protected override void Draw(Display display) 
    { 
        if (display.DrawingCanceled || !Visible || Map == null || Location == null) 
            return; 
  
        _pointSymbol.DrawPoint(display, Location);
 
        if (display.DrawingCanceled || Location == null) return;
 
        display.DrawText(string.Format("{0:0.000}, {1:0.000}", Location.X, Location.Y), 
            _font, Color.Black, Color.White, Location, TextAlignment.BottomCenter); 
    } 
}

There are a number of different map actions and drawing options to use or extend so I recommend digging into the SDK API reference to find the best match for your needs.


I hope you find this information useful and any feedback is always appreciated.

Saving the Map State to IsolatedStorage with the ArcGIS Silverlight API

A common scenario when using an application is to have some means of saving the state. In this sample I’ll be taking the Esri Map control and saving the last extent and visible layers so that the next time the application starts the settings are remembered.

Ideally we would be able to bind all the properties using the TwoWay binding built into Silverlight though this is not always practical or possible. Map extent is a good example of this as firstly it is not a DependencyProperty and secondly, the value that we save or set for the extent may not be the actual extent value e.g. the browser window size has changed. With a little bit of code we can achieve what we want.

To start we’ll define our state model base (I would say view model as it represents the same thing but we aren’t using binding, either way you get the picture). 

public abstract class StateModelBase 
{
    public void Save(IPersistable persistance)
    {
        persistance.Save(this);
    }
}

Now we can define the map state class, this will handle the state management for the Map control and extends our StateModelBase. This could contain anything you want to store such as child layer definitions etc.

[KnownType(typeof(MapState))]
public class MapState : StateModelBase
{
    private Map _map;

    public MapState()
    {
        LayersVisible = new Dictionary<string, bool>();
    }

    /// <summary>
    /// Extent of the <c>Map</c>
    /// </summary>
    [DataMember]
    public Envelope Extent { get; set; }

    /// <summary>
    /// The visible state of each <c>Layer</c> in the <c>Map</c> <c>LayersCollection</c>
    /// </summary>
    [DataMember]
    public IDictionary<string, bool> LayersVisible { get; set; }

    /// <summary>
    /// Called to set the <c>Map</c> control to persist settings for 
    /// </summary>
    /// <param name="map"></param>
    public void Initialise(Map map)
    {
        _map = map;

        if (Extent == null)
            return;

        if (Extent != _map.Extent)
            _map.ZoomTo(Extent);

        foreach (var item in LayersVisible.Where(item => _map.Layers[item.Key] != null))
        {
            _map.Layers[item.Key].Visible = item.Value;
        }
    }

    /// <summary>
    /// Update the state values.
    /// </summary>
    public void Update()
    {
        Extent = _map.Extent;
        LayersVisible = _map.Layers.Where(layer => !string.IsNullOrWhiteSpace(layer.ID)).ToDictionary(layer => layer.ID, layer => layer.Visible);
    }
}

Now we have what we want to persist we need the mechanism to do it. For interacting with the IsolatedStorage we can use the following code.

public interface IPersistable
{
    T Load<T>() where T : StateModelBase;
    void Save<T>(T viewModel) where T : StateModelBase;
}
/// <summary>
/// Implementation of <c>IPersistable</c> that persists to and from Isolated Storage
/// </summary>
public class IsolatedStoragePersitence : IPersistable
{
    private readonly string _applicationName;

    public IsolatedStoragePersitence(string applicationName)
    {
        _applicationName = applicationName;
    }

    private string GetIsFile(Type t)
    {
        var assemblyName = new AssemblyName(t.Assembly.FullName);
        return string.Format("{0}_{1}_{2}.dat", _applicationName, t.Name, assemblyName.Version);
    }
        
    /// <summary>
    /// Load the <c>StateModelBase</c> from Isolated Storage 
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    public T Load<T>() where T : StateModelBase
    {
        //return null;
        string data = string.Empty;

        if (IsolatedStorageFile.IsEnabled)
        {
            using (var userAppStore = IsolatedStorageFile.GetUserStoreForApplication())
            {
                var dataFileName = GetIsFile(typeof(T));
                if (userAppStore.FileExists(dataFileName))
                {
                    using (var iss = userAppStore.OpenFile(dataFileName, FileMode.Open))
                    {
                        using (var sr = new StreamReader(iss))
                        {
                            data = sr.ReadToEnd();
                        }
                    }
                }
            }
        }

        if (string.IsNullOrWhiteSpace(data))
            return null;

        var serializer = new DataContractJsonSerializer(typeof(T));

        var stream = new MemoryStream(Encoding.UTF8.GetBytes(data));

        var viewModel = (T)serializer.ReadObject(stream);

        return viewModel;
    }

    /// <summary>
    /// Save the <c>StateModelBase</c> to Isolated Storage 
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="viewModel"></param>
    public void Save<T>(T viewModel) where T : StateModelBase
    {
        if (!IsolatedStorageFile.IsEnabled)
            return;
        var dataFileName = GetIsFile((viewModel.GetType()));
        using (var userAppStore = IsolatedStorageFile.GetUserStoreForApplication())
        {
            if (userAppStore.FileExists(dataFileName))
                userAppStore.DeleteFile(dataFileName);

            using (var iss = userAppStore.CreateFile(dataFileName))
            {
                using (var sw = new StreamWriter(iss))
                {
                    string dataAsString;

                    using (var ms = new MemoryStream())
                    {
                        var serializer = new DataContractJsonSerializer(typeof(T));
                        serializer.WriteObject(ms, viewModel);
                        ms.Position = 0;
                        using (var sr = new StreamReader(ms))
                        {
                            dataAsString = sr.ReadToEnd();
                        }
                    }

                    sw.Write(dataAsString);
                    sw.Close();
                }
            }
        }
    }
}

The last thing to take care of is wiring up the parts. As we want to load our state when the application runs and save the state when it exits we can hook into the Application events. In our App.xaml.cs add

internal static new App Current { get { return (App)Application.Current; } }

internal IPersistable Persistance { get; private set; }

private void Application_Startup(object sender, StartupEventArgs e)
{
    Persistance = new IsolatedStoragePersitence("SamplePersistence");
    RootVisual = new MainPage();
}

private void Application_Exit(object sender, EventArgs e)
{
    ((MainPage)RootVisual).MapState.Update();
    ((MainPage)RootVisual).MapState.Save(Persistance);
}

and in our MainPage.xaml.cs class we can add

public partial class MainPage
{
    public MapState MapState { get; set; }

    public MainPage()
    {
        InitializeComponent();

        // Load our viewmodels
        MapState = App.Current.Persistance.Load<MapState>() ?? new MapState();
        MapState.Initialise(MyMap);
    }
}

Finally we need a Map and some layers in the UI. I’ve taken some markup from the Esri samples but feel free to add anything you like and play around with the code.

<UserControl x:Class="SilverlightPersistence.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
    xmlns:esri="http://schemas.esri.com/arcgis/client/2009" 
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">

    <Grid x:Name="LayoutRoot" Background="White">
        <esri:Map x:Name="MyMap" WrapAround="True" Extent="-15000000,3600000,-11000000,5300000" Background="White">
            <esri:ArcGISTiledMapServiceLayer ID="World"
                Url="http://services.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer" />
            <esri:ArcGISTiledMapServiceLayer ID="ShadedRelief" 
                    Url="http://services.arcgisonline.com/ArcGIS/rest/services/World_Shaded_Relief/MapServer" Visible="False"/>
            <esri:ArcGISDynamicMapServiceLayer ID="California" VisibleLayers="8,10"
                    Url="http://maverick.arcgis.com/ArcGIS/rest/services/California/MapServer" />
            <esri:FeatureLayer ID="Earthquakes" Mode="OnDemand"
                    Url="http://sampleserver3.arcgisonline.com/ArcGIS/rest/services/Earthquakes/EarthquakesFromLastSevenDays/MapServer/0" />
        </esri:Map>

        <Border Background="#77919191" BorderThickness="1" CornerRadius="5"
            HorizontalAlignment="Right"  VerticalAlignment="Top"
            Margin="20" Padding="5" BorderBrush="Black" >
            <ListBox x:Name="MyList" ItemsSource="{Binding ElementName=MyMap, Path=Layers}">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <StackPanel Orientation="Horizontal">
                            <!--Layer visibility checkbox-->
                            <CheckBox IsChecked="{Binding Visible, Mode=TwoWay}" />                            
                            <!--Layer name-->
                            <TextBlock Text="{Binding ID, Mode=OneWay}" Margin="5,0,0,0" > 
                            <!-- Tooltip on hover-->
                                <ToolTipService.ToolTip>
                                    <StackPanel MaxWidth="400">
                                        <TextBlock FontWeight="Bold" Text="{Binding CopyrightText}" TextWrapping="Wrap" />
                                        <TextBlock Text="{Binding Description}" TextWrapping="Wrap" />
                                    </StackPanel>
                                </ToolTipService.ToolTip>
                            </TextBlock>
                        </StackPanel>
                    </DataTemplate>
                </ListBox.ItemTemplate>
            </ListBox>
        </Border>

    </Grid>
</UserControl>

Build and run your app and next time you run it the state will be remembered. Hopefully you can use this example as a starting point for your coding.

Dynamically Loading Layers at Runtime with Silverlight

If you’ve ever worked on a ArcGIS Server project that involves more than one environment then chances are you’ve encountered the scenario where your map service endpoints need to be changed when migrating from DEV to TEST and PROD and so on. There are a few ways to handle this such as hard coding the url’s and then recompiling for each environment (don’t do that Smile), storing the url’s in the web.config (ok, but it’s not that flexible if you want to add or remove layers) or you could store the config in a database and use a service to load the data. Each of these methods has it’s pros and cons but I’ve been using another method which is useful so let’s take a look.

First we need to create a new Silverlight project in Visual Studio and add the web project to host it. Add a reference for the latest ESRI.ArcGIS.Client then paste the following into the MainPage.xaml (be careful of carriage returns I had to insert for formatting).

<UserControl x:Class="Sample.DynamicLayerLoading.MainPage"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  xmlns:esri="clr-namespace:ESRI.ArcGIS.Client;assembly=ESRI.ArcGIS.Client"
  mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400">

  <Grid x:Name="LayoutRoot" Background="White">
    <esri:Map WrapAround="True" x:Name="Map">
      <esri:ArcGISTiledMapServiceLayer ID="BaseMapLayer"
        Url="http://services.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer" />
    </esri:Map>
  </Grid>
</UserControl>

If you run the application you should see a map

image

Now add a file called Layers.xaml to the root of your web site. The solution structure should be

image

Within this file you can put any valid Xaml for map layers, just remember to add the correct namespace declaration and enclose the layers in a <esri:LayerCollection> tag. Mine contains

<esri:LayerCollection
  xmlns:esri="http://schemas.esri.com/arcgis/client/2009"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

  <esri:ArcGISDynamicMapServiceLayer ID="MyLayer"
    Url="http://maverick.arcgis.com/AWorld_WGS84/MapServer"
    VisibleLayers="1,2" Opacity="0.5"/>

  <esri:FeatureLayer ID="MyFeatureLayer"
    Url="http://sampleserver1.arcgisonline.com/ArcGIS/rest/services/Specialty/ESRI_StatesCitiesRivers_USA/MapServer/0"
    Where="POP1990 > 100000" />
</esri:LayerCollection>

Lastly you need to paste the following into the MainPage.xaml.cs file

using System;
using System.Linq;
using System.Net;
using System.Windows.Browser;
using ESRI.ArcGIS.Client;

namespace Sample.DynamicLayerLoading
{
   public partial class MainPage
   {
       public MainPage()
       {
           InitializeComponent();
           Map.LoadLayersFromXaml(
               OnLoadLayersFromXamlComplete,
               HtmlPage.Document.DocumentUri.ToString()
                 .Substring(0, HtmlPage.Document.DocumentUri.ToString().LastIndexOf('/')),
               1);
       }

       private void OnLoadLayersFromXamlComplete()
       {
            // TODO : whatever you want after the layers are initialised 
       }
   }

   internal static class MapExtensions
   {
       /// <summary> 
       /// Loads the layer collection from the downloaded xaml file. 
       /// </summary> 
       /// <param name="map"></param> 
       /// <param name="onCompleted">The callback to execute once the method has finished processing</param> 
       /// <param name="absoluteUrlBase">The absolute url to the application root. Used to determine proxy url</param> 
       /// <param name="startingIndex">The index at which to start inserting the map layers</param> 
       public static void LoadLayersFromXaml(this Map map, Action onCompleted, string absoluteUrlBase, int startingIndex)
       {
           var client = new WebClient();
           client.DownloadStringCompleted += (sender, args) =>
           {
               // Parse xaml to a LayerCollection  
               string xaml = args.Result;
               var layerCollection = System.Windows.Markup.XamlReader.Load(xaml);
               var layers = (layerCollection as LayerCollection);

               if (layers != null)
                   for (var i = 0; i < layers.Count; i++)
                   {
                       if (layers[i] is ArcGISTiledMapServiceLayer && !string.IsNullOrWhiteSpace(((ArcGISTiledMapServiceLayer)layers[i]).ProxyURL))
                           ((ArcGISTiledMapServiceLayer)layers[i]).ProxyURL = string.Format("{0}{1}", absoluteUrlBase, ((ArcGISTiledMapServiceLayer)layers[i]).ProxyURL.Split('/').Last());
                       else if (layers[i] is ArcGISImageServiceLayer && !string.IsNullOrWhiteSpace(((ArcGISImageServiceLayer)layers[i]).ProxyURL))
                           ((ArcGISImageServiceLayer)layers[i]).ProxyURL = string.Format("{0}{1}", absoluteUrlBase, ((ArcGISImageServiceLayer)layers[i]).ProxyURL.Split('/').Last());
                       else if (layers[i] is ArcGISDynamicMapServiceLayer && !string.IsNullOrWhiteSpace(((ArcGISDynamicMapServiceLayer)layers[i]).ProxyURL))
                           ((ArcGISDynamicMapServiceLayer)layers[i]).ProxyURL = string.Format("{0}{1}", absoluteUrlBase, ((ArcGISDynamicMapServiceLayer)layers[i]).ProxyURL.Split('/').Last());
                        else if (layers[i] is FeatureLayer && !string.IsNullOrWhiteSpace(((FeatureLayer)layers[i]).ProxyUrl))
                            ((FeatureLayer)layers[i]).ProxyUrl = string.Format("{0}{1}", absoluteUrlBase, ((FeatureLayer)layers[i]).ProxyUrl.Split('/').Last());
                        map.Layers.Insert(i + startingIndex, layers[i]);
                    }
                onCompleted();
            };

            client.DownloadStringAsync(new Uri(string.Format("{0}/Layers.xaml", absoluteUrlBase), UriKind.Absolute));
       }
   }
}

Run the application again and now you will see

image

The layers are dynamically loaded at runtime allowing you to change them without needing to rebuild your xap file. One thing to note is that you do not want to use this technique to load a Bing Imagery service with the token specified as this would be available to anyone sniffing the downloaded Layers.xaml file.