Fluent Where Clause

I like working with fluent APIs and using the fluent syntax with LINQ, it makes more sense to me and is easier to follow. With this in mind I thought it would be fun to create a simple fluent style where clause for ArcGIS queries or DefinitionExpressions. The main benefits of this approach are to enhance readability and to reduce runtime errors from using magic strings as usually you would construct these queries using string.Format(…). This is by no means a fully featured implementation but it serves as a starting point for anyone that is interested and I may extend it in the future.

At the center of this I created a new class called WhereClause. Within this class are a number of standard operations that you would use when constructing a query such as comparing values and joining statements. From each function I return the WhereClause object so that subsequent functions can be chained. The code looks like.

public class WhereClause
{
    StringBuilder _whereClause;
    int _nestLevel;

    const string OpenBracket = "(";
    const string CloseBracket = ")";

    const string EqualsOperator = "=";
    const string GreaterThanOperator = ">";
    const string LessThanOperator = "<";
    const string InOperator = "in ({0})";
    const string InForStringOperator = "in ('{0}')";

    const string AndOperator = "AND";
    const string OrOperator = "OR";
    const string LikeOperator = "LIKE '{0}%'";

    const string Separator = ",";
    const string SeparatorForString = "','";

    public static WhereClause Create()
    {
        return new WhereClause();
    }

    WhereClause()
    {
        _whereClause = new StringBuilder();
    }

    public WhereClause For<T>(T forObject, Expression<Func<string>> property) where T : class
    {
        AddCloseBracket();
        PropertyInfo propertyInfo;
        if (property.Body is MemberExpression)
        {
            propertyInfo = ((MemberExpression) property.Body).Member as PropertyInfo;
        }
        else
        {
            propertyInfo = ((MemberExpression) ((UnaryExpression)property.Body).Operand).Member as PropertyInfo;
        }
        if (propertyInfo != null) propertyInfo.SetValue(forObject, _whereClause.ToString().Trim(), null);

        return this;
    }

    public WhereClause Field<T>(Expression<Func<T>> expression)
    {
        return Field(GetPropertyName(expression));
    }

    public WhereClause Field(string name)
    {
        AddOpenBracket();
        AppendPart(name);
        return this;
    }        

    public WhereClause And()
    {
        AddCloseBracket();
        AppendPart(AndOperator);
        return this;
    }

    public WhereClause AndField<T>(Expression<Func<T>> expression)
    {
        return AndField(GetPropertyName(expression));
    }

    public WhereClause AndField(string name)
    {            
        AppendPart(AndOperator);
        AppendPart(name);
        return this;
    }

    public WhereClause Or()
    {
        AddCloseBracket();
        AppendPart(OrOperator);
        return this;
    }

    public WhereClause OrField<T>(Expression<Func<T>> expression)
    {
        return OrField(GetPropertyName(expression));
    }

    public WhereClause OrField(string name)
    {
        AppendPart(OrOperator);
        AppendPart(name);
        return this;
    }

    public WhereClause Equals<T>(T value)
    {
        if (typeof(T) == typeof(string) || typeof(T) == typeof(DateTime))
            return Equals(value.ToString());

        if (IsIEnumerableOfT(typeof(T)))
            throw new NotSupportedException("Collection should be of type list");

        AppendPart(EqualsOperator);
        AppendPart(value.ToString());
        return this;
    }

    public WhereClause Equals<T>(List<T> values)
    {
        if (typeof(T) == typeof(string) || typeof(T) == typeof(DateTime))
            return Equals(values.Cast<string>());

        AppendPart(string.Format(InOperator, string.Join(Separator, values)));
        return this;
    }

    WhereClause Equals(string value)
    {
        AppendPart(EqualsOperator);
        AppendPart("'" + value + "'");
        return this;
    }

    WhereClause Equals(IEnumerable<string> values)
    {
        AppendPart(string.Format(InForStringOperator, string.Join(SeparatorForString, values)));
        return this;
    }

    public WhereClause GreaterThan<T>(T value)
    {
        if (typeof(T) == typeof(string) || typeof(T) == typeof(DateTime))
            return GreaterThan(value.ToString());

        if (IsIEnumerableOfT(typeof(T)))
            throw new NotSupportedException();

        AppendPart(GreaterThanOperator);
        AppendPart(value.ToString());
        return this;
    }

    WhereClause GreaterThan(string value)
    {
        AppendPart(GreaterThanOperator);
        AppendPart("'" + value + "'");
        return this;
    }

