Migrating to ArcGIS for Windows Mobile 3.0

This post has some tips for people wanting to migrate custom tasks and extensions written for the ArcGIS Runtime SDK for Windows Mobile. Before reading you should also check the article on the Esri resource center for migrating your applications to ArcGIS Mobile 3.0 as it contains some additional information that may be of help.

With the release of the 3.0 SDK there have been a number of changes to the way that custom extensions work and are deployed. Lets start with changes to the code for creating a custom task and project extension.

For the project center definition, previously you would have declared your task as

CustomTask : UserControl, IProjectTask

in 3.0 this becomes

CustomTask : ProjectTaskControl

The ProjectTaskControl type is simpler to work with. You can still set properties on it and an Icon but instead of using XML serialisation it now uses JSON. A sample base class for a task may be

public abstract class TaskBase : ProjectTaskControl
{
    readonly string _taskName;

    protected TaskBase(string taskName)
    {
        _taskName = taskName;
    }

    /// <summary>
    /// Gets/sets Description for your capability
    /// </summary>
    public override string Description
    {
        get { return base.Description; }
        set
        {
            base.Description = value;
            RaisepropertyChangedEvent("Description");
            IsDirty = true;
        }
    }

    /// <summary>
    /// Gets/sets DisplayName for your capability
    /// </summary>
    public override string DisplayName
    {
        get { return base.DisplayName; }
        set
        {
            base.DisplayName = value;
            RaisepropertyChangedEvent("DisplayName");
            IsDirty = true;
        }
    }

    public override ImageSource Icon
    {
        get
        {
            var uri = new Uri(string.Format("pack://application:,,,/SampleTasks;Component/Assets/{0}.png", _taskName));
            return new BitmapImage(uri);
        }
    }

    /// <summary>
    /// Generates an object from its JSON representation.
    /// </summary>
    public override void ReadJson(IDictionary<string, object> readData)
    {
        if (!readData.ContainsKey("Attributes"))
            return;
        IDictionary<string, object> values = readData["Attributes"] as Dictionary<string, object>;
        DisplayName = values["name"] as string;
        Description = values["description"] as string;
    }

    /// <summary>
    /// Converts an object into its JSON representation.
    /// </summary>
    public override void WriteJson(IDictionary<string, object> writeData)
    {
        IDictionary<string, object> values = new Dictionary<string, object>();
        writeData.Add("Attributes", values);
        values.Add("name", DisplayName);
        values.Add("description", Description);
    }
}

and used as

public sealed partial class CustomTask : TaskBase
{
    public SearchTask() : base("CustomTask")
    {
        InitializeComponent();

        DisplayName = "My custom task";
        Description = "blah blah blah.";
    }
}

For the implementation of the Task in the windows or windows mobile dll respectively the type is not changed though you must include the JSON serialisation. Without this you won’t see your task in the deployed application.

/// <summary>
/// Generates an object from its JSON representation
/// </summary>
public override void ReadJson(IDictionary<string, object> readData)
{
    if (!readData.ContainsKey("Attributes"))
        return;
    IDictionary<string, object> values = readData["Attributes"] as Dictionary<string, object>;
    Name = values["name"] as string;
    Description = values["description"] as string;
}

/// <summary>
/// Converts an object into its JSON representation.
/// </summary>
public override void WriteJson(IDictionary<string, object> writeData)
{
    IDictionary<string, object> values = new Dictionary<string, object>();
    writeData.Add("Attributes", values);
    values.Add("name", Name);
    values.Add("description", Description);
}

Writing a project extension has seen a similar change required so now instead of

public partial class CustomExtension : UserControl, IProjectExtension

you would use

public sealed partial class CustomExtension : ProjectExtensionControl

The other important change is to how extensions are deployed. Now when you build your projects you set the path to be

c:\ProgramData\ESRI\MobileProjectCenter\Extensions for the project center dll

c:\ProgramData\ESRI\MobileProjectCenter\Extensions\Win32 for the windows application dll and

c:\ProgramData\ESRI\MobileProjectCenter\Extensions\WinCE for the windows mobile dll

