Getting started with the DevExpress datagrid for WPF - XPO data binding

05 February 2009

This time I'm trying to use the DXGrid for WPF with XPO. There are at least two different approaches I'm going to try, and I found a few small issues on the way as well.

In my demo project (see the download at the bottom) I have created a few persistent classes and a routine to create some test data (in SQL Server by default). As it turned out, I didn't use more than the Customer and Order classes in the grids, but I left the rest in place anyway. A Customer has a Name and a collection of Orders. An Order has a Date and a collection of OrderLines (but the OrderLines are irrelevant here).

Generally, hooking up a WPF grid to an XPCollection<T> is easy. I create the following grid in my window:

<dxg:GridControl Name="customerGrid" AutoPopulateColumns="True">
  <dxg:GridControl.View>
    <dxg:GridColumnView />
  </dxg:GridControl.View>
</dxg:GridControl>

Then, in code, I add this :

UnitOfWork uow;

protected override void OnInitialized(EventArgs e) {
  base.OnInitialized(e);

  uow = new UnitOfWork();
  customerGrid.DataSource = new XPCollection<Customer>(uow);
}

That's it, for a start - running the application at this point results in a grid showing me Customer objects. Now, what I want to do is add another grid that shows the related orders for the customer selected in the first grid. Easy enough, one might think - with standard WPF controls, I would typically bind the second control like this: ItemsSource="{Binding ElementName=customerGrid,Path=SelectedItem.Orders}". (Did I get that right? Just wrote it down from the top of my head - it's not really important.)

Not quite so easy with the DXGrid, or at least it doesn't look that easy at first glance. As Thomas Grusche commented on my previous post, the DXGrid doesn't derive from ItemsControl, so the binding will have to look a little different. The property to bind to is DataSource, and to get hold of the selected object in the grid, the path must be this: View.CurrentRowData.DataContext

Cool, so my first try looked like this: DataSource="{Binding ElementName=customerGrid,Path=View.CurrentRowData.DataContext.Orders}". It didn't work - the second grid didn't show anything. Some debugging and fiddling around, and it turns out that the object returned by DataContext isn't the actual object from my source collection at all. It is an instance of a class called RowTypeDescriptor, which looks deceivingly like the original object, but is in fact an artificial thing that just looks like it has the same properties. Certain properties are missing, and IList implementations like my collection of related objects is among those. Now what?

There are several possible solutions to this issue. One of them is writing a bit of code in the form of a handler for the CurrentRowChanged event. But I'm going to explore that approach below, so let's focus on the other technique: take advantage of the property This in the XPO base class XPBaseObject, which returns a reference to the object instance. This property gets included in the RowTypeDescriptor, so I can use it to get back to the actual instance of Customer. My working order grid now uses this XAML code:

<dxg:GridControl Name="orderGrid" AutoPopulateColumns="True"
  DataSource="{Binding ElementName=customerGrid,Path=View.CurrentRowData.DataContext.This.Orders}">
  <dxg:GridControl.View>
    <dxg:GridColumnView />
  </dxg:GridControl.View>
</dxg:GridControl>

