Month: July 2011

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.

ArcGIS Server REST SOEs with Complex Types

A week or so ago I had to write my first ArcGIS Server SOE in order to add some custom functionality into a legacy .NET web forms application, if you are not familiar with the concept of SOEs (Server Object Extensions) then I suggest you start by reading the Esri help as there is a fair amount of good documentation on the ArcGIS resource pages

All the examples were geared around using simple types as operation parameters which in some cases is fine but I decided to build mine using complex types. There are a few reasons that I prefer this approach.

  • If I need to add a parameter that is related to the type I can just add it as a property rather than having to edit the operation definition.
  • If I want to work with large amounts of data or collections of data then having to manually add these can be very laborious.
  • Having to manually parse the operation input to extract parameters values is also tedious.
  • I wanted to simplify the calling of the SOE from managed code and make the underlying mechanism as transparent to the user as possible.

Sample Implementation

For the purposes of this post I’ve created a sample solution that contains the basic structure for how I implemented this. The solution consists of

  • The SOE (which is a self registering console application – thanks to Kirk Kuykendall for the idea on this)
  • A common dll used to define the SOE model and names
  • A client dll used as an API to invoke the SOE operations
  • A client test project, in this case just a web page that calls the extension

Click here to download the source

Note that you will need the ArcGIS .NET SDK installed to build the solution.

Working with Complex Types as input parameters

There are standard conventions that must be followed to build a SOE so that it can be used with ArcGIS Server. The method definition for a SOE operation is

private byte[] SampleOperationHandler(NameValueCollection boundVariables,  
                                      JsonObject operationInput,  
                                      string outputFormat,  
                                      string requestProperties,  
                                  out string responseProperties)

the input parameter values are part of the operationInput and each one must be looked up by it’s name as defined in the operation schema. This schema is defined in the CreateRestSchema method and for the sample it looks like

private RestResource CreateRestSchema() 
{ 
    var soeResource = new RestResource(_soeName, false, RootResHandler); 

    // Add the operations 
    var operations = new List<RestOperation>(); 

    var sampleRestOperation = new RestOperation( 
                Common.Names.SampleOperationName, 
                new[] { typeof(GpsData).Name }, 
                new[] { "json" }, 
                SampleOperationHandler, 
                true); 
    operations.Add(sampleRestOperation); 

    // TODO : add more operations as needed 
    soeResource.operations = operations; 

    return soeResource; 
}

where GpsData is the name of the only parameter I’m defining. The type of this parameter isn’t defined so in theory I could pass anything but we need to know what it is when we parse the operationInput in order to extract the correct data. If we are using the Simple Type approach then it may be something like operationInput.TryGetAsBoolean… but here we are passing a Json object so we can use

JsonObject jsonObj;  
bool found = operationInput.TryGetJsonObject(typeof(GpsData).Name, out jsonObj); 
if (!found) 
    throw new ArgumentNullException(typeof(GpsData).Name); 
var gpsData = JsonableObject.FromJson<GpsData>(jsonObj.ToJson());

Now we have our deserialised object containing our data to work with in a few lines rather than the same code per property.  The conversion to and from Json is handled using the Json .NET library.

Invoking the Extension

Once the SOE is built and registered we need a way of calling it. The ClientTest project is a standard ASP.NET web site with one page that calls the sample operation on a button click.

protected void btnCallSampleOperation_Click(object sender, EventArgs e) 
{ 
    var gpsData = new GpsData 
    { 
        LayerName = "Land area", 
        NumberOfSatellites = 4, 
        FixAcquired = true, 
        Location = new Location 
        { 
            Longitude = 175.495605, 
            Latitude = -39.546977 
        } 
    }; 

   var result = OperationHelper.CallSampleOperation( 
     @"http://dave-pc/ArcGIS/rest/services/TestSoe/MapServer", 
     gpsData); 
}

Note that we only have to specify the url of the MapServer endpoint as the helper method constructs the full url for us. This provides a very clean and easy way to not only invoke the extension but also to ensure that we aren’t going to get runtime errors due to using magic strings.

public static OperationResult CallSampleOperation(  
                   string restEndpoint,  
                   GpsData gpsData,  
                   string proxyUrl) 
{ 
   var fullUrl = string.Format("{0}/exts/{1}/{2}", 
         restEndpoint.TrimEnd('/'), 
         Names.SoeName, 
         Names.SampleOperationName); 
 
   var parameters = new Dictionary<string, string> 
         { { typeof(GpsData).Name, gpsData.ToJson() } }; 
 
   var request = new Request(fullUrl, proxyUrl); 
 
   return request.SubmitRequest<OperationResult>(parameters); 
}

Checking the Data

The best way to find out how the SOE works is to run it for yourself. First you need to publish a map service with at least one layer and enable the extension.

soe in arccatalog

Once you have it setup you can browse the REST endpoint for your service and there will be an extra option showing the Extension

rest metadata 1

Click on that to view the supported operations. This page can be customised, here I have included the version number for the SOE.

rest

If you click on the SampleOperation you will see the following screen which can also be used to test the extension so long as you input the data correctly.

sampple operation

An easier way is to run the test page though and view the request using Fiddler. If you start the TestClient Default.aspx page and click on the button you should see the following

fiddler

This shows the Json as part of the message body with the result below. Note that you will have to change the values for the layer name and REST service url to match your environment.


Hopefully this gives you a starting point for working with REST SOEs and complex types in your applications. Any feedback is appreciated and thanks for reading.