Binding to Relational Data

Although UI designer support is still being developed to help bring relational data into your WPF pages specifically, the tools we've already got can be pressed into service for WPF work without issue. For example, assume a table like the one in Figure 7-18 defined in an Access database (family.mdb).

People

H X

ID

Name

Age

Add New Field

1

Tom

11

2

John

12

3

Melissa

38

*

(New)

0

Record: H - |1 of 3

> >1 J

"ÎL No Filter

Search

Figure 7-18. A Person table in Access (family.mdb)

Although we could write the ADO.NET code to bring this table into our project, we don't have to. Instead, we can bring in the data using the typed dataset designer, which has been in Visual Studio since .NET 1.0. Bringing a new typed data set into your project is as simple as right-clicking on your project, choosing Add ^ New Item ^ Dataset, choosing a name, and clicking Add. This brings up the typed dataset designer, onto which you can drag any number of tables, setting up relationships and specifying the way you'd like the data to be projected into your project. A ready source of data for the data set design is the Server Explorer, which you can use to connect to various databases. To connect to the Access database, family.mdb, I right-clicked on Data Connections and chose Add Connection, configuring things properly for Access. I then drilled in to the People table and dragged it onto the designer surface, as shown in Figure 7-19.

Figure 7-19. Creating a typed data set in a WPF project works just fine

All of these dragging and dropping shenanigans produced for me three interesting classes: PeopleRow, PeopleDataTable, and PeopleTableAdapter, summarized in Example 7-34 from the generated Family.Designer.cs file.

Example 7-34. The interesting class the typed dataset designer generates namespace AdoBinding {

public partial class Family : System.Data.DataSet {

public partial class PeopleRow : System.Data.DataRow {

public int ID { get {...} set {...} } } public string Name { get {...} set {...} } } public int Age { get {...} set {...} } }

