Loring Software, Inc

A Software Developer's Notebook
I wanted to pick up messing with WPF again, since I had not played with it since probably July. I bumped into a friend a couple weeks ago who is developing a Flex application, and I thought I would check out what it was like. I was actually impressed with Flex's architecture. I didn't download it, but followed through some of the tutorials. When I first played with WPF, I always felt that a) there were almost too many ways to solve every problem, which can often leave you wondering what the best way is, and b) binding was a bit convoluted. And while I didn't actually code anything in Flex, it just seemed cleaner. I suppose once I got into it I would uncover the ugly details, so I will save my opinion until I do.

So, in the meantime, I fired up VS and created a WPF application. Something I had read on a message board earlier gave me an idea I wanted to try out. I wanted to put together a simple application which called a web service, and bound some of its data to the fields. The message board had eluded to the Foreign Exchange Rates Web Service at the Federal Reserve.

So, I started with a simple window and a couple text boxes:

Sample App Window

Simple enough.  The stock XAML looks like this:

<Grid Background="DarkOrange" x:Name="MyGrid" >
   <Label Height="28" Margin="69,47,44,0" Name="label1" VerticalAlignment="Top">British Pound at NY noon ET</Label>
   <TextBox Margin="69,83,89,0" Name="textBox1" Text="" VerticalAlignment="Top" />
   <TextBox Margin="69,121,89,0" Name="GBP" Text="" VerticalAlignment="Top" />
</
Grid>

Now the trick is to get some data into the fields.  So, where do we get the data?  We need to add a service call to the FX site.  Simply Add/Service Reference, and type in the URI for the WSDL to the site: http://www.newyorkfed.org/markets/fxrates/WebService/v1_0/FXWS.wsdl.

The first time I did this, I did not check the "Generate Asynchronous Operations" box in the Advanced window.  Let's keep that in the back of our heads.

The call I am interested in is getLatestNoonRate().  The service returns an XML string, which we will need to parse to get the data out.  So in the the Window_Loaded event, I add

      XmlDocument a_doc = new XmlDocument();
      a_doc.LoadXml(a_serv.getLatestNoonRate("GBP"));

With some investigation of the returned XML, I see there are a couple nodes of interest: TIME_PERIOD, and OBS_VALUE, both nested a ways down in the XML.  I want to bind the data fields on my app to the returned XmlDocument, so I do some looking into that.  The first thing is to apply a data context to "something".  Back to the world of many ways to skin this beast.  Everything points to creating an XmlDataProvider, and making that the data context.  You can create the provider in code and set it to the grid property.  You can create it in a resource in the window, and set it in either code or XAML.  Or, the one I finally settled on was to put a property inside the grid, a la:

<Grid Background="DarkOrange" x:Name="MyGrid" >
    <Grid.DataContext>
       <XmlDataProvider x:Name="FXProvider" XPath="//frbny:DataSet/frbny:Series/frbny:Obs" />
    </Grid.DataContext>
    ...

And then assign the XmlDocument to the provider:

      FXProvider.Document = a_doc;

Note the XPath that I had to figure out in the Provider.  This proved to be the hardest part of the application for me.  Since the XML that was returned from the service had a namespace, I couldn't just set the XPath to "DataSet/Series/Obs", as you would with just raw XML (as you see in all the examples for XPath).  No, you have to introduce a Namespace Manager which you apply to the DataProvider.  This ties the namespace names in the XML to namespace names in your XPath queries.  I found a nice piece of code that dynamically fills in an XMLNamespaceManager for you based on the root XML object on Dot Net Junkies.  It is not perfect, since it doesn't handle all cases, but it will work for the standard case where the namespaces are all defined at the root.

      XmlNamespaceManager a_mgr = CreateNsMgr(a_doc);
     
FXProvider.XmlNamespaceManager = a_mgr;

So, now I have the data in XML, the XML is in a provider, and the provider is the data context for the grid.  From here you just have to come up with the appropriate databinding statements to go in the text fields.

        <TextBox Name="textBox1" Text="{Binding XPath=frbny:TIME_PERIOD/text()}"/>
        <TextBox Name="GBP" Text="{Binding XPath=frbny:OBS_VALUE/text()}" />

