I need to allow an advanced user to enter an XPath expression and show them the value(s) or nodes or attributes found. In the .Net framework, the System.Xml.XPath.Extensions can be used to call XPathEvaluate, but Silverlight doesn't have this MSDN reference. Has anyone rewritten the extension methods for use in Silverlight? What is the best approach to take? Why aren't they available in Silverlight or in the toolkit (vote on the issue here)?
I think the reason XPath is not available in Silverlight is that MS wants you to use Linq to XML instead. But that doesn't exactly help you. Unfortunately I think it will be difficult to achieve what you want. If you must have this functionality I think you will have to resort to sending your query to the server, evaluating it there, and returning the result. It is ugly, but I think it is the only way.
One solution is to use a generic handler and outsource the processing to the server on an asynchronous request. Here is a step by step:
First:
Create a generic handler in your web project. Add an ASHX file with the following code for your ProcessRequest (simplified for brevity):
public void ProcessRequest(HttpContext context)
{
context.Response.ContentType = "text/plain";
string xml = context.Request.Form["xml"].ToString();
string xpath = context.Request.Form["xpath"].ToString();
XmlReader reader = new XmlTextReader(new StringReader(xml));
XDocument doc = XDocument.Load(reader);
var rawResult = doc.XPathEvaluate(xpath);
string result = String.Empty;
foreach (var r in ((IEnumerable<object>)rawResult))
{
result += r.ToString();
}
context.Response.Write(result);
}
It should be noted that there are some namespaces you will need references to for xml processing:
System.IO
System.Xml
System.Xml.XPath
System.Xml.Linq
Second:
You need some code that would allow you to make an asynchronous post to the generic handler. The code below is lengthy, but essentially you pass the following:
The Uri of your generic handler
A dictionary of key value pairs (assuming the xml document and the xpath)
Callbacks for success and failure
A reference to the dispatch timer of the UserControl so that you can access your UI (if necessary) in the callbacks.
Here is some code I've placed in a utility class:
public class WebPostHelper
{
public static void GetPostData(Uri targetURI, Dictionary<string, string> dataDictionary, Action<string> onSuccess, Action<string> onError, Dispatcher threadDispatcher)
{
var postData = String.Join("&",
dataDictionary.Keys.Select(k => k + "=" + dataDictionary[k]).ToArray());
WebRequest requ = HttpWebRequest.Create(targetURI);
requ.Method = "POST";
requ.ContentType = "application/x-www-form-urlencoded";
var res = requ.BeginGetRequestStream(new AsyncCallback(
(ar) =>
{
Stream stream = requ.EndGetRequestStream(ar);
StreamWriter writer = new StreamWriter(stream);
writer.Write(postData);
writer.Close();
requ.BeginGetResponse(new AsyncCallback(
(ar2) =>
{
try
{
WebResponse respStream = requ.EndGetResponse(ar2);
Stream stream2 = respStream.GetResponseStream();
StreamReader reader2 = new StreamReader(stream2);
string responseString = reader2.ReadToEnd();
int spacerIndex = responseString.IndexOf('|') - 1;
string status = responseString.Substring(0, spacerIndex);
string result = responseString.Substring(spacerIndex + 3);
if (status == "success")
{
if (onSuccess != null)
{
threadDispatcher.BeginInvoke(() =>
{
onSuccess(result);
});
}
}
else
{
if (onError != null)
{
threadDispatcher.BeginInvoke(() =>
{
onError(result);
});
}
}
}
catch (Exception ex)
{
string data2 = ex.ToString();
}
}
), null);
}), null);
}
}
Third:
Make the call to your utility class and pass your xml and xpath:
private void testButton_Click(object sender, RoutedEventArgs e)
{
Dictionary<string, string> values = new Dictionary<string, string>();
values.Add("xml", "<Foo />");
values.Add("xpath", "/*");
//Uri uri = new Uri("http://eggs/spam.ashx");
Uri uri = new Uri("http://localhost:3230/xPathHandler.ashx");
WebPostHelper.GetPostData(uri, values,
(result) =>
{
MessageBox.Show("Your result " + result);
},
(errMessage) =>
{
MessageBox.Show("An error " + errMessage);
},
this.Dispatcher);
}
Let me reiterate that the code here is simplified for brevity. You may want to use a serializer to pass more complex types to and from your generic handler. You will need to write null guard "sanity checks" in a defensive way when you get values from the context.Request.Form collection. But the basic idea is as documented above.