Share via


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.

Aa480226.wpfdabndpt201(en-us,MSDN.10).gif

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.

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.

Aa480226.wpfdabndpt202(en-us,MSDN.10).gif

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).

Aa480226.wpfdabndpt203(en-us,MSDN.10).gif

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