Identify Task for WMS layers in ArcGIS Silverlight

Apparently it doesn’t seem to be working as expected- even if the WMS layer is stored in an MXD document.  Instead of returning the layer attributes, it returns the WMS GetFeatureInfo request i.e. something similar to:

http://www2.demis.nl/WMS/wms.ashx?WMS=SAMPLE&VERSION=1.3.0&REQUEST=GetFeatureInfo&<
>CRS=CRS:84&BBOX=-180,-90,180,90&WIDTH=1304&HEIGHT=652&LAYERS=Countries&
QUERY_LAYERS=Countries&
STYLES=&EXCEPTIONS=XML&FORMAT=image/png&INFO_FORMAT=text/html&FEATURE_COUNT=50&I=304&J=192

The workaround is to use this URL to make a direct call to the WMS service and then return the results with an asynchronous request. And since the Identify task is an asynchronous request itself, you have to get one asynchronous request to call another asynchronous request. Clear as mud – right?

This example is based on the Identify Task Silverlight sample so I will assume you have the ‘standard’ identify functionality working.

The ‘first thing you need to do is create a new WCF Service which would be the one that will make the WMS GetFeatureInfo request. Lets imaginatively call it ‘WMSService’. Its Service Contract would be:

  1. [ServiceContract]
  2.     public interface IWMSService
  3.     {
  4.         [OperationContract]
  5.         string GetFeatureInfo(string url);
  6.     }

And the WMSService.svc.cs file would look like:

  1. [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
  2.     public class WMSService : IWMSService
  3.     {
  4.         public string GetFeatureInfo(string url)
  5.         {
  6.             HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;
  7.             using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
  8.             {
  9.                 StreamReader reader = new StreamReader(response.GetResponseStream());
  10.                 return (reader.ReadToEnd());
  11.             }
  12.         }
  13.     }

Next we need to get back to our Silverlight project and add a reference to the new WCF Service:

  1. using WMSServiceReference;

Then, replace the cb_SelectionChanged event – remember I am assuming you are using the original Identify sample- with the following:

  1. private void cb_SelectionChanged(object sender, SelectionChangedEventArgs e)
  2.         {
  3.             int index = IdentifyComboBox.SelectedIndex;
  4.             if (index > -1 && index < identifyItems.Count)
  5.             {
  6.                 IDictionary<string, object> di = _dataItems[index].Data;
  7.                 object url = string.Empty;
  8.                 if (di.TryGetValue(“Url”, out url) || di.TryGetValue(“URL”, out url) || di.TryGetValue(“url”, out url))
  9.                 {
  10.                     string url1 = ((string)url).Replace(“text/html”, “text/xml”);
  11.                     WMSServiceClient wmsClient = new WMSServiceClient();
  12.                     wmsClient.GetFeatureInfoCompleted += new EventHandler<GetFeatureInfoCompletedEventArgs>(wmsClient_GetFeatureInfoCompleted);
  13.                     wmsClient.GetFeatureInfoAsync(url1);
  14.                 }
  15.                 else
  16.                 {
  17.                     IdentifyDetailsDataGrid.ItemsSource = _dataItems[index].Data;
  18.                 }
  19.             }
  20.         }

Notice that before I send the FeatureInfo request on its merry way, I am changing the requested format from html to xml which would be easier handled in Silverlight, rather than an HTML snippet.

So when the request completes, it will hit the GetFeatureInfoCompleted event. The response will get returned as an XML string, and the problem now is how to display this in the Identify grid since we don’t know anything about the number or type of attributes that will get returned.

  1. void wmsClient_GetFeatureInfoCompleted(object sender, GetFeatureInfoCompletedEventArgs e)
  2.         {
  3.             string res = e.Result;
  4.             IdentifyDetailsDataGrid.ItemsSource = GenerateData(res).ToDataSource();
  5.         }

The magic here occurs via the GenerateData and ToDataSource functions. Both of them have been taken from this excellent blog post by Vladimir Bodurov:  How to Bind Silverlight DataGrid From IEnumerable of IDictionary by Transforming Each Dictionary Key Into a Property of Anonymous Typed Object. I changed the GenerateData function to recursively loop through the returned XML as follows:

  1. public IEnumerable<IDictionary> GenerateData(string xmlResponse)
  2. {
  3.     var dict = new Dictionary<string, object>();
  4.     XDocument xmldoc = XDocument.Parse(xmlResponse);
  5.     XElement root = xmldoc.Root;
  6.     foreach (XNode node in root.Nodes())
  7.     {
  8.         dict = RecurseXmlDocument((XNode)node, dict);
  9.         yield return dict;
  10.     }
  11. }
  12. private static Dictionary<string, object> RecurseXmlDocument(XNode root, Dictionary<string, object> dict)
  13. {
  14.     if (root is XElement)
  15.     {
  16.         XElement el = (XElement)root;
  17.         if (el.HasElements)
  18.             RecurseXmlDocument(el.FirstNode, dict);
  19.         else
  20.         {
  21.             string strFieldName = el.Name.ToString();
  22.             string strFieldValue = el.Value;
  23.             dict[strFieldName] = strFieldValue;
  24.             if (el.NextNode != null)
  25.             {
  26.                 RecurseXmlDocument(el.NextNode, dict);
  27.             }
  28.             return dict;
  29.         }
  30.     }
  31.     return dict;
  32. }

You can download the DataSourceCreator.cs here.

Once finished you should be able to use the Identify tool on any WMS layer and get back the results. Well, ‘any’ may be an overstatement as I only tried it with a couple!

Enjoy!