Partager via


Databinding in WindowsForms and WPF Hybrid Apps

(Blogging Tunes: Listening to Groove Salad on SomaFm)

Okay, so we've talked about how you can host a Windows Forms Control in a WPF application, but what if you want to bind the Windows Forms controls and WPF controls to the same data source?  Is that even possible?  Does the Pope spit in the woods?  (or something like that).

Let's examine this problem by building a sample based on the Northwind database that I'm sure you have become painfully familiar with over the years.  First, let's create an Avalon application using VS.  Now we need to design our user interface for the application.  What we intend to do is create a form that will contain a list of customers and their corresponding orders.  You will recognize this as the classic master/details relationship.  (If it helps you to hum Depeche Mode's "Master and Servant" in your head, then knock yourself out).

We'll use a WPF ListBox to contain a list of the customer names and then a series of WPF TextBoxes that will contain the more information about the customer.  But hey, this is all about the co-existence of Windows Forms and WPF isn't it?  So we will also add a Windows Forms DataGridView control to the form that will contain all of the products ordered by specific customers.  So rather than bore you with the complete XAML listing for the UI, I will just highlight the salient points.

We will create a DataTemplate for our ListBox.  This will allow us to have control over the visual tree of the ListBox and to decide exactly what data elements we want to appear in the ListBox.

       <Grid.Resources>
        <DataTemplate x:Key="ListItemsTemplate">
          <StackPanel Orientation="Horizontal">
            <TextBlock Text="{Binding Path=ContactName}"/>
          </StackPanel>
        </DataTemplate>
      </Grid.Resources>

The interesting bit here is the "{Binding Path=ContactName}" part.  This tells the TextBlock part of our ListBox that it should get it's values from the ContactName field in the database.  Next let's layout the controls on the form:

       <StackPanel Orientation="Vertical" Grid.Row="0" Grid.Column="0">
        <Label Margin="20,5,5,0">List of Customers:</Label>
        <ListBox x:Name="listBox1" Height="200" Width="200" HorizontalAlignment="Left" 
           ItemTemplate="{StaticResource ListItemsTemplate}" IsSynchronizedWithCurrentItem="True" Margin="20,5,5,5"/>
        </StackPanel>
       <StackPanel Orientation="Vertical" Grid.Row="0" Grid.Column="1">
        <Label Margin="20,38,5,2">First Name:</Label>
        <Label Margin="20,0,5,2">Company Name:</Label>
        <Label Margin="20,0,5,2">Phone:</Label>
        <Label Margin="20,0,5,2">Address:</Label>
        <Label Margin="20,0,5,2">City:</Label>
        <Label Margin="20,0,5,2">Region:</Label>
        <Label Margin="20,0,5,2">Postal Code:</Label>
      </StackPanel>
       <StackPanel Orientation="Vertical" Grid.Row="0" Grid.Column="2">
        <TextBox Margin="5,38,5,2" Width="200" Text="{Binding Path=ContactName}"/>
        <TextBox Margin="5,0,5,2" Width="200" Text="{Binding Path=CompanyName}"/>
        <TextBox Margin="5,0,5,2" Width="200" Text="{Binding Path=Phone}"/>
        <TextBox Margin="5,0,5,2" Width="200" Text="{Binding Path=Address}"/>
        <TextBox Margin="5,0,5,2" Width="200" Text="{Binding Path=City}"/>
        <TextBox Margin="5,0,5,2" Width="30" HorizontalAlignment="Left" Text="{Binding Path=Region}"/>
        <TextBox Margin="5,0,5,2" Width="50" HorizontalAlignment="Left" Text="{Binding Path=PostalCode}"/>
      </StackPanel>
       <wfi:WindowsFormsHost Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" Margin="20,5,5,5" Height="300">
        <wf:DataGridView x:Name="dataGridView1"/>
      </wfi:WindowsFormsHost>

Notice that our ListBox tag has its ItemTemplate property set to {StaticResource ListItemTemplate}.  This is how we tell the ListBox to use the DataTemplate that we created above to manage the items in the ListBox.  Note that we also set the IsSynchronizedWithCurrentItem property to TRUE.  This will make sure that the ListBox is in sync with the underlying data source.

The next bit that is of interest is our TextBoxes.  Note that we have their respective Text properties set to point to the column of the database table that we want displayed.  This is accomplished using the {Binding Path=foo} statement.  After our TextBoxes, we add our WindowsFormsHost control (you should already be up to speed on this control from our previous discussions).  Then we add the Windows Forms DataGridView control to the WindowsFormsHost.