So far, so good. Now, the DXGrid also supports our fantastic server mode for data binding to XPO. (If you're not familiar with it, it means that the grid queries only the data from the data source that is actually needed in its current view configuration, depending on sorting, filtering, grouping, scrolling, ... very nice feature supported also by our WinForms and ASP.NET datagrids.) This uses a different type of datasource, called XPServerCollectionSource.

Hooking up my customer grid with this datasource is easy. Just change the line that sets the DataSource property to this:

customerGrid.DataSource = new XPServerCollectionSource(uow, typeof(Customer));

Yep, that's all. But there's a problem with the master/detail setup: if the detail grid binds to the Orders collection from the Customer object, the server-mode feature is no longer being used and the entire collection will still be retrieved. Not good. So I'll have to make sure that the binding for the detail grid uses an XPServerCollectionSource as well. This is a use case for the CurrentRowChanged event, as mentioned above. I change my customer grid to this:

<dxg:GridControl Name="customerGrid" AutoPopulateColumns="True">
  <dxg:GridControl.View>
    <dxg:GridColumnView CurrentRowChanged="GridColumnView_CurrentRowChanged" />
  </dxg:GridControl.View>
</dxg:GridControl>

And this is the event handler:

private void GridColumnView_CurrentRowChanged(object sender,
  DevExpress.Wpf.Grid.CurrentRowChangedEventArgs e) {
  GridColumnView view = (GridColumnView) customerGrid.View;

  var selectedCustomer = (Customer) view.GetRowValue(
    (view.CurrentRowData.DataContext as RowTypeDescriptor).RowHandle.Value);
  orderGrid.DataSource = new XPServerCollectionSource(
    uow, typeof(Order), new BinaryOperator("Customer", selectedCustomer));
}

Right, this works. Btw, my demo also includes a little panel that shows the instructions XPO sends to the database - so you can see that it really doesn't request the whole Orders collection at once anymore.

Finally, there's a way to automate the process of creating an XPServerCollectionSource when the master selection changes. It uses the ability of WPF to integrate a value converter into the data binding process. This converter takes the original collection and creates a corresponding XPServerCollectionSource. Here it is:

public class CollectionConverter : IValueConverter {
  object IValueConverter.Convert(object value, Type targetType, object parameter,
    System.Globalization.CultureInfo culture) {
    if (value == null)
      return null;
    var sourceColl = (XPBaseCollection) value;
    return new XPServerCollectionSource(sourceColl.Session,
      sourceColl.GetObjectClassInfo( ).ClassType, sourceColl.Criteria);
  }

 
  object IValueConverter.ConvertBack(object value, Type targetType, object parameter,
    System.Globalization.CultureInfo culture) {
      throw new Exception("The method or operation is not implemented.");
  }
}

And here's the XAML code for this automated binding:

<Page x:Class="DxGrid2.XPServerCollectionSource2Page"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="XPServerCollectionSource2" Height="600" Width="800"
  xmlns:dxg="http://schemas.devexpress.com/winfx/2008/xaml/grid"
  xmlns:local="clr-namespace:DxGrid2"
  >
  <Page.Resources>
    <local:CollectionConverter x:Key="collectionConverter" />
  </Page.Resources>
  <DockPanel>
    <StackPanel Orientation="Horizontal">
      <dxg:GridControl Name="customerGrid" AutoPopulateColumns="True">
        <dxg:GridControl.View>
          <dxg:GridColumnView />
        </dxg:GridControl.View>
      </dxg:GridControl>
      <dxg:GridControl Name="orderGrid" AutoPopulateColumns="True"
        DataSource="{Binding ElementName=customerGrid,Path=View.CurrentRowData.DataContext.This.Orders,Converter={StaticResource collectionConverter}}">
        <dxg:GridControl.View>
          <dxg:GridColumnView />
        </dxg:GridControl.View>
      </dxg:GridControl>
    </StackPanel>
  </DockPanel>
</Page>

There are several steps necessary to make this possible:

  1. Include the xmlns entry in the Page tag to make the namespace of the application available in XAML.
  2. Create an instance of the CollectionConverter in the page's Resources section.
  3. Add the Converter to the Binding of the orderGrid, referencing the resource.

As a result, there's no event handler needed anymore. Declarative binding, like it should be in WPF.

I'm sure that over time we'll make this whole process a bit easier in certain places. We could include IList type properties in the RowTypeDescriptor, or we could find another, more general way of making the original object from the source collection available declaratively. We could automate the server-mode support better, and of course implement master/detail support in the grid internally. Eventually I guess all these things are going to get done, but meanwhile the techniques outlined above provide the functionality quite cleanly.

Here's the download of the demo application: DxGrid2.zip

1 comment(s)
Jason Richards

I cant get this to work, not the latter, just the simple

DataSource="{Binding ElementName=customerGrid,Path=View.CurrentRowData.DataContext.This.Orders}"

having said that i am using a master datasource xpcollection(typeof(x)) format but this should make no difference

15 April, 2010

Please login or register to post comments.