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.
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); }
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.
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.