4.4. Data Sources
So far, we've been dealing simply with objects. However, that's
not the only place that data can come from; XML and relational databases spring
to mind as popular alternatives. Further, since neither XML nor relational
databases store their data as .NET objects, some translation is going to be
needed to support data binding which, as you recall, requires .NET properties
on data-source objects. And even if we can declare objects directly in XAML,
we'd still like a layer of indirection for pulling objects from other sources
and even pushing that work off to a worker thread if said retrieval is a
ponderous operation.
In short, we'd like some indirection away from the direct
declaration of objects for translation and loading. For this indirection, we
have but to turn to IDataSource implementations, one of which is the
object data source.
4.4.1. Object Data Source
An implementation of the IDataSource interface provides
a layer of indirection for all kinds of operations that produce objects against
which to data-bind. For example, if we wanted to load a set of Person objects
over the Web, we could encapsulate that logic into a bit of code, such as
Example 4-34.
Example 4-34. A type to be used by ObjectDataSource
namespace PersonBinding {
public class Person : INotifyPropertyChanged {...}
public class People : ObservableCollection<Person> {}
public class RemotePeopleLoader : People {
public RemotePeopleLoader( ) {
// Load people from afar
...
}
}
}
In Example 4-34,
the RemotePeopleLoader class derives from the People collection
class, retrieving the data in the constructor, because the object data source
expects the object it creates to be the collection, as in
Example 4-35.
Example 4-35. Using the ObjectDataSource
<Window.Resources>
...
<ObjectDataSource
x:Key="Family"
TypeName="PersonBinding.RemotePeopleLoader"
Asynchronous="True" />
</Window.Resources>
<Grid DataContext="{StaticResource Family}">
...
<ListBox ItemsSource="{Binding}" ...>
</Grid>
The ObjectDataSource element is most often placed in a
resource block to be used by name elsewhere in the XAML. The TypeName property
refers to the fully qualified type name of the class that will be the
collection.
 |
Most of the classes in WPF that take type
parameters, such as the DataType property of the DataTemplate
element, can be set with the type markup extension, which includes class,
namespace, and assembly information using the mapping syntax:
<!-- set up DataTemplate for Bar.Quux in assembly foo -->
<?Mapping
XmlNamespace="local"
ClrNamespace="Bar" Assembly="foo" /><Window ... xmlns="local">
<Window.Resources>
<DataTemplate
DataType="{x:Type local:Quux}">...</DataTemplate>
</Window.Resources>
...
</Window>
However, the ObjectDataSource takes its type
information in its own form:
<!-- set up ObjectDataSource for Bar.Quux in assembly foo -->
<ObjectDataSource x:Key="foo" TypeName="Bar.Quux, foo" />
One can only hope that the two techniques will be rationalized
by RTM.
|
|
With an object data source acting as an intermediary between the
data and the bindings, we need to update our code when we're retrieving the People
collection (now a base class of the RemotePeopleLoader, but still the
container of our Person objects), as in
Example 4-36.
Example 4-36. Accessing the data held by an object data
source
public partial class Window1 : Window {
...
ICollectionView GetFamilyView( ) {
IDataSource
ds = (IDataSource)this.FindResource("Family");
People people = (People)ds.Data;
return BindingOperations.GetDefaultView(people);
}
void birthdayButton_Click(object sender, RoutedEventArgs e) {
ICollectionView view = GetFamilyView( );
Person person = (Person)view.CurrentItem;
++person.Age;
MessageBox.Show(...);
}
void addButton_Click(object sender, RoutedEventArgs e) {
IDataSource ds = (IDataSource)this.FindResource("Family");
People people = (People)ds.Data;
people.Add(new Person("Chris", 35));
}
}
Since the Family resource is now an ObjectDataSource,
itself an implementation of the IDataSource, in
Example 4-36, when we need the People collection, we're
casting to IDataSource on the Family resource and pulling the
collection out of the Data property.
 |