These dlls are automatically packaged with a mobile project when you reference them in the mobile project center and will be copied to a subfolder of the project. This means that if you want to debug your code you need either change the output path of your library to match your project location or manually copy the built dll each time you make code changes so that you are using the latest version. You could also add a build event to do this. If you forget this step then you will see a message like

debugarcgismobileerror

Now you can add and deploy your code there may be some breaking changes in the API that you need to correct, here are some that I have encountered.

When using a sketch tool you now need to call Activate on it

var sketchTool = new EnvelopeSketchTool();
sketchTool.SketchCompleted += SketchToolOnSketchCompleted;
sketchTool.AttachToSketchGraphicLayer(_graphicsLayer);
sketchTool.Activate();

Setting layer visibility has moved to the MobileCacheMapLayerDefinition

internal static void ToggleLayerVisibility(string layerName)
{
    if (string.IsNullOrEmpty(layerName)) return;

    var layer = MobileApplication.Current.Project.EnumerateFeatureSourceInfos().FirstOrDefault(lyr => string.Equals(lyr.Name, layerName, StringComparison.InvariantCultureIgnoreCase));

    if (layer == null) return;

    layer.MobileCacheMapLayerDefinition.Visibility = !layer.MobileCacheMapLayerDefinition.Visibility;
}

When checking for a layer you now use FeatureSourceInfo

MobileApplication.Current.Project.EnumerateFeatureSourceInfos()

Hopefully you find these tips useful for helping update your own code. Let me know of any other useful information that I’ve missed.

Geolocation with the Esri Silverlight API

Most modern browsers have support for the W3C geolocation API which allows you to access the physical location of the network connection for the user accessing your site. This is handy when you want to show or track the location and use it within your application. Access to this data is with JavaScript but as you can communicate between JavaScript and Silverlight you can also take advantage of the geolocation data in your Silverlight applications.

The first thing to do is add the JavaScript code to get the geolocation data (note that the browser will prompt the user for access). Here I’m using the watchPosition function so that each time the location changes we can update our display.

function init() {
    //Check if browser supports W3C Geolocation API
    if (navigator.geolocation) {
        navigator.geolocation.watchPosition(success);
    } 
}

function success(position) {
    var control = document.getElementById("SlControl");
    control.Content.SilverlightControl.UpdateLocation(position.coords.longitude,
                                                        position.coords.latitude, 
                                                        position.coords.accuracy);
}

“SlControl” is the id of the Silverlight plugin host object.

<object data="data:application/x-silverlight-2," id="SlControl" ...

SilverlightControl is the name assigned from the register method in our Silverlight code and UpdateLocation is the name of the method we need to invoke.

public partial class MainPage : UserControl
{
    public MainPage()
    {
        InitializeComponent();

        HtmlPage.RegisterScriptableObject("SilverlightControl", this);
    }

    [ScriptableMember]
    public void UpdateLocation(double longitude, double latitude, double accuracyInMeters)
    {
        ((LocationLayer)MyMap.Layers["Location"]).SetLocation(longitude, latitude, accuracyInMeters);
    }
}

In UpdateLocation we pass the values from JavaScript through to our layer object and use them or abuse them as we want. For this example I display the current location along with the accuracy and a history of the locations. The layer code is

/// <summary>
/// Represents a location and its accuracy if set
/// </summary>
public class LocationLayer : GraphicsLayer
{
    static SpatialReference _webMercatorSref = new SpatialReference(102100);
    static SpatialReference _wgs84Sref = new SpatialReference(4326);
    static ESRI.ArcGIS.Client.Projection.WebMercator _mercator = new ESRI.ArcGIS.Client.Projection.WebMercator();

    PointCollection _locationHistory;

    public LocationLayer()
    {
        Renderer = new SimpleSymbolRenderer();
        _locationHistory = new PointCollection();
    }
        
