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.

Advertisements

6 comments

  1. Dave,

    Would this solution of using a userToken solve the issue presented on this ESRI Silverlight forum post?:

    http://forums.arcgis.com/threads/21704-Decoupling-asynchronous-querytask-away-from-the-map.-Wait-for-the-async-to-finish?p=134508#post134508

    Basically the programmer wants to create a single method that calls out to an asyncronous task, and then passes back a value that comes out of that task. I’m not sure if your solution applies as I can’t seem to wrap my head around these asyncronous calls yet.

    Thanks.

    1. Yes it would. This is exactly what you need to use for that scenario. The userToken can is an optional parameter that is passed in the result so that you can link the input with the output. I’d recommend playing with the sample code in my post to see the difference between the incorrect and correct way as that should help you to identify the execution timeline.

      1. Dave,

        I looked more closely at your code examples and it doesn’t look like it can solve the particular question asked by Michael Blom in that forum post. He is looking to create a method like the following:

        public bool GetABoolean(int id)
        {

        bool aBool;

        client.CallAnAsyncMethod(id); // value is returned in a completed event handler. Need to somehow get that value into aBool.

        return aBool; // this needs to NOT execute until aBool has a value

        }

        So it would be a single method that can use a call to an async method, somehow get a result from that async method, and then return that result to the calling code. So the user of the GetABoolean method would not even know that the method uses the async method to calculate some value. The method would just allow the user to pass in a value, and get back another value.

        In looking at your example I could not think of a way to accomplish something similar. Does that sound like the case?

        Thanks.

  2. Hi Tom,

    the type of syntax you want is not currently available, you’d need something like the await keyword that is part of the new async update coming with the next .NET update. Instead you can use the approach I use in the code where you supply an action that executes once the asynchronous operation completes and then calls your next block of code.

    public void GetABoolean(int id, Action onComplete)
    {
    client.CallAnAsyncMethodComplete += (sender, e) =>
    {
    onComplete(e.Value); // assuming e.Value is the value for aBool
    };

    client.CallAnAsyncMethod(id);
    }

    and to call it becomes

    GetABoolean(2, (boolValue) => { // do something with the value now });

    There are more examples of using this in my latest post about geolocation if you want some more code to look at.

    Cheers, Dave

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s