Even though the object data source exposes its data
from the Data property, that doesn't mean that you should bind to it.
If you notice from Example 4-35,
we're still binding the listbox as before:
<!-- do not bind to Path=Data -->
<ListBox ItemsSource="{Binding}" ...>
The reason this works is because WPF has built-in knowledge of IDataSource,
so there's no need for you to do the indirection yourself.
|
|
4.4.1.1. Asynchronous data retrieval
In Example 4-35,
we applied the Asynchronous property, which is easily the most
interesting piece of functionality that the object data source gives us that we
lack when we declare object graphs directly in XAML. When the Asynchronous
property is set to true (the default is false), the task of creating the object
specified by the TypeName property is handled on a worker thread, only
performing the binding on the UI thread when the data has been retrieved. This
is not the same as binding to the data as its retrievede.g., from a stream over
the networkbut it's better than blocking the UI thread while a long retrieval
happens.
4.4.1.2. Passing parameters
In addition to the Asynchronous property, the object
data source also provides the Parameters property, which is a
comma-delimited list of strings to be passed as string arguments to the type
created by the object data source. For example, if we wanted to pass in a set
of URLs from which to try and retrieve the data, we could use the Parameters
property as in Example 4-37.
Example 4-37. Passing parameters via ObjectDataSource
<ObjectDataSource
x:Key="Family"
TypeName="PersonBinding.RemotePeopleLoader"
Asynchronous="True"
Parameters="http://sellsbrothers.com/sons.dat,
http://sellssisters.com/daughters.dat" />
In Example 4-37,
we've added a list of two URLs, which will be translated into a call to the RemotePeopleLoader
constructor that takes two strings, as in
Example 4-38.
Example 4-38. Accepting arguments passed by
ObjectDataSource
namespace PersonBinding {
public class RemotePeopleLoader : People {
public RemotePeopleLoader(string url1, string url2) {
// Load People from afar using two URLs
...
}
}
Unfortunately, if we put other data types into the list of
parameters supported by the object data source's Parameters property,
like integers, they will not be translated, even if constructors of the
appropriate types are available; the object data source only supports the
creation of objects with constructors taking zero or more strings. If any data
conversion is necessary, you will have to do it.
4.4.2. XmlDataSource
As I mentioned, while objects are the only thing that data
binding supports, data isn't only stored as objects in the world. In fact, most
data isn't stored in objects. One increasingly popular way to store data is
XML. For example, Example 4-39
shows our family data represented in XML.
Example 4-39. My family rendered in XML
<!-- family.xml -->
<Family xmlns="">
<Person Name="Tom" Age="9" />
<Person Name="John" Age="11" />
<Person Name="Melissa" Age="36" />
</Family>
With this file available in the same folder as the executing
application, we can bind to it using the XmlDataSource, as shown in
Example 4-40.
Example 4-40. An XmlDataSource in action
<!-- Window1.xaml -->
<Window ...>
<Window.Resources>
...
<XmlDataSource
x:Key="Family"
Source="family.xml"
XPath="/Family/Person" />
</Window.Resources>
<Grid DataContext="{StaticResource Family}">
...
<ListBox ... ItemsSource="{Binding}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock TextContent="{Binding XPath=@Name}" />
<TextBlock TextContent=" (age: " />
<TextBlock TextContent="{Binding XPath=@Age}" ... />
<TextBlock TextContent=")" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<TextBlock ...>Name:</TextBlock>
<TextBox Text="{Binding XPath=@Name}" .../>
<TextBlock ...>Age:</TextBlock>
<TextBox Text="{Binding XPath=@Age}" ... />
...
</Grid>
</Window>
Notice the use of the XmlDataSource with a relative URL
that points to the family.xml file and the XPath expression
that pulls out the Person elements under the Family element root. The
only other thing that changes in the XAML file from the use of the ObjectDataSource
is that when binding Name and Age to TextBlock and TextBox
controls, we use XPath statements instead of Path statements.
4.4.2.1. XML island data
If you happen to know your data at compile time, the XML data
source also supports "data islands
" in the same way that XAML creating objects directly does, as in
Example 4-41.
Example 4-41. An XML data island in XAML
<XmlDataSource x:Key="Family" XPath="/Family/Person">
<Family xmlns="">
<Person Name="Tom" Age="9" />
<Person Name="John" Age="11" />
<Person Name="Melissa" Age="36" />
</Family>
</XmlDataSource>
In Example 4-41,
we've just copied the contents of family.xml under the XmlDataSource
element, dropping the Source attribute but leaving the XPath
statement.
However, now that we're using XML instead of object data, some
of the operations in our sample application need changing, such as accessing
and/or changing the current item (as we do in the Birthday button
implementation), adding a new item, and sorting items or filtering items. In
short, anything where we assumed a collection of Person objects needs
to change. On the other hand, moving between items using the ICollectionView.MovingCurrentToXxx(
) family of methods continues to work just fine, as does our AgeToForegroundValueConverter.
 |
Our implementation of IValueConverter.Convert
continues to work because we parsed the string value of the object instead of
casting directly to an Int32. A cast would have been preferred in the Person
object case, because Age was of type Int32 and parsing it was
unnecessary. However, in the XML case and in the absence of any application of
an XSD, Age is of type String, so the parsing is necessary.
|
|
4.4.2.2. XML data sources and item access
To access and manipulate items in an XML data source, instead of
instances of your custom type, you'll be using instances of the XmlElement
class from the System.Xml namespace, as in
Example 4-42.
Example 4-42. Accessing XML from an XML data source
// Window1.xaml.cs
...
namespace PersonBinding {
public partial class Window1 : Window {
...
ICollectionView GetFamilyView( ) {
IDataSource ds = (IDataSource)this.FindResource("Family");
IEnumerable people = (IEnumerable)ds.Data;
return BindingOperations.GetDefaultView(people);
}
void birthdayButton_Click(object sender, RoutedEventArgs e) {
ICollectionView view = GetFamilyView( );
XmlElement person = (XmlElement)view.CurrentItem;
person.SetAttribute("Age",
(int.Parse(person.Attributes["Age"].Value) + 1).ToString( ));
MessageBox.Show(
string.Format(
"Happy Birthday, {0}, age {1}!",
person.Attributes["Name"].Value,
person.Attributes["Age"].Value),
"Birthday");
}
...
}
}
The first thing to notice in
Example 4-42 is that in our implementation of GetFamilyView,
we're no longer looking for the People collection directly but rather
some implementation of IEnumerable provided by the XML data source. IEnumerable
is the simplest thing you can have in .NET and still have a collection, so
that's what the GetdefaultView method requires.
Also notice in Example
4-42 that the CurrentItem property of the collection view is
an instance of the XmlElement. To increment the age, we access the
element's Age attribute, pull out its value, parse it as an integer,
increment it, convert the whole thing back to a string, and set it as the new Age
attribute value of the current element. Showing each attribute is merely
another couple of attribute accesses.
4.4.2.3. XML data sources and adding items
When adding (or removing) items, it's best to get access to the XmlDataSource
itself so that you can access the Document property for creating and
adding new elements, as in Example
4-43.
Example 4-43. Adding an item to an XML data source
void addButton_Click(object sender, RoutedEventArgs e) {
XmlDataSource xds = (XmlDataSource)this.FindResource("Family");
XmlElement person = xds.Document.CreateElement("Person");
person.SetAttribute("Name", "Chris");
person.SetAttribute("Age", "35");
xds.Document.ChildNodes[0].AppendChild(person);
}
Here, we're using the XmlDataSource to get to the XmlDocument
and then using the XmlDocument to create a new element called Person
(to fit in with the rest of our Person elements), setting the Name
and Age attributes, and adding the element under the Family root
element (available at ChildNodes[0] on the top-level Document
object).
4.4.2.4. XML data sources and sorting
Sorting XML data-source items is a matter of remembering that
we're dealing with XmlElements, as in
Example 4-44.
Example 4-44. Sorting XML
class PersonSorter : IComparer {
public int Compare(object x, object y) {
XmlElement lhs = (XmlElement)x;
XmlElement rhs = (XmlElement)y;
// Sort Name ascending and Age descending
int nameCompare =
lhs.Attributes["Name"].Value.CompareTo(
rhs.Attributes["Name"].Value);
if( nameCompare != 0 ) {
return nameCompare;
}
return int.Parse(rhs.Attributes["Age"].Value) -
int.Parse(lhs.Attributes["Age"].Value);
}
}
void sortButton_Click(object sender, RoutedEventArgs e) {
ListCollectionView view = (ListCollectionView)GetFamilyView( );
// Managing the view.Sort collection would work, too
if( view.CustomSort == null ) {
view.CustomSort = new PersonSorter( );
}
else {
view.CustomSort = null;
}
}
In Example 4-44,
we're sorting just like before, but we're pulling out the Name and Age
attributes and converting as appropriate to do so.
4.4.2.5. XML data sources and filtering
XML filtering is very much like object filtering, except that
we're dealing with XmlElements, as in
Example 4-45.
Example 4-45. Filtering XML
void filterButton_Click(object sender, RoutedEventArgs e) {
ICollectionView view = GetFamilyView( );
if( view.Filter == null ) {
view.Filter = delegate(object item) {
return
int.Parse(((XmlElement)item).Attributes["Age"].Value) >= 18;
};
}
else {
view.Filter = null;
}
}
Here our filter delegate casts each item to an XmlElement
to do the filtering.
4.4.3. Relational Data Source
As of the current build, WPF has no direct support for binding
to relational databases, and the indirect support
is not in such great shape, either. For an example of the current state of
binding to relational data in WPF, I recommend the WinFX SDK sample entitled
"Binding with Data in an ADO DataSet Sample."
4.4.4. Custom Data Sources
If you'd like to take advantage of the indirection that data
sources provide for retrieving objects, but none of the built-in data sources
tickles you, a custom implementation of IDataSource
should do the trick. For example, instead of creating the RemotePersonLoader
collection to load the remove family data (it was kind of hokey to add
collection items in the collection's constructor, anyway), we could have
created a custom implementation of IDataSource to do the magic, as in
Example 4-46.
Example 4-46. A simple custom data source
namespace PersonBinding {
public class Person : INotifyPropertyChanged {...}
public class People : ObservableCollection<Person> {}
public class RemotePeopleSource : IDataSource {
People people = null;
public RemotePeopleSource( ) {
// Load People from afar
...
// Let data binding know we've got data
if( DataChanged != null ) {
DataChanged(this, EventArgs.Empty);
}
}
// IDataSource Members
// Gets the underlying data object
public object Data {
get { return people; }
}
// Occurs when a new data object becomes available
// Especially handy for async object retrieval
public event EventHandler DataChanged;
// Refreshes the data source object using the most current
// values for the object's configuration properties
public void Refresh( ) {
// Not needed in our case...
}
}
}
In Example 4-46,
we've implemented IDataSource by creating an instance of the People
collection, and, after some mysterious data retrieval process in the
constructor, we fire off an event to let data binding know that we've got data
and that it should now recheck the Data property. This protocol is
especially useful if you're doing asynchronous data retrieval like the object
data source does.
If your data source provides custom propertiese.g., Asynchronousit's
possible that one or more of the properties could be changed at runtime. If
you've got more than one property that affects data retrieval, you may not want
to kick off the search for the new data until the Refresh method is
called; otherwise, you may start things off after one property is changed but
before the client has a chance to change the rest of them.
 |