    /// <summary>
    /// Sets the location for the layer.
    /// </summary>
    /// <param name="longitude"></param>
    /// <param name="latitude"></param>
    /// <param name="accuracyInMeters"></param>
    public void SetLocation(double longitude, double latitude, double accuracyInMeters)
    {
        Graphics.Clear();

        // Check the SR of the Map control. This could be replaced with a generic 
        // auto project behavior or attached property
        if (SpatialReference.AreEqual(Map.SpatialReference, _wgs84Sref, true))
        {
            AddLocation(new MapPoint { X = longitude, Y = latitude, SpatialReference = _wgs84Sref }, accuracyInMeters);
        }
        else if (SpatialReference.AreEqual(Map.SpatialReference, _webMercatorSref, true))
        {
            AddLocation((MapPoint)_mercator.FromGeographic(new MapPoint { X = longitude, Y = latitude }), accuracyInMeters);
        }
        else
        {
            GeometryServer.Project(new[] { new MapPoint { X = longitude, Y = latitude, SpatialReference = _wgs84Sref } },
                                    Map.SpatialReference,
                                    accuracyInMeters,
                                    (graphics, userToken) => { AddLocation((MapPoint)graphics.First().Geometry, (double)userToken); });
        }
    }

    void AddLocation(MapPoint geometry, double accuracyInMeters)
    {
        _locationHistory.Add(geometry);

        var history = new Polyline();
        history.Paths.Add(_locationHistory);
        Graphics.Add(new Graphic { Geometry = history });

        var locationGraphic = new Graphic { Geometry = geometry };
        locationGraphic.Attributes.Add("X", geometry.X);
        locationGraphic.Attributes.Add("Y", geometry.Y);
        Graphics.Add(locationGraphic);

        if ((int)accuracyInMeters == 0)
        {
            Refresh();
            return;
        }

        GeometryServer.Buffer(new[] { geometry }, new[] { accuracyInMeters }, LinearUnit.Meter, accuracyInMeters,
                            (geo, userToken) =>
                            {
                                var accuracyGraphic = new Graphic { Geometry = geo };
                                accuracyGraphic.Attributes.Add("X", geo.Extent.GetCenter().X);
                                accuracyGraphic.Attributes.Add("Y", geo.Extent.GetCenter().Y);
                                accuracyGraphic.Attributes.Add("AccuracyInMeters", (double)userToken);
                                Graphics.Insert(0, accuracyGraphic);
                                Map.ZoomTo(geo);
                            });
    }
}

I also use some helper classes for rendering the graphics and calling the geometry service methods. These are

internal sealed class SimpleSymbolRenderer : IRenderer
{
    public SimpleSymbolRenderer(MarkerSymbol markerSymbol, LineSymbol lineSymbol, FillSymbol fillSymbol)
    {
        MarkerSymbol = markerSymbol;
        LineSymbol = lineSymbol;
        FillSymbol = fillSymbol;
    }

    public SimpleSymbolRenderer()
        : this(new SimpleMarkerSymbol(), new SimpleLineSymbol(), new SimpleFillSymbol())
    {
    }

    public MarkerSymbol MarkerSymbol { get; set; }

    public LineSymbol LineSymbol { get; set; }

    public FillSymbol FillSymbol { get; set; }

    public Symbol GetSymbol(Graphic graphic)
    {
        var geometry = graphic.Geometry;

        if (graphic.Symbol != null) return graphic.Symbol;

        if (geometry is MapPoint)
            return MarkerSymbol;
        if (geometry is Polyline)
            return LineSymbol;
        if (geometry is Polygon || geometry is Envelope)
            return FillSymbol;
        return null;
    }
}
internal static class GeometryServer
{
    public static string Url = @"http://tasks.arcgisonline.com/ArcGIS/rest/services/Geometry/GeometryServer";

    static GeometryService CreateService()
    {
        var geometryService = new GeometryService(Url);
        geometryService.Failed += (sender, e) => OnFailed(e.Error);

        return geometryService;
    }

    static void OnFailed(Exception exception)
    {
        // Do something with this
    }

