In this section, we'll go over searching content from online layers.
In Chapter 1, Introduction to ArcGIS Runtime, we were introduced to a tool that searched through several layers using a FindTask
constructor. We didn't really discuss how it worked, but we made some changes to it and just enjoyed when it searched through several layers. Let's go over that code in more detail. Here's the code again:
var findTask = new FindTask(new System.Uri(this.USAUri)); var findParameters = new FindParameters(); findParameters.LayerIDs.Add(0); // Cities findParameters.LayerIDs.Add(3); // Counties findParameters.LayerIDs.Add(2); // States findParameters.SearchFields.Add("name"); findParameters.SearchFields.Add("areaname"); findParameters.SearchFields.Add("state_name"); findParameters.ReturnGeometry = true; SpatialReference sr = new SpatialReference(wkid); findParameters.SpatialReference = sr; findParameters.SearchText = this.SearchText; findParameters.Contains = true; FindResult findResult = await findTask.ExecuteAsync(findParameters); var foundCities = 0; var foundCounties = 0; var foundStates = 0; // Loop thru results; count the matches found in each layer foreach (FindItem findItem in findResult.Results) { switch (findItem.LayerID) { case 0: // Cities foundCities++; break; case 3: // Counties foundCounties++; break; case 2: // States foundStates++; break; } } // Report the number of matches for each layer var msg = string.Format("Found {0} cities, {1} counties, and {2} states containing '" + this.SearchText + "' in a Name attribute", foundCities, foundCounties, foundStates); // Bind the results to a DataGrid control on the page IReadOnlyList<FindItem> temp = findResult.Results; ObservableCollection<FindItem> obsCollection = new ObservableCollection<FindItem>(); foreach (FindItem item in temp) { obsCollection.Add(item); } this.GridDataResults = obsCollection; // show message Messenger.Default.Send<NotificationMessage>(new NotificationMessage(msg));
The first thing that happens with this code is the FindTask
constructor is instantiated with an online service. After that, the most important part of this algorithm is FindParameters
. The FindParameters
class allows you to specify which layers to search though using LayerIDs
. As we discussed earlier, a map service can have one or more layers in it. You specify those layers using their ID, which is always an integer. If you don't know the layers, inspect the service to determine the layers that are in the service and their unique IDs. After the layers are specified, the fields to search against are added to SearchFields
. You can specify the field names as shown previously, or you can instantiate a generic List<string>
instance as shown here:
System.Collections.Generic.List<string> searchFields = new System.Collections.Generic.List<string>(); searchFields.Add("areaname"); findParameters.SearchFields = searchFields;
You can also specify the fields using a comma-separated list like so:
findParameters.SearchFields.AddRange(new string[] { "CITY_NAME", "NAME", "SYSTEM", "STATE_ABBR", "STATE_NAME" });
If you don't specify SearchFields
, all fields will be searched, and that can reduce performance, especially in large layers. After the fields are set, you really need to set the coordinate system so that it matches the coordinate system of MapView
. The next parameter is the text you want to search for. If you set Contains
to true
, an SQL-like expression is used that matches any part of the string literal the user specifies. If Contains
is set to false
, it will explicitly look for any record that exactly matches the text that the user enters.
Some other properties worth discussing are ReturnGeometry
and LayerDefinitions
. The ReturnGeometry
property is a Boolean value that indicates whether to return the geometry or not. This option is helpful if you want to do something with the geometry of the results. The LayerDefinitions
property is a get
or set
property that allows you to determine whether the layers had an attribute definition used against them when the layers were originally published. For example, if someone had all of the countries in the world, but only wanted to show European countries, they would have set an expression in ArcMap
via a definition query (CONTINENT = 'Europe'
) that only shows those countries. When the layer is published, ArcGIS Server will honor that expression as it is stored with the layer. You can get or change the expression with LayerDefinitions
, using an example such as this:
findParameters.LayerDefinitions = new LayerDefinition[] { new LayerDefinition() { LayerID = 0, Definition = "COUNTRY='Norway" }, new LayerDefinition() { LayerID = 2, Definition = "COUNTRY='France" } };
Once the parameters are specified, they are passed into the FindTask
constructor:
FindResult findResult = await findTask.ExecuteAsync(findParameters);
The FindTask
constructor will execute the task asynchronously, and then return a FindResult
class. The FindResult
class contains the results of FindTask
and must be awaited. The FindResult
class has two properties worth further discussion: ExceededTransferLimit
and Results
. The ExceededTransferLimit
property is a Boolean value that simply lets you know whether the number of records returned exceeds the maximum number of records that ArcGIS Server has been set to return when querying a layer. The default value in ArcGIS Server is 1,000 records. Generally speaking, you should check this just to make sure that you haven't exceeded this value, not only for performance reasons, but also because if you exceed this value only 1,000 records will be returned. In essence, if your query returned 1,500 records, but ArcGIS Server is set to 1,000, your query will not have the other 500 records. As a result, it's important that you communicate with your ArcGIS Server administrator to let them know that this setting needs to be higher than the default value.
The Results
property is the most important object you will be using after FindTask
finishes, because it has what you're interested in: data. As shown in the preceding code, ExecuteAsync
returns Results
, which you can use to get the total number of records, iterate over to present to the user, summarize as shown in the code, and many other useful techniques. The Results
property contains a collection of FindItem
, which, as shown previously, you can iterate over using a foreach
statement. Once the data is summarized, it is then translated into an ObservableCollection
object so that the View
can bind to the data.
The ExecuteAsync
method also has an overload that includes a System.Threading.CancellationToken
parameter, which allows you to cancel a task using code such as this:
System.Threading.CancellationTokenSource canceller = new System.Threading.CancellationTokenSource(); canceller.CancelAfter(10000); // 10 seconds var token = canceller.Token; // Execute the task with the cancellation token; await the result FindResult findResult = null; Task<FindResult> task = findTask.ExecuteAsync(findParameters, token); try { findResult = await task; } catch (System.Threading.Tasks.TaskCanceledException exp) { // ... handle cancel here ... }
This code will simply stop running if the FindTask
property takes longer than 10 seconds, and then you could let your user know that the query took too long.
As noted earlier, QueryTask
is more powerful than FindTask
because it allows you to include spatial queries. But before we delve into spatial queries, let's go over a simple example to show how similar QueryTask
is to FindTask
. Before you set up a QueryTask
property, you must first define a Query
task. This is similar in concept to FindParameters
, but with a lot more options.
A basic example of Query
looks like the following code:
Esri.ArcGISRuntime.Tasks.Query.Query query = new Esri.ArcGISRuntime.Tasks.Query.Query(sqlQuery); query.Geometry = this.mapView.Extent; query.OutSpatialReference = this.mapView.SpatialReference; query.OutFields.Add("*");
The Query
task has four overloads, which allow you to either pass an IEnumerable
method of object IDs in a layer or table, a WHERE
clause, TimeExtent
, or a Geometry
class with the spatial relationship. Let's first discuss the object IDs. An object ID is simply a unique ID for each feature in a FeatureLayer
resource. Every record in a FeatureService
or FeatureLayer
resource will automatically have a unique object ID. A WHERE
clause is the same as discussed earlier with a FindTask
property. A TimeExtent
instance is a time window, such as 5 minutes ago:
var timeWindow = new Esri.ArcGISRuntime.Data.TimeExtent (DateTime.Now.Subtract(new TimeSpan(0, 5, 0, 0)), DateTime.Now); // 5 minutes ago to present var queryParams = new Esri.ArcGISRuntime.Tasks.Query.Query(timeWindow);
The last option we will discuss here is the Geometry
and spatial relationship overload. With this overload, you can pass in geometry and query the layer using a spatial relationship. For example, you can pass in a MapPoint
class and use it to see if it intersects with a polygon:
In the preceding screenshot, the point intersects with the state of Texas, so in this case, the state would be returned in the query. The other spatial relationships available include Contains
, Crosses
, EnvelopeIntersects
, IndexIntersects
, Overlap
, Touches
, Within
, and Relation
.
Once you've set up the Query
object, it can be passed into the QueryTask
constructor, as shown here:
Query query = new Query("areaname = '" + this.SearchText + "'"); query.OutFields.Add("*"); System.Uri uri = new System.Uri(this.USAUri + "/0"); QueryTask queryTask = new QueryTask(uri); QueryResult queryResult = await queryTask.ExecuteAsync(query);
Note that this code is using the same layer we used in earlier chapters. In this case, we're querying the cities layer (layer 0
) using the areaname
field. However, with QueryTask
, you can build more complicated queries such as this:
query.Where = "(TYPE = 2 AND STATUS = 1) OR (SPEED <= 10)";
Once we execute the QueryTask
constructor, it returns a QueryResult
class. The most important thing to note about the difference between FindTask
and QueryTask
is that QueryTask
works on a single layer, not multiple layers. Also, QueryResult
has a property called FeatureSet
, which contains a set of features. The Features
class is a base class to graphics
, but you can add these features to a GraphicsLayer
class using code such as this:
// Execute the task and await the result QueryResult queryResult = await queryTask.ExecuteAsync(query); // Get the list of features (graphics) from the result var resultFeatures = queryResult.FeatureSet.Features; // Display result graphics graphicsLayer.GraphicsSource = resultFeatures;
You also have the option of whether or not to return the geometry by setting ReturnGeometry
to true
or false
for Query
. With Query
, you can also set the GeometryPrecision
property, which indicates the precision of the geometry. Also, you can set the OrderByFields
property, which works just like ORDER BY
in an SQL database. Another useful property is OutStatistics
, which returns statistics about the field values. With OutStatistics
you can compute the average, count, max, min, standard deviation, sum, and variance. You can use OutStatistics
on a layer using an example such as this:
SpatialReference sr = new SpatialReference(wkid); Query queryCityStats = new Query("pop2000 > 1000"); queryCityStats.OutSpatialReference = sr; queryCityStats.OutFields.Add("*"); queryCityStats.OutStatistics = new List<OutStatistic> { new OutStatistic(){ OnStatisticField = "pop2000", OutStatisticFieldName = "citysum", StatisticType = StatisticType.Sum }, new OutStatistic(){ OnStatisticField = "pop2000", OutStatisticFieldName = "cityavg", StatisticType = StatisticType.Average } }; QueryTask queryTaskCityStats = new QueryTask(new System.Uri(this.USAUri + "/0")); QueryResult queryResultCityStatus = await queryTaskCityStats.ExecuteAsync(queryCityStats); IReadOnlyList<Feature> featuresCityStats = queryResultCityStatus.FeatureSet.Features; Feature feature = featuresCityStats[0]; double sum = (double)feature.Attributes["citysum"]; double avg = (double)feature.Attributes["cityavg"];
In this example code, an OutStatistics
property is created using a generic List
instance of the OutStatistic
type on the field called pop2000
. The average and sum of the population of all cities over 1,000 are calculated. The OutStatisticFieldName
instance is set to citysum
and cityavg
. Using the results of the QueryTask
constructor, the attribute values are cast to a couple of variables of the type double
.
Let's create an app as we did earlier, where the user was able to search for a city, state, or county using a string literal. However, instead of walking you through all of the steps, you can create a project from scratch or just check out the provided sample project called Chapter7
. In those early chapters, we used a FindTask
constructor, so let's refactor and use a QueryTask
constructor instead. You won't need a DataGrid
control. Also, instead of reminding you of everything you need to do to set up this project, we will skip that step and let you build the project based on what you've learned in earlier chapters:
public async void Search(int wkid) { // Create the symbol SimpleMarkerSymbol markerSymbol = new SimpleMarkerSymbol(); markerSymbol.Size = 25; markerSymbol.Color = Colors.Red; markerSymbol.Style = SimpleMarkerStyle.Diamond; SimpleFillSymbol sfsState = new SimpleFillSymbol() { Color = Colors.Red, Style = SimpleFillStyle.Solid }; SimpleFillSymbol sfsCounty = new SimpleFillSymbol() { Color = Colors.Red, Style = SimpleFillStyle.Solid }; SpatialReference sr = new SpatialReference(wkid); Query queryCity = new Query("areaname = '" + this.SearchText + "'"); queryCity.OutSpatialReference = sr; queryCity.OutFields.Add("*"); QueryTask queryTaskCity = new QueryTask(new System.Uri(this.USAUri + "/0")); QueryResult queryResultCity = await queryTaskCity.ExecuteAsync(queryCity); Query queryStates = new Query("state_name = '" + this.SearchText + "'"); queryStates.OutSpatialReference = sr; queryStates.OutFields.Add("*"); QueryTask queryTaskStates = new QueryTask(new System.Uri(this.USAUri + "/2")); QueryResult queryResultStates = await queryTaskStates.ExecuteAsync(queryStates); Query queryCounties = new Query("name = '" + this.SearchText + "'"); queryCounties.OutSpatialReference = sr; queryCounties.OutFields.Add("*"); QueryTask queryTaskCounties = new QueryTask(new System.Uri(this.USAUri + "/3")); QueryResult queryResultCounties= await queryTaskCounties.ExecuteAsync(queryCounties); // Get the list of features (graphics) from the result IReadOnlyList<Feature> featuresCity = queryResultCity.FeatureSet.Features; foreach (Feature featureCity in featuresCity) { Graphic graphicCity = (Graphic)featureCity; graphicCity.Symbol = markerSymbol; this.graphicsLayerCity.Graphics.Add(graphicCity); } // Get the list of features (graphics) from the result IReadOnlyList<Feature> featuresStates = queryResultStates.FeatureSet.Features; foreach (Feature featureState in featuresStates) { Graphic graphicState = (Graphic)featureState; graphicState.Symbol = sfsState; this.graphicsLayerState.Graphics.Add(graphicState); } // Get the list of features (graphics) from the result IReadOnlyList<Feature> featuresCounties = queryResultCounties.FeatureSet.Features; foreach (Feature featureCounty in featuresCounties) { Graphic graphicCounty = (Graphic)featureCounty; graphicCounty.Symbol = sfsCounty; this.graphicsLayerCounty.Graphics.Add(graphicCounty); } }
You will see something like the following screenshot if you enter Lancaster as the search string:
Note that any city, state, or county with the name Lancaster
was found and shown on the map. There were no states with the name Lancaster
in them, but there were several cities (green triangles) and counties (filled in red) with that name.
The first thing you'll note is that three separate QueryTask
objects were created. Then, we retrieved the features, converted them to graphics, and then added them to the GraphicsLayer class
. You now know how to create the GraphicsLayer
class.
The QueryTask
option is a very powerful search object because it simply provides a lot of options. The ExecuteAsync
method has several variations (ExecuteCountAync
, ExecuteObjectIDsQueryAsync
, and ExecuteRelationshipQueryAsync
), which mainly allow you to search for the number of features in a layer, search and return just object IDs, and search for data in a related table. For best performance, search using QueryTask
with ExecuteObjectIDsQueryAsync
. This method will only return Object IDs instead of the attributes and geometry, so it is inherently faster. With these Object IDs, you can perform additional operations, such as placing the features into a selected state.
It's also possible to search for tables or layers that are related to the layer you're searching on, using ExecuteRelationshipQueryAsync
. This method expects a RelationshipParameter
class, which is another class for defining the relationships. Take a look at map service by navigating to http://sampleserver3.arcgisonline.com/ArcGIS/rest/services/Petroleum/KSPetro/MapServer. Click on the Wells layer. Near the bottom of the page, you'll see a section called Relationships. This shows that this layer has a relationship to a table called Tops
and another layer called Fields
. To set up the relationship so that you can return records, you would specify the RelationshipParameter
class similar to this:
RelationshipParameter relationshipParameters = new RelationshipParameter(){ ObjectIds = (int[])ObjIds, DefinitionExpression = exp, OutFields = new string[] { "OBJECTID, KID, FORMATION" }, RelationshipId = 1, OutSpatialReference = MyMap.SpatialReference };
In this example, the Object IDs can come from a query or by clicking on a single feature using an Identify
operation (discussed later in this chapter); then you must specify the DefinitionExpression
property (the WHERE
clause), the output fields, relationship ID, and spatial reference. The relationship ID is in parentheses, as shown here:
In this example, the relationship ID is 3
. The RelationshipParameter
class returns a RelationshipResult
object, which you can use to iterate over.
52.15.245.1