Web API

Enhancing GIS Web Apps with SignalR

This post has been moved to http://davetimmins.com/post/2011/November/Enhancing-GIS-WebApps-with-SignalR/

Advertisements

Geolocation with the Esri Silverlight API

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

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

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

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

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

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

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

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

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

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

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

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

    PointCollection _locationHistory;

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

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

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

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

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

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

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

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

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

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

    public MarkerSymbol MarkerSymbol { get; set; }

    public LineSymbol LineSymbol { get; set; }

    public FillSymbol FillSymbol { get; set; }

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

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

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

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

        return geometryService;
    }

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

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

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

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

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

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

        bufferParams.Features.AddRange(graphics);

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

        service.BufferAsync(bufferParams, userToken);
    }

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

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

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

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

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

The xaml is very basic

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

When all put together the output looks something like this.

Capture2

Adding a FeatureLayer by Name

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

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

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

Here’s the code to create the FeatureLayer

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

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

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

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

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

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

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

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

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

and the data contract that is returned from the endpoint

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

}

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

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

and how to call it

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

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

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

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.