Windows Presentation Foundation Data Binding: Part 2
Shawn Wildermuth
May 2006
Applies to:
Microsoft Windows Presentation Foundation
Summary: Part 2 of this series continues to illustrate how to use the XAML-based data binding to perform data manipulation in Microsoft Windows Presentation Foundation projects. (21 printed pages)
Contents
Introduction
Binding to Database Data
Where Are We?
References
Introduction
Most of the Windows Presentation Foundation (WPF) examples that are making noise in the community are about the sizzle of the graphical engine. For the majority of user interface developers, most of their job is developing everyday data entry forms in the enterprise development world. Does WPF have a solution for solving their problems? It sure does…
Binding to Database Data
In the first part of this article series, we delved into the raw binding syntax, and examined how to bind simple objects to your XAML objects. While that is an important part of the puzzle, for most situations, the real requirement is going to be binding to data stored in a database. In the majority of cases, this is support for binding in two different scenarios: Database Data (for example, DataSets, DataTables, and DataRows) and custom business objects.
Binding to Database Data
Databases are still the center of most development done today, especially enterprise development. To exemplify this, we can use a simple example of a WPF dialog box that will allow a user to browse employees in a database. We want to be able to show a small amount of information, including a picture of the employee, in our browser. We will need to load up a table with all the information that we require. We can do this by creating a new DataTable with the information from the database, as follows.
C#
DataTable theTable = new DataTable();
string connString =
ConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
string query = @"SELECT EmployeeID, FirstName, LastName, Title, HireDate, Photo
FROM Employees";
// Fill the Set with the data
using (SqlConnection conn = new SqlConnection(connString))
{
SqlDataAdapter da = new SqlDataAdapter(query, conn);
da.Fill(theTable);
}
Visual Basic .NET
Dim theTable As DataTable = New DataTable()
String connString =
ConfigurationManager.ConnectionStrings("Northwind").ConnectionString
String query = "SELECT EmployeeID, FirstName, LastName, Title, HireDate, Photo " + _
" FROM Employees"
' Fill the Set with the data
Using conn as New SqlConnection(connString))
Dim da As SqlDataAdapter = New SqlDataAdapter(query,conn)
da.Fill(theTable)
End Using
After we have the data, we can use it to set the DataContext, in order to allow it to be bound in the XAML.
C#
// Set the Data Context
DataContext = theTable;
Visual Basic .NET
' Set the Data Context
DataContext = theTable
Now that we are getting the data and supplying it to the window, we can do the data binding in the XAML. The Binding in the ComboBox simply instructs the binding to get the data from the DataContext of the parent (in this case, it walks up the control tree until it finds a DataContext in the Window).
<ComboBox Name="theCombo"
IsSynchronizedWithCurrentItem="True" ItemsSource="{Binding}"
... />
The IsSynchronizedWithCurrentItem attribute is important in that, when the selection changes, that is what changes the "current item" as far as the window is concerned. This tells the WPF engine that this object is going to be used to change the current item. Without this attribute, the current item in the DataContext won't change, and therefore your text boxes will assume that it is still on the first item in the list.
To show the employee names in the combo box, we create bindings in the ItemsTemplate to show the FirstName and LastName from the DataTable.
<DataTemplate x:Key="EmployeeListTemplate">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=FirstName}" />
<TextBlock Text=" " />
<TextBlock Text="{Binding Path=LastName}" />
</StackPanel>
</DataTemplate>
Next, we add text boxes to hold our name, title, and hire date.
<TextBlock Canvas.Top="5">First Name:</TextBlock>
<TextBox Canvas.Top="5" Text="{Binding Path=FirstName}" />
<TextBlock Canvas.Top="25">Last Name:</TextBlock>
<TextBox Canvas.Top="25" Text="{Binding Path=LastName}" />
<TextBlock Canvas.Top="45">Title:</TextBlock>
<TextBox Canvas.Top="45" Text="{Binding Path=Title}" />
<TextBlock Canvas.Top="65">Hire Date:</TextBlock>
<TextBox Canvas.Top="65" Text="{Binding Path=HireDate}" />
Since we want the photo as well, we need to add an image to the XAML.
<Image Name="theImage" Canvas.Top="5" Canvas.Left="5" Width="75"/>
The only problem with the image is that it does not support automatic binding of the photo data to the image. To facilitate this, we can handle the SelectionChanged event of the ComboBox to fill in our Image.
<ComboBox Name="theCombo"
IsSynchronizedWithCurrentItem="True"
Width="200"
ItemsSource="{Binding}"
ItemTemplate="{StaticResource EmployeeListTemplate}"
SelectionChanged="theCombo_OnSelectionChanged" />
In code, we need to load up the image from the DataTable and create a BitmapImage object to fill in the Image tag. Note that this is not a Bitmap from GDI+ (System.Drawing), but a new Bitmap object in WPF.
C#
// Handler to show the image
void theCombo_OnSelectionChanged(object sender, RoutedEventArgs e)
{
ShowPhoto();
}
// Shows the Photo for the currently selected item
void ShowPhoto()
{
object selected = theCombo.SelectedItem;
DataRow row = ((DataRowView)selected).Row;
// Get the raw bytes of the image
byte[] photoSource = (byte[])row["Photo"];
// Create the bitmap object
// NOTE: This is *not* a GDI+ Bitmap object
BitmapImage bitmap = new BitmapImage();
MemoryStream strm = new MemoryStream();
// Well-known work-around to make Northwind images work
int offset = 78;
strm.Write(photoSource, offset, photoSource.Length - offset);
// Read the image into the bitmap object
bitmap.BeginInit();
bitmap.StreamSource = strm;
bitmap.EndInit();
// Set the Image with the Bitmap
theImage.Source = bitmap;
}
Visual Basic .NET
' Handler to show the image
Sub theCombo_OnSelectionChanged(ByVal sender As Object, ByVal e As RoutedEventArgs)
ShowPhoto();
End Sub
// Shows the Photo for the currently selected item
Sub ShowPhoto()
Dim selected As Object = theCombo.SelectedItem
Dim row As DataRow = (CType(selected, DataRowView)).Row
' Get the raw bytes of the image
Dim photoSource() As Byte = CType(row("Photo"), Byte())
' Create the bitmap object
' NOTE: This is *not* a GDI+ Bitmap object
Dim bitmap As BitmapImage = New BitmapImage()
Dim strm As MemoryStream = New MemoryStream()
' Well-known work-around to make Northwind images work
Dim offset As Integer = 78
strm.Write(photoSource, offset, photoSource.Length - offset)
' Read the image into the bitmap object
bitmap.BeginInit()
bitmap.StreamSource = strm
bitmap.EndInit()
' Set the Image with the Bitmap
theImage.Source = bitmap
End Sub
We grab the SelectedItem from the ComboBox, and convert it to a DataRow so that we can get to our data. We then grab the byte array from the Photo column. This is the photo as stored in the Northwind database. We can use an in-memory stream to stream the photo bytes into a BitmapImage object. The only change is a commonly used workaround to skip the first 78 bytes of the Northwind's image header, because it isn't used any longer. Once we read the stream into the bitmap, we can assign it to the Image object as the source.
We want to make sure that our data binding is two-way, so let's make a button that shows the current information, so that we know it's in our DataRow.
C#
void SaveButton_OnClick(object sender, RoutedEventArgs e)
{
object selected = theCombo.SelectedItem;
DataRow row = ((DataRowView)selected).Row;
MessageBox.Show(string.Format("{0} {1} {2} - {3:d}",
row["Title"], row["FirstName"], row["LastName"], row["HireDate"]));
}
Visual Basic .NET
Sub SaveButton_OnClick(ByVal sender As Object, ByVal e As RoutedEventArgs)
Dim selected As Object = theCombo.SelectedItem
Dim row As DataRow = (CType(selected, DataRowView)).Row
MessageBox.Show(String.Format("{0} {1} {2} - {3:d}", _
row("Title"), row("FirstName"), row("LastName"), row("HireDate")))
End Sub
Our entire XAML file ends up looking as follows.
<Window x:Class="ExampleCS.EmployeeBrowser"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
Title="Employee Browser"
Loaded="OnLoaded"
Width="300"
Height="170"
WindowStartupLocation="CenterScreen"
>
<Window.Resources>
<DataTemplate x:Key="EmployeeListTemplate">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=FirstName}" />
<TextBlock Text=" " />
<TextBlock Text="{Binding Path=LastName}" />
</StackPanel>
</DataTemplate>
</Window.Resources>
<Window.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<LinearGradientBrush.GradientStops>
<GradientStop Color="DarkGray" Offset="0" />
<GradientStop Color="White" Offset=".75" />
<GradientStop Color="DarkGray" Offset="1" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Window.Background>
<StackPanel Name="theStackPanel"
VerticalAlignment="Top">
<ComboBox Name="theCombo"
IsSynchronizedWithCurrentItem="True"
Width="200"
ItemsSource="{Binding}"
ItemTemplate="{StaticResource EmployeeListTemplate}"
SelectionChanged="theCombo_OnSelectionChanged" />
<Canvas>
<Canvas.Resources>
<Style TargetType="{x:Type TextBox}">
<Setter Property="Canvas.Left" Value="160" />
<Setter Property="Padding" Value="0" />
<Setter Property="Height" Value="18" />
<Setter Property="Width" Value="120" />
</Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Canvas.Left" Value="90" />
<Setter Property="Padding" Value="0" />
<Setter Property="Height" Value="18" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
</Canvas.Resources>
<Image Name="theImage" Canvas.Top="5" Canvas.Left="5" Width="75"/>
<TextBlock Canvas.Top="5">First Name:</TextBlock>
<TextBox Canvas.Top="5" Text="{Binding Path=FirstName}" />
<TextBlock Canvas.Top="25">Last Name:</TextBlock>
<TextBox Canvas.Top="25" Text="{Binding Path=LastName}" />
<TextBlock Canvas.Top="45">Title:</TextBlock>
<TextBox Canvas.Top="45" Text="{Binding Path=Title}" />
<TextBlock Canvas.Top="65">Hire Date:</TextBlock>
<TextBox Canvas.Top="65" Text="{Binding Path=HireDate}" />
<Button Canvas.Top="85" Canvas.Left="90" Width="190"
Name="SaveButton" Click="SaveButton_OnClick">Save</Button>
</Canvas>
</StackPanel>
</Window>
Now, when we run the browser, we get an interface like the one shown in Figure 1.
Figure 1. Employee Browser
This simple example is fairly straightforward, but what if we are using related DataTables inside a DataSet? Let's see whether it is just as easy.
Binding Related DataTables
Let's extend the Employee Browser to include orders for which the employees are the salesperson. To do this, we will need to get order information. We could do this with a new query every time we switch users, but instead, let's load the data into a DataSet alongside the Employee, and use a DataRelation to relate the two pieces of information.
C#
DataSet theSet = new DataSet();
string connString = ConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
string employeeQuery = @"
SELECT EmployeeID, FirstName, LastName, Title, HireDate, Photo
FROM Employees
";
string orderQuery = @"
SELECT o.OrderID, EmployeeID, CompanyName, OrderDate, SUM((UnitPrice * Quantity)* (1-Discount)) as OrderTotal
FROM Orders o
JOIN [Order Details] od on o.OrderID = od.OrderID
JOIN Customers c on c.CustomerID = o.CustomerID
GROUP BY o.OrderID, o.EmployeeID, o.OrderDate, CompanyName";
// Fill the Set with the data
using (SqlConnection conn = new SqlConnection(connString))
{
SqlDataAdapter da = new SqlDataAdapter(employeeQuery, conn);
da.Fill(theSet, "Employees");
da.SelectCommand.CommandText = orderQuery;
da.Fill(theSet, "Orders");
}
// Create the relationship
DataTable empTable = theSet.Tables["Employees"];
DataTable ordTable = theSet.Tables["Orders"];
theSet.Relations.Add("Emp2Ord",
empTable.Columns["EmployeeID"],
ordTable.Columns["EmployeeID"],
false);
// Set the Context of the Window to be the
// DataTable we've created
DataContext = empTable;
Visual Basic .NET
Dim theSet As DataSet = New DataSet()
Dim connString As String = _
ConfigurationManager.ConnectionStrings("Northwind").ConnectionString
String employeeQuery = _
"SELECT EmployeeID, FirstName, LastName, Title, HireDate, Photo " + _
" FROM Employees"
String orderQuery = _
"SELECT o.OrderID, EmployeeID, CompanyName, OrderDate, " + _
" SUM((UnitPrice * Quantity)* (1-Discount)) as OrderTotal " +
"FROM Orders o " +
"JOIN (Order Details) od on o.OrderID = od.OrderID " +
"JOIN Customers c on c.CustomerID = o.CustomerID " +
"GROUP BY o.OrderID, o.EmployeeID, o.OrderDate, CompanyName"
' Fill the Set with the data
Using conn as New SqlConnection(connString)
Dim da As SqlDataAdapter = New SqlDataAdapter(employeeQuery,conn)
da.Fill(theSet, "Employees")
da.SelectCommand.CommandText = orderQuery
da.Fill(theSet, "Orders")
End Using
' Create the relationship
Dim empTable As DataTable = theSet.Tables("Employees")
Dim ordTable As DataTable = theSet.Tables("Orders")
theSet.Relations.Add("Emp2Ord",
empTable.Columns("EmployeeID"),
ordTable.Columns("EmployeeID"),
False)
' Set the Context of the Window to be the
' DataTable we've created
DataContext = empTable
This code will create a DataSet that has two tables: Employees and Orders. These tables are related by the EmployeeID through a Relation called Emp2Ord. We can still bind to the Employee DataTable so that our original data binding in the XAML works just fine. Much like Windows Forms or ASP.NET data binding, we can bind to the name of the Relation, to allow us to bind to a set of related records.
<ListBox Name="OrderList" Width="280" Height="200"
ItemsSource="{Binding Emp2Ord}"
ItemTemplate="{StaticResource OrderListTemplate}" />
This list box still uses the same DataContext as the rest of the Employee Browser; it is just specifying binding through the relationship instead. Once we bind the list box to the Relation, we can bind to individual fields in the ItemTemplate, just as we did in the employee combo box.
<DataTemplate x:Key="OrderListTemplate">
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Top" Width="100"
Text="{Binding Path=CompanyName}" />
<StackPanel>
<TextBlock Text="{Binding Path=OrderID}" />
<TextBlock Text="{Binding Path=OrderDate}" />
<TextBlock Text="{Binding Path=OrderTotal}" />
</StackPanel>
</StackPanel>
</DataTemplate>
With this additional data binding, we are now showing a list box of the order information that is related only to the selected user, as shown in Figure 2.
Figure 2. Our improved Employee Browser
This allows us to bind to more complex data than just simple rectangular pieces of data. In many organizations, they use custom .NET types (or business objects) to hold their data and business logic. Can WPF bind to these objects as easily as they can DataSets?
Binding to Business Objects
In the original incarnation of .NET, including Windows Forms and ASP.NET, the DataSet and its related objects were first-class citizens. They bound data simply and worked well. However, if you chose to build object models or business objects to hold your data instead, you were left to manually bind data from your objects to controls. In .NET 2.0, objects were raised to first-class citizens, allowing simplified binding to objects. In WPF, this continues to be true. In WPF, it is just as easy to bind to objects as DataSets.
To create our favorite Employee Browser with business objects, let's first create a class to hold our Employee.
C#
public class Employee
{
// Fields
int _employeeID;
string _firstName;
string _lastName;
string _title;
DateTime _hireDate;
BitmapImage _photo;
// Constructor
public Employee(IDataRecord record)
{
_employeeID = (int) record["EmployeeID"];
_firstName = (string) record["FirstName"];
_lastName = (string)record["LastName"];
_title = (string)record["Title"];
_hireDate = (DateTime)record["HireDate"];
CreatePhoto((byte[])record["Photo"]);
}
// BitmapImage creation
void CreatePhoto(byte[] photoSource)
{
// Create the bitmap object
// NOTE: This is *not* a GDI+ Bitmap object
_photo = new BitmapImage();
MemoryStream strm = new MemoryStream();
// Well-known hack to make Northwind images work
int offset = 78;
strm.Write(photoSource, offset, photoSource.Length - offset);
// Read the image into the bitmap object
_photo.BeginInit();
_photo.StreamSource = strm;
_photo.EndInit();
}
}
Visual Basic .NET
Public Class Employee
' Fields
Dim _employeeID As Integer
Dim _firstName As String
Dim _lastName As String
Dim _title As String
Dim _hireDate As DateTime
Dim _photo As BitmapImage
' Constructor
Public Sub New(ByVal record As IDataRecord)
_employeeID = CType(record("EmployeeID"), Integer)
_firstName = CType(record("FirstName"), String)
_lastName = CType(record("LastName"), String)
_title = CType(record("Title"), String)
_hireDate = CType(record("HireDate"), DateTime)
CreatePhoto(CType(record("Photo"), Byte()))
End Sub
' BitmapImage creation
Private Sub CreatePhoto(ByVal photoSource() As Byte)
' Create the bitmap object
' NOTE: This is *not* a GDI+ Bitmap object
_photo = New BitmapImage()
Dim strm As MemoryStream = New MemoryStream()
' Well-known hack to make Northwind images work
Dim offset As Integer = 78
strm.Write(photoSource, offset, photoSource.Length - offset)
' Read the image into the bitmap object
_photo.BeginInit()
_photo.StreamSource = strm
_photo.EndInit()
End Sub
End Class
This class takes in an IDataRecord class (a single result from a DataReader, but we'll get there in a minute), and fills in the same fields we used with the DataTable example earlier in this article. Note that we've moved the creation of the BitmapImage here to the business object, to make it simpler to use the employee in the UI class.
Next, we will want property accessors for the fields.
C#
// Read-Only
public int EmployeeID
{
get { return _employeeID; }
}
public string FirstName
{
get { return _firstName; }
set { _firstName = value; }
}
public string LastName
{
get { return _lastName; }
set { _lastName = value; }
}
public string Title
{
get { return _title; }
set { _title = value; }
}
public DateTime HireDate
{
get { return _hireDate; }
set { _hireDate = value; }
}
// Read-Only
public BitmapImage Photo
{
get { return _photo; }
}
Visual Basic .NET
' Read-Only
Public ReadOnly Property EmployeeID() As Integer
Get
Return _employeeID
End Get
End Property
Public Property FirstName() As String
Get
Return _firstName
End Get
Set (ByVal Value As String)
_firstName = value
End Set
End Property
Public Property LastName() As String
Get
Return _lastName
End Get
Set (ByVal Value As String)
_lastName = value
End Set
End Property
Public Property Title() As String
Get
Return _title
End Get
Set (ByVal Value As String)
_title = value
End Set
End Property
Public Property HireDate() As DateTime
Get
Return _hireDate
End Get
Set (ByVal Value As DateTime)
_hireDate = value
End Set
End Property
' Read-Only
Public ReadOnly Property Photo() As BitmapImage
Get
Return _photo
End Get
End Property
In these, we are simply allowing read–write (or read-only) access to the fields in the class.
Now, we can write a collection to hold our employees.
C#
public class EmployeeList : ObservableCollection<Employee>
{
public EmployeeList()
{
string connString =
ConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
string query = @"
SELECT EmployeeID, FirstName, LastName, Title, HireDate, Photo
FROM Employees
";
// Fill the Set with the data
using (SqlConnection conn = new SqlConnection(connString))
{
try
{
SqlCommand cmd = conn.CreateCommand();
cmd.CommandText = query;
conn.Open();
SqlDataReader rdr = cmd.ExecuteReader();
while (rdr.Read())
{
Add(new Employee(rdr));
}
}
finally
{
if (conn.State != ConnectionState.Closed) conn.Close();
}
}
}
}
Visual Basic .NET
Public Class EmployeeList
Inherits ObservableCollection<Employee>
Public Sub New()
String connString =
ConfigurationManager.ConnectionStrings("Northwind").ConnectionString
String query = _
"SELECT EmployeeID, FirstName, LastName, Title, HireDate, Photo " + _
" FROM Employees"
' Fill the Set with the data
Using conn as New SqlConnection(connString)
Try
Dim cmd As SqlCommand = conn.CreateCommand()
cmd.CommandText = query
conn.Open()
Dim rdr As SqlDataReader = cmd.ExecuteReader()
While rdr.Read()
Add(New Employee(rdr))
End While
Finally
If conn.State <> ConnectionState.Closed Then
conn.Close()
End If
End Try
End Using
End Sub
End Class
The base class of the collection is the ObservableCollection class, which provides a mechanism to allow the UI to know if new members are added to the collection. We've moved the data access from the UI page to the collection class. When this class is created, we query the database and add new employees to the collection from the DataReader.
Now that we have the collection and the individual objects, we can import the classes into the XAML with a mapping (explained in detail in the first part of this article series).
<Window
...
xmlns:e="Example" DataContext="{StaticResource EmployeeList}"
>
<Window.Resources>
<e:EmployeeList x:Key="EmployeeList" />
...
</Window.Resources>
...
</Window>
We import the class into the XAML document by using the ?Mapping declaration. And, we specify the EmployeeList in the Resources, so that we can use it as the DataContext of our window. In this way, the rest of our XAML file is identical to the original Employee Browser, because we are still tying to the same field names we had in the DataSet example. The only change we can make is to bind the BitmapImage in the XAML document, instead of doing it in the code-behind.
...
<Image Name="theImage" Canvas.Top="5" Canvas.Left="5" Width="75"
Source="{Binding Path=Photo}"/>
...
We now have an identically behaving Employee Browser (see Figure 3).
Figure 3. Business object–based Employee Browser
In addition to using the type mapping, you can also use the ObjectDataProvider to bring the objects into the XAML. As I showed in the first part of this article series, all you need is to specify a key and the type name.
<ObjectDataProvider x:Key="EmployeeList"
TypeName="Example.Data.EmployeeList, ExampleCS"/>
The x:Key is just a moniker to be used in the binding, and the Typename is the class name and assembly (in this case, the same assembly that our UI resides in). The rest of the XAML remains the same, because we're loading in the same data.
Where Are We?
We can now load data from a database, using either DataSets or custom objects, and bind the data directly to WPF objects. You should now be ready to dive into your first WPF database project.
References
- WinFX SDK
- Programming the Windows Presentation Foundation, by Chris Sells and Ian Griffiths
- Part 1 of this article series