Once again, I had to play with these strings a bit until I got it right.  No preceding slash, you have to have the function looking "text()".  Not intuitive

So, I run the application, and since the fetching of the data is in the Winndow_Load, the screen locks up while it hits the web service.  Ah, I need to make the function call asynchronous!  This is where I found out that the service wizard does not create async calls to the service by default.  But the configuration window can be opened again, aysnc selected, and lots of new functions magically appear.

Once again, we have multiple ways to skin the cat.  There are the Begin/End calls that take a function pointer, and calls that run off an event.  I chose the event mechanism, since the IDE is quite happy to generate your functions for you by just typing a few letters and pressing tab:

 FXWSClient a_serv = new FXWSClient("FXWS.cfc");
 a_serv.getLatestNoonRateCompleted +=
    new
EventHandler<getLatestNoonRateCompletedEventArgs>(a_serv_getLatestNoonRateCompleted);
 
a_serv.getLatestNoonRateAsync("GBP");

void a_serv_getLatestNoonRateCompleted(object sender, getLatestNoonRateCompletedEventArgs e)
{
    XmlDocument a_doc = new XmlDocument();
    a_doc.LoadXml(e.Result);
    XmlNamespaceManager a_mgr = CreateNsMgr(a_doc);
    FXProvider.Document = a_doc;
    FXProvider.XmlNamespaceManager = a_mgr;
}

But when you run it, a strange thing happens.  The screen still freezes on startup (but only occasionally).  I recalled having a problem like this not too long ago, where it turns out the call to instanciate the service was blocking (the new FXWSClient("FXWS.cfc")).  As it turns out, when you instanciate a service, it has to call up the DNS manager on your system to resolve the domain name.  This is a blocking call.  So only half my code is asynchronous.  But in this case, the resolution of the domain name actually takes longer than the call to get the exchange rate!

So I am just going to back out all that async code that I worked so hard on, and put everything in a BackgroundWorker thread.

BackgroundWorker m_worker = new BackgroundWorker();
private void Window_Loaded(object sender, RoutedEventArgs e)
{
    m_worker.DoWork += new DoWorkEventHandler(m_worker_DoWork);
    m_worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(m_worker_RunWorkerCompleted);
    System.Diagnostics.Debug.WriteLine("calling");
    m_worker.RunWorkerAsync();
}
void m_worker_DoWork(object sender, DoWorkEventArgs e)
{
    FXWSClient a_serv = new FXWSClient("FXWS.cfc");
    XmlDocument a_doc = new XmlDocument();
    a_doc.LoadXml(a_serv.getLatestNoonRate("GBP"));
    e.Result = a_doc;
    System.Diagnostics.Debug.WriteLine("done");
}
void m_worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    XmlDocument a_doc = e.Result as XmlDocument;
    if (a_doc != null)
    {
        XmlNamespaceManager a_mgr = CreateNsMgr(a_doc);
        FXProvider.Document = a_doc;
        FXProvider.XmlNamespaceManager = a_mgr;
    }
}

Perfect.  The app starts up and sits there like a dope for a few seconds.  I can move it around the screen. And then, blam, in comes the exchange rate.  I do get this odd error message in the IDE:

System.Windows.Data Error: 43 : BindingExpression with XPath cannot bind to non-XML object.; XPath='frbny:TIME_PERIOD/text()' BindingExpression:Path=/InnerText; DataItem='XmlDataCollection' (HashCode=7457061); target element is 'TextBox' (Name='textBox1'); target property is 'Text' (type 'String') XmlDataCollection:'MS.Internal.Data.XmlDataCollection'

And it also gives it to the GDB field as having the error.  I figure it has to do with the namespace manager, so I swap the document and xmlNamespaceManager lines and the error goes away.  I also try putting a

using (FXProvider.DeferRefresh())

block around it instead, and that also gets rid of it.  I like the DeferRefresh().  It is an elegant solution to a problem I didn't solve very well when I built a DAL generator a couple years ago.

 



Copyright © 2024 Loring Software, Inc. All Rights Reserved
Questions about what I am writing about? Email me