views:

471

answers:

2

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)?

+1  A: 

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.

Henrik Söderlund
I doubt its so much that Microsoft __want__ you to use Linq. Its more likely that in view of wanting to keep download sizes to a minimum and the fact that Linq is pretty good at doing what you could do with XPath that it wasn't prioritized.
AnthonyWJones
It turns out that XPath support is available in Silverlight 4: http://programmerpayback.com/2010/04/01/xpath-support-in-silverlight-4-xpathpad/
Henrik Söderlund
A: 

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:

  1. System.IO

  2. System.Xml

  3. System.Xml.XPath

  4. 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:

  1. The Uri of your generic handler

  2. A dictionary of key value pairs (assuming the xml document and the xpath)

  3. Callbacks for success and failure

  4. 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.

David in Dakota
-1. These days I rarely downvote but I feel compeled to do so in this case. Taking the XML which could be quite large, encoding it into `x-www-form-urlencoded` (with all sorts of character encoding headaches that may create), restoring it all on the server just to execute an XPath and return a result (which also could be large) which as far as I can see also generates incomprehensible result.
AnthonyWJones
Anthony, I take your rare downvote as a "point well taken" however I'd encourage you to offer a solution. An xml file can be quite large (e.g. iTunes library) but in many circumstances they are smaller by design (e.g. typical RSS feed). The AJAX style post with character encodings (Server.UrlDecode probably should be in the sample) work fine (take the code above and implement it, you will be able to see for yourself).
David in Dakota