    public static void Buffer(IEnumerable<Geometry> geometries, IEnumerable<Double> bufferDistances, LinearUnit unit, object userToken, Action<Geometry, object> bufferComplete)
    {
        Buffer(GeometriesToGraphics(geometries), bufferDistances, unit, userToken, bufferComplete);
    }

    public static void Buffer(IList<Graphic> graphics, IEnumerable<Double> bufferDistances, LinearUnit unit, object userToken, Action<Geometry, object> bufferComplete)
    {
        var service = CreateService();

        service.BufferCompleted += (sender, e) =>
        {
            if (e.Results.Any())
                bufferComplete(e.Results.First().Geometry, e.UserState);
        };

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

        var bufferParams = new BufferParameters
        {
            BufferSpatialReference = spatialReference,
            OutSpatialReference = spatialReference,
            UnionResults = true,
            Unit = unit
        };

        bufferParams.Features.AddRange(graphics);

        foreach (var distance in bufferDistances)
            bufferParams.Distances.Add(distance);

        service.BufferAsync(bufferParams, userToken);
    }

    public static void Project(IList<Geometry> geometries, SpatialReference spatialReference, object userToken, Action<IList<Graphic>, object> projectComplete)
    {
        Project(GeometriesToGraphics(geometries), spatialReference, userToken, projectComplete);
    }

    public static void Project(IList<Graphic> graphics, SpatialReference spatialReference, object userToken, Action<IList<Graphic>, object> projectComplete)
    {
        var service = CreateService();

        service.ProjectCompleted += (sender, e) =>
        {
            if (e.Results.Any())
                projectComplete(e.Results, e.UserState);
        };

        service.ProjectAsync(graphics, spatialReference, userToken);
    }

    static IList<Graphic> GeometriesToGraphics(IEnumerable<Geometry> geometries)
    {
        return geometries.Select(geometry => new Graphic { Geometry = geometry }).ToList();
    }
}

The xaml is very basic

<Grid x:Name="LayoutRoot" Background="White">
    <esri:Map x:Name="MyMap" WrapAround="True">
        <esri:ArcGISTiledMapServiceLayer ID="MyLayer"
            Url="http://services.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer" />
        <local:LocationLayer ID="Location">
            <esri:GraphicsLayer.MapTip>
                <Border BorderBrush="LightGray" BorderThickness="0.5" Margin="10">
                    <StackPanel Orientation="Vertical" Margin="0" Background="White">
                        <StackPanel Height="1.5" Background="#FF00A9DA"></StackPanel>
                        <StackPanel Orientation="Vertical" Margin="6">
                            <TextBlock Text="{Binding [X], StringFormat='X \{0\}'}" />
                            <TextBlock Text="{Binding [Y], StringFormat='Y \{0\}'}" />
                            <TextBlock Text="{Binding [AccuracyInMeters], StringFormat='Accurate to \{0\}m'}" />
                        </StackPanel>
                    </StackPanel>
                </Border>
            </esri:GraphicsLayer.MapTip>
        </local:LocationLayer>
    </esri:Map>
</Grid>

When all put together the output looks something like this.

Capture2

Adding a FeatureLayer by Name

Feature layers are immensely useful and powerful types that can be used within your Esri web applications. At a basic level they can be thought of as a GraphicsLayer with an associated QueryTask. The usual way of adding a FeatureLayer would be to reference the url of the layer you want to use. This url is composed of the map service endpoint and the layer id e.g.http://server/ArcGIS/rest/services/name/MapServer/0 where 0 is the layer id.

What we will look at now is how we can add a FeatureLayer by referencing the layer name instead of the id. The reason that this would be useful is that this would protect your layer from failing to load if the underlying map document that has been published as a service has had its layer order changed as this would mean that the layer id would change too.

To achieve this functionality we can query the REST endpoint of the service, lookup the service information and then use this to determine the layer id that corresponds to the supplied layer name. This technique can be used to retrieve all of the map service information but here I will just be returning the layer id and name.

Here’s the code to create the FeatureLayer

