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](images/WpfApplication2.JPG)
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.
|