    public WhereClause GreaterThanOrEquals<T>(T value)
    {
        if (typeof(T) == typeof(string) || typeof(T) == typeof(DateTime))
            return GreaterThanOrEquals(value.ToString());

        if (IsIEnumerableOfT(typeof(T)))
            throw new NotSupportedException();

        AppendPart(GreaterThanOperator + EqualsOperator);
        AppendPart(value.ToString());
        return this;
    }

    WhereClause GreaterThanOrEquals(string value)
    {
        AppendPart(GreaterThanOperator + EqualsOperator);
        AppendPart("'" + value + "'");
        return this;
    }

    public WhereClause LessThan<T>(T value)
    {
        if (typeof(T) == typeof(string) || typeof(T) == typeof(DateTime))
            return LessThan(value.ToString());

        if (IsIEnumerableOfT(typeof(T)))
            throw new NotSupportedException();

        AppendPart(LessThanOperator);
        AppendPart(value.ToString());
        return this;
    }

    WhereClause LessThan(string value)
    {
        AppendPart(LessThanOperator);
        AppendPart("'" + value + "'");
        return this;
    }

    public WhereClause LessThanOrEquals<T>(T value)
    {
        if (typeof(T) == typeof(string) || typeof(T) == typeof(DateTime))
            return LessThanOrEquals(value.ToString());

        if (IsIEnumerableOfT(typeof(T)))
            throw new NotSupportedException();

        AppendPart(LessThanOperator + EqualsOperator);
        AppendPart(value.ToString());
        return this;
    }

    WhereClause LessThanOrEquals(string value)
    {
        AppendPart(LessThanOperator + EqualsOperator);
        AppendPart("'" + value + "'");
        return this;
    }

    public WhereClause StartsWith(string value)
    {
        AppendPart(string.Format(LikeOperator, value));
        return this;
    }

    void AppendPart(string part)
    {
        if (_whereClause.ToString().EndsWith(" "))
            _whereClause.Append(part);
        else
            _whereClause.Append(" " + part);
    }

    static string GetPropertyName<T>(Expression<Func<T>> expression)
    {
        var exp = (MemberExpression)expression.Body;
        return exp.Member.Name;
    }

    static bool IsIEnumerableOfT(Type type)
    {
        return type.GetInterfaces().Any(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>));
    }

    void AddOpenBracket()
    {
        AppendPart(OpenBracket);
        _nestLevel++;
    }

    void AddCloseBracket()
    {
        if (_nestLevel == 0) return;

        AppendPart(CloseBracket);
        _nestLevel--;
    }
}

In order to use this we need to do 3 things.

  1. Create a new instance of a WhereClause
  2. Add the statements we want to execute
  3. Apply the statement to a property

An example is

var queryTask = new QueryTask("http://sampleserver1.arcgisonline.com/ArcGIS/rest/services/Demographics/ESRI_Census_USA/MapServer/5");
queryTask.ExecuteCompleted += QueryTask_ExecuteCompleted;

var query = new Query { ReturnGeometry = true, OutSpatialReference = MyMap.SpatialReference };
query.OutFields.Add("*");

var states = new List<string> { "Florida", "Alaska", "Idaho", "Hawaii" };

// Example of using string values and chaining statements
WhereClause.Create()
    .Field("STATE_NAME").Equals(states)
    .Or()
    .Field("POP2007").LessThanOrEquals(1000000)
    .Or()
    .Field("STATE_NAME").StartsWith("Te")
    .For(query, () => query.Where);

queryTask.ExecuteAsync(query);

You should be able to see the 3 steps outlined above. The output from is looks like

( STATE_NAME in (‘Florida’,’Alaska’,’Idaho’,’Hawaii’) ) OR ( POP2007 <= 1000000 ) OR ( STATE_NAME LIKE ‘Te%’ )

query2

query2output

This has already eliminated some potential typo’s but you can also use your own object properties for the statement field names as long as they match the underlying data. The advantage of this is that any errors will throw compile time errors as opposed to runtime errors.

An example of this is

var featureLayer = MyMap.Layers["MyFeatureLayer"] as FeatureLayer;

if (featureLayer == null || featureLayer.InitializationFailure != null)
    return;

// The property values here could be from your view model
WhereClause.Create()
    .Field(() => Pop1990).GreaterThan(Pop1990) // Using the property name as the query column
    .AndField("CITY_NAME").StartsWith("Bi")    // Using text for the query column
    .Or()
    .Field("CITY_NAME").StartsWith("Water")
    .For(featureLayer, () => featureLayer.Where);

featureLayer.Update();

( Pop1990 > 20000 AND CITY_NAME LIKE ‘Bi%’ ) OR ( CITY_NAME LIKE ‘Water%’ )

query1

I’d be interested to hear what you think of this approach or any suggestions you may have. Thanks for reading.

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.

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.

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.