Now for the majority of the databinding "goo"...  First we need to add a datasource to our project.  Normally, this is accomplished by having the Windows Forms designer open and then choosing "Data...Add New Data Source..." from the main menu, however, since this is a WPF application, this option appears disabled.  To enable it, open the Server Explorer window.  You should then be able to choose the "Add New Data Source" option from the "Data" menu.

For this sample, we will add a the Northwind database as our data source and choose the "Customers" and "Orders" tables.  This will add a strongly typed data set to your project as well as a bunch of other supportive classes (like table adapters for each of the chosen tables).

Next let's go the the code behind for the Window1 class (Window1.xaml.cs) and add the following to the class definition:

         private BindingSource nwBindingSource;
        private NorthwindDataSet nwDataSet;
        private NorthwindDataSetTableAdapters.CustomersTableAdapter customersTableAdapter = new WPFWithWFAndDatabinding.NorthwindDataSetTableAdapters.CustomersTableAdapter();
        private NorthwindDataSetTableAdapters.OrdersTableAdapter ordersTableAdapter = new WPFWithWFAndDatabinding.NorthwindDataSetTableAdapters.OrdersTableAdapter();

This will create a BindingSource object for us that we will use to bind the WPF and the Windows Forms controls to.  It will also create instances of our table adapter classes and define our data set.

In the Window1 class constructor add the following code just below the call to InitializeComponent():

             this.nwDataSet = new NorthwindDataSet();
            this.nwBindingSource = new BindingSource();
             this.nwDataSet.DataSetName = "nwDataSet";
             this.nwBindingSource.DataMember = "Customers";
            this.nwBindingSource.DataSource = this.nwDataSet;
             this.customersTableAdapter.ClearBeforeFill = true;

This will create instance of our data set and our binding source as well as wire up our binding source to the Customers table.  Next we need to add the following logic to our WindowLoaded method:

             this.customersTableAdapter.Fill(this.nwDataSet.Customers);
            this.ordersTableAdapter.Fill(this.nwDataSet.Orders);
             this.mainGrid.DataContext = this.nwBindingSource;
            this.listBox1.ItemsSource = this.nwBindingSource;
             // Since we are creating a master/details form, we need to point the DataGridView
            // to the foreign key between the tables.
            this.dataGridView1.DataSource = this.nwBindingSource;
            this.dataGridView1.DataMember = "FK_Orders_Customers";
             // Here is the tricky part.  Due to current limitations, you have to give a little help
            // to the currency management aspect of the data models.
             // We need to create an event handler that will fire whenever the current item is changed
            // via the WPF ListBox and then force it to be in sync with the BindingSource.
             BindingListCollectionView cv = (BindingListCollectionView)CollectionViewSource.GetDefaultView(this.nwBindingSource);
            cv.CurrentChanged += new EventHandler(WPF_CurrentChanged);

This will populate our data set with the Customers and Orders tables.  In addition, it will set the DataContext of the Grid element in our XAML to point to the BindingSource.  This means that the BindingSource will be used for all controls that are downstream of the Grid tag.  We also set the ItemsSource property of the ListBox to the BindingSource object as well.

Next we set the DataSource property of the DataGridView to the same BindingSource object, but for the DataMember, we set it to the foreign key between the Customers and Orders tables because we are creating a "master/details" view.

As the comments indicate, there is something else we need to do here to make this work.  Currently, there is an issue with currency not being properly managed across the WPF and Windows Forms boundaries.  Therefore, we have to give it just a bit of help here.  What we need to do is grab a pointer to the BindingListCollectionView and hook its CurrentChanged event so that we can keep the two in sync.  Now let's add the WPF_CurrentChanged handler:

         void WPF_CurrentChanged(object sender, EventArgs e)
        {
            BindingListCollectionView cv = sender as BindingListCollectionView;
            this.nwBindingSource.Position = cv.CurrentPosition;
        }

Now what we have done here is to say that whenever the current item changes via the WPF controls (in this case our ListBox) we want to force the BindingSource to point to the same item so that any WindowsForms controls also bound to the BindingSource will be in sync.  Note that we plan to fix this in later releases so that you don't have to perform this step manually.

Okay, let's run it and see what happens...

Note that as we pick a different customer in the WPF ListBox that the TextBox controls update as well as the WindowsForms-based DataGridView.

Now wasn't that FUN!  If you want to see the complete sample, just go HERE.

Comments