Month: August 2011

Where is IServerLayer?

Anytime you use an API you are going to find things that you like and things you want to work differently. After working against the Web ADF I was really pleased with the work the teams at Esri did on the Web APIs, particularly the Silverlight API which I have used on a variety of projects over the past 3 or so years. That isn’t to say it doesn’t have any quirks or issues, that’s just the nature of the beast, especially as more and more functionality is added as well as trying to prevent breaking changes to maintain backwards compatibility. I actually hope they make the changes they want with the next major release even if it does mean breaking changes as house keeping and replacing legacy code is better than working around it.

One of the more frequent issues I run into using the Silverlight API is converting server side layers into another type so I can use them for some custom operation e.g. running an identify task for each server side layer in the map. I determine a layer as being server side if it has a Url property. Here’s a way you can easily convert your layers with the aim of being able to extend the code for your own use.

First we’ll define our IServerLayer interface. This could easily be part of the core API which would take a lot of the hassle away here 🙂

public interface IServerLayer
{
    /// <summary>
    /// The url of the service endpoint.
    /// </summary>
    string Url { get; set; }

    /// <summary>
    /// The proxy url used by the service.
    /// </summary>
    string ProxyUrl { get; set; }
}

now the implementation and conversion

public class ServerLayer : IServerLayer
{
    public ServerLayer(string url, string proxyUrl)
    {
        if (string.IsNullOrEmpty(url))
            throw new ArgumentNullException("Url", "The layer url cannot be null.");
        Url = url;
        ProxyUrl = proxyUrl;
    }

    public static implicit operator ServerLayer(Layer layer)
    {
        if (layer is ArcGISTiledMapServiceLayer)
        {
            var tiledMapServiceLayer = (ArcGISTiledMapServiceLayer)layer;
            return new ServerLayer(tiledMapServiceLayer.Url, tiledMapServiceLayer.ProxyURL);
        }

        if (layer is ArcGISDynamicMapServiceLayer)
        {
            var dynamicMapServiceLayer = (ArcGISDynamicMapServiceLayer)layer;
            return new ServerLayer(dynamicMapServiceLayer.Url, dynamicMapServiceLayer.ProxyURL);
        }

        if (layer is ArcGISImageServiceLayer)
        {
            var imageServiceLayer = (ArcGISImageServiceLayer)layer;
            return new ServerLayer(imageServiceLayer.Url, imageServiceLayer.ProxyURL);
        }

        if (layer is FeatureLayer)
        {
            var featureLayer = (FeatureLayer)layer;
            return new ServerLayer(featureLayer.Url, featureLayer.ProxyUrl);
        }

        // TODO : add others layers you want to support, such as KmlLayer

        throw new NotSupportedException(string.Format("The type '{0}' for layer '{1}' is not supported", layer.GetType(), layer.ID));
    }

    public string Url { get; set; }

    public string ProxyUrl { get; set; }
}

and finally an example of calling it

foreach (var layer in MyMap.Layers)
{
    try
    {
        LayersList.Items.Add(string.Format("Url is {0}, for {1}", ((ServerLayer)layer).Url, layer.GetType()));
    }
    catch (NotSupportedException ex)
    {
        LayersList.Items.Add(ex.Message);
    }
}

which results in

serverlayer

This is just one way of achieving greater consistency throughout your code. As always thanks for reading and any feedback is appreciated.

Advertisements

Auto Refreshing Map Layers

More and more I see the need for data to be served up in real time or with an associated timestamp. The addition of time extents to data was a welcome addition to the ArcGIS stack but what if you just want to frequently refresh the display of data for layers within your map display with data that you know will be changing. The concept for this is very simple, you just need to refresh the layer at a defined interval. Within the Esri Silverlight API there are a number of layers that support this but they also need to have the DisableClientCaching property as this will allow you to tell the application to get a new version of the data rather than using the cached data from the browser. Here’s a simple way to add this functionality into your Esri Silverlight projects for any layer with the Refresh method and DisableClientCaching property using a bit of reflection, this includes your own layers that implement these. 

public class AutoRefresher : DependencyObject
{
    private DispatcherTimer _timer;

    public AutoRefresher()
    {
        Interval = 10;
    }

    /// <summary>
    /// Number of seconds between refreshes. Default is 10
    /// </summary>
    public int Interval { get; set; }

    /// <summary>
    /// Gets the refresher attached to the dependency object
    /// </summary>
    /// <param name="obj"></param>
    /// <returns></returns>
    public static AutoRefresher GetRefresher(DependencyObject obj)
    {
        return (AutoRefresher)obj.GetValue(RefresherProperty);
    }
    /// <summary>
    /// Attaches a refresher to a dependency object
    /// </summary>
    /// <param name="obj"></param>
    /// <param name="value"></param>
    public static void SetRefresher(DependencyObject obj, AutoRefresher value)
    {
        obj.SetValue(RefresherProperty, value);
    }

    /// <summary>
    /// Identifies the Refresher attached dependency property.
    /// </summary>
    public static readonly DependencyProperty RefresherProperty =
        DependencyProperty.RegisterAttached("Refresher", typeof(AutoRefresher), typeof(AutoRefresher), new PropertyMetadata(OnRefresherPropertyChanged));

    private static void OnRefresherPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var layer = d as Layer;
        if (layer == null) return;
        
        var newVal = e.NewValue as AutoRefresher;
        var oldVal = e.OldValue as AutoRefresher;

        if (oldVal != null)
            oldVal.DetachTimer(layer);

        if (newVal != null)
            newVal.AttachTimer(layer);        
    }

    private void DetachTimer(Layer layer)
    {
        if (_timer != null && _timer.IsEnabled)
            _timer.Stop();

        _timer = null;
    }

    private void AttachTimer(Layer layer)
    {
        var type = layer.GetType();

        var propertyInfos = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
        var propertyInfo = propertyInfos.FirstOrDefault(pi => string.Equals("DisableClientCaching", pi.Name, StringComparison.OrdinalIgnoreCase));

        MethodInfo methodInfo = null;
        if (propertyInfo != null)
        {
            // Set DisableClientCaching = true
            propertyInfo.SetValue(layer, true, null);
            methodInfo = type.GetMethod("Refresh", BindingFlags.Public | BindingFlags.Instance);
        }

        if (methodInfo == null)
            throw new NotSupportedException(
                "The layer type that the behavior is attached to does not support automatic data refresh.");

        _timer = new DispatcherTimer { Interval = new TimeSpan(0, 0, Interval) };
        _timer.Tick += (sender, e) =>
        {
            if (layer.Visible) methodInfo.Invoke(layer, null);
        };

        if (layer.IsInitialized && layer.InitializationFailure == null)
            _timer.Start();
        else
            layer.Initialized += (sender2, e2) => _timer.Start();
    }
}

Then in markup

<esri:ArcGISDynamicMapServiceLayer 
    ID="Blah" Url="your url">
    <local:AutoRefresher.Refresher>
        <local:AutoRefresher Interval="5" />
    </local:AutoRefresher.Refresher>                
</esri:ArcGISDynamicMapServiceLayer>

or code

var layer = MyMap.Layers["Blah"] as ArcGISDynamicMapServiceLayer;            
var refresher = new AutoRefresher { Interval = 5 };
layer.SetValue(AutoRefresher.RefresherProperty, refresher);

Depending on your requirements you may want to do something as simple as update the TimeExtent on the map control or for specific layers but this gives you another mechanism to work with real time data. Also working with time is always a headache especially when using data from multiple time zones so experiment to find the best solution for your needs.

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.