public static class FeatureLayerExtensions
{
    public static void FromLayerNameEndpoint(string mapServiceLayerNameEndpoint,
                                                Action<ESRI.ArcGIS.Client.FeatureLayer> onLayerCreated)
    {
        var featureLayer = new ESRI.ArcGIS.Client.FeatureLayer();

        featureLayer.GetLayerId(mapServiceLayerNameEndpoint,
                                (fLayer, url) =>
                                {
                                    fLayer.Url = url;
                                    if (onLayerCreated != null) onLayerCreated(fLayer);
                                });
    }

    public static void GetLayerId(this ESRI.ArcGIS.Client.FeatureLayer featureLayer,
                                    string endpoint,
                                    Action<ESRI.ArcGIS.Client.FeatureLayer, string> onLayerIdFound)
    {
        featureLayer.GetLayerId(endpoint, string.Empty, onLayerIdFound);
    }

    public static void GetLayerId(this ESRI.ArcGIS.Client.FeatureLayer featureLayer,
                                    string endpoint,
                                    string proxyUrl,
                                    Action<ESRI.ArcGIS.Client.FeatureLayer, string> onLayerIdFound)
    {
        var layerName = endpoint.TrimEnd('/').Split('/').Last();
        var uri = endpoint.TrimEnd('/').Substring(0, endpoint.TrimEnd('/').LastIndexOf('/'));

        var request = new WebClient();
        request.OpenReadCompleted += (sender, e) =>
        {
            // Using the built in Json serializer but you could use something else e.g. Json.NET
            var s = new System.Runtime.Serialization.Json.DataContractJsonSerializer(typeof(LayerInfo));
            var layers = ((LayerInfo)s.ReadObject(e.Result)).Layers;

            foreach (var layer in layers)
            {
                if (string.Equals((string)layer.Name, layerName, StringComparison.OrdinalIgnoreCase))
                {
                    if (onLayerIdFound != null) onLayerIdFound(featureLayer, string.Format("{0}/{1}", uri, layer.Id));
                    return;
                }
            }

            throw new InvalidOperationException(string.Format("A layer with the name '{0}' was not found in the map service.", layerName));
        };

        if (string.IsNullOrEmpty(proxyUrl))
            request.OpenReadAsync(new Uri(EnsureJsonResponseFormatUrl(uri)));
        else
            request.OpenReadAsync(new Uri(string.Format("{0}?{1}", proxyUrl, EnsureJsonResponseFormatUrl(uri))));
    }
        
    static string EnsureJsonResponseFormatUrl(string url)
    {
        if (url == null) throw new ArgumentNullException("url");

        if (url.IndexOf("f=json") == -1)
        {
            if (url.IndexOf('?') == -1)
                url += "?";
            else
                url += "&";
            return url + "f=json";
        }
        else
            return url;
    }
}

and the data contract that is returned from the endpoint

[DataContract]
public class LayerInfo
{
    [DataMember(Name = "layers")]
    public LayerInfoItem[] Layers { get; set; }

}

/// <summary>
/// Contains layer properties from the MapServer REST endpoint.
/// Could be extended to return all values available e.g. type, minScale etc. 
/// </summary>
[DataContract]
public class LayerInfoItem
{
    [DataMember(Name = "id")]
    public int Id { get; set; }

    [DataMember(Name = "name")]
    public string Name { get; set; }
}

and how to call it

var url = "http://sampleserver3.arcgisonline.com/ArcGIS/rest/services/Earthquakes/EarthquakesFromLastSevenDays/MapServer/Earthquakes from last 7 days";
FeatureLayerExtensions.FromLayerNameEndpoint(url,
                                                (featureLayer) =>
                                                {    
                                                    // TODO : any other initialization 
                                                    featureLayer.Opacity = 0.6;
                                                    MyMap.Layers.Add(featureLayer);
                                                });

Now when you run your application you will see the FeatureLayer added to your map. If you have fiddler running you can see the requests being made. I’d encourage you to dig into these to help understand the process involved.

A final point; A FeatureLayer cannot have its Url property set after the layer has been initialized. This is important here as it means that we need to create and add the layer in code rather than with markup.