Windows Mobile

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.

Advertisements

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.