public partial class PeopleDataTable : System.Data.DataTable, System.Collections.IEnumerable {

public PeopleRow AddPeopleRow(string Name, int Age) {...}

public PeopleRow FindByID(int ID) {...}

public void RemovePeopleRow(PeopleRow row) {...}

namespace FamilyTableAdapters {

public partial class PeopleTableAdapter :

System.ComponentModel.Component {

public virtual Family.PeopleDataTable GetData() {...}

The PeopleRow class is a typed wrapper around the DataRow class built into ADO.NET. It's the thing that maps between the underlying database types and the CLR types. When you bind to relational data in WPF, you'll be binding to a DataTable full of these DataRow-derived objects. Actually, just plain DataRow objects work, too—you don't have to use the typed dataset designer to make this work. However, if you do, you also get the benefit of the generated table adapters, like our PeopleDataTable, which knows the shortest way to create and find PeopleRow objects, and the PeopleTableAdapter, which knows how to read and write data to and from Access (in our case), to get the data and track updates for pushing back to the database.

The one other thing we get is the connection string plopped into the app.config so that it can be maintained separately from the code, as you can see in Example 7-35.

Example 7-35. The connection string we get when we add a new data connection

<?xml version="1.0" encoding="utf-8" ?> <configuration> <connectionStrings> <add name="AdoBinding.Properties.Settings.familyConnectionString" connectionString="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=family.mdb" providerName="System.Data.OleDb" /> </connectionStrings> </configuration>

With these two wrappers in place, and the connection string all set up for us in the app's .config file, all we really have to do is create an instance of the PeopleTableAdapter and call GetData, binding to the results. We could do this in the main window's constructor if we wanted to, as shown in Example 7-36.

Example 7-36. Using the classes generated by the dataset designer public Window1() { InitializeComponent();

// Get the data for binding synchronously

DataContext = (new FamilyTableAdapters.PeopleTableAdapter()).GetData();

In Example 7-36, the GetData call is synchronous, which is fine for our simple sample. However, because in a real app we're often accessing data that is located over a network connection, synchronously retrieving the data and blocking the UI thread while we wait isn't such a good idea. This is an excellent use of the asynchronous support we've got in the object data provider (see Example 7-37).

Example 7-37. Binding to relational data declaratively

<!-- Window1.xaml --> <Window ... xmlns:local="clr-namespace:AdoBinding"

xmlns:tableAdapters="clr-namespace:AdoBinding.FamilyTableAdapters">

<Window.Resources> <ObjectDataProvider x:Key="Family"

ObjectType="{x:Type tableAdapters:PeopleTableAdapter}"

IsAsynchronous="True"

MethodName="GetData" />

<local:AgeToForegroundConverter x:Key="ageConverter" /> </Window.Resources>

<Grid DataContext="{StaticResource Family}">

<ListBox ... ItemsSource="{Binding}"> <ListBox.ItemTemplate> <DataTemplate>

Example 7-37. Binding to relational data declaratively (continued)

<TextBlock> <TextBlock Text="{Binding Path=Name}" /> (age: <TextBlock Text="{Binding Path=Age}" Foreground=" {Binding Path=Age, Converter=

{StaticResource ageConverter}}" />) </TextBlock> </DataTemplate> </ListBox.ItemTemplate> </ListBox>

At the top of Example 7-37, we're doing just what we did in code—that is, creating an instance of the PeopleTableAdapter type, calling GetData, and binding to the results. The difference is that we're doing it declaratively, which makes it very easy to bind asynchronously—all we have to do is set the IsAsynchronous property to True and the data retrieval happens on a worker thread, keeping the UI from freezing.

Another thing to notice is that the bindings are all the same as before, although in this case, we're using the names of the columns as properties and trusting ADO.NET and WPF to negotiate properties dynamically at runtime via the ICustomTypeDescriptor interface.* Finally, notice that our use of the age to foreground brush value converter remains the same.

Example 7-37 uses a template created specifically for use by the ListBox object's ItemTemplate property instead of a typed data template to automatically share across content controls. This is because we're no longer dealing with objects of the custom Person class at the top level of a namespace, but objects of type DataRowView.

Because we've got the ADO.NET DataRowView type and the typed dataset designergenerated PeopleDataTable and PeopleDataRow types instead of our custom Person and People types, implementing our data management code is a little different, as you can see in Example 7-38.

* The ICustomTypeDescriptor interface has been with us since .NET 1.0 for data bound objects to expose properties not known until runtime (e.g., the dynamic results of an SQL query). In the case ofADO.NET, even though we used the typed dataset designer to get typed properties, WPF will still use the DataRowView class's implementation of ICustomTypeDescriptor, which is why typed and untyped data sets work equally well.

Example 7-38. Accessing the data held by ADO.NET // Window1.xaml.cs using System.Data; using System.Data.OleDb;

public partial class Window1 : Window {

public Windowl() { InitializeComponent();

this.birthdayButton.Click += birthdayButton_Click; this.backButton.Click += backButton_Click; this.forwardButton.Click += forwardButton_Click; this.addButton.Click += addButton_Click; this.sortButton.Click += sortButton_Click; this.filterButton.Click += filterButton_Click; this.groupButton.Click += groupButton_Click;

ICollectionView GetFamilyView() { DataSourceProvider provider =

(DataSourceProvider)this.FindResource("Family"); return CollectionViewSource.GetDefaultView(provider.Data);

void birthdayButton_Click(object sender, RoutedEventArgs e) { ICollectionView view = GetFamilyView();

// Each item is a DataRowView, which we can use to access // the typed PersonRow AdoBinding.Family.PeopleRow person =

(AdoBinding.Family.PeopleRow)((DataRowView)view.CurrentItem).Row;

++person.Age; MessageBox.Show( string.Format(

"Happy Birthday, {0}, age {l}!", person.Name, person.Age), "Birthday");

void backButton_Click(object sender, RoutedEventArgs e) { ICollectionView view = GetFamilyView(); view.MoveCurrentToPrevious(); if( view.IsCurrentBeforeFirst ) { view.MoveCurrentToFirst( );

void forwardButton_Click(object sender, RoutedEventArgs e) { ICollectionView view = GetFamilyView();

Example 7-38. Accessing the data held by ADO.NET (continued)

view.MoveCurrentToNext(); if( view.IsCurrentAfterLast ) { view.MoveCurrentToLast();

void addButton_Click(object sender, RoutedEventArgs e) { // Creating a new PeopleRow DataSourceProvider provider =

(DataSourceProvider)this.FindResource("Family"); AdoBinding.Family.PeopleDataTable table =

(AdoBinding.Family.PeopleDataTable)provider.Data; table.AddPeopleRow("Chris", 37);

void sortButton_Click(object sender, RoutedEventArgs e) { ICollectionView view = GetFamilyView(); if( view.SortDescriptions.Count == 0 ) { view.SortDescriptions.Add(

new SortDescription("Name", ListSortDirection.Ascending)); view.SortDescriptions.Add(

new SortDescription("Age", ListSortDirection.Descending));

view.SortDescriptions.Clear();

void filterButton_Click(object sender, RoutedEventArgs e) { // Can't set the Filter property, but can set the // CustomFilter on a BindingListCollectionView BindingListCollectionView view =

(BindingListCollectionView)GetFamilyView(); if( string.IsNullOrEmpty(view.CustomFilter) ) { view.CustomFilter = "Age > 25";

view.CustomFilter = null;

void groupButton_Click(object sender, RoutedEventArgs e) { ICollectionView view = GetFamilyView(); if( view.GroupDescriptions.Count == 0 ) { // Group by age view.GroupDescriptions.Add(new PropertyGroupDescription("Age"));

view.GroupDescriptions.Clear();

In Example 7-38, you'll notice that manipulating and displaying a person is different because we're dealing with a DataRowView object's Row property to get the typed PeopleRow we want. Also, adding a new person is different because we're dealing with a PeopleDataTable. Finally, filtering is different because the BindingListCollectionView doesn't support the Filter property (setting it causes an exception at runtime). However, we set the CustomFilter string on the BindingListCollectionView using the ADO. NET filter syntax. Everything else, though—including accessing the collection view, navigating the rows, and even sorting and grouping—is the same, as shown in Figure 7-20.

|il AdoBinding

Name: Tom

Age: 11

[ Birthday ] [ < j [ > ] [ Add "] [ Sort ] [ Filter | [ Group

Figure 7-20. ADO.NET data binding in action

So, although there was no relational data-specific data provider, none is needed—the object data provider works just fine for data binding to relational data in WPF.

Was this article helpful?

0 0
Project Management Made Easy

Project Management Made Easy

What you need to know about… Project Management Made Easy! Project management consists of more than just a large building project and can encompass small projects as well. No matter what the size of your project, you need to have some sort of project management. How you manage your project has everything to do with its outcome.

Get My Free Ebook


Post a comment