Blogs

WPF

February 2009 - Posts

  • DevExpress WPF Grid Control - Editing Data

         

    Prompted by a forum post, I decided to have a look at how inplace editing works in the DXGrid for WPF. Good results – it's really easy.

    The first interesting thing to realize is that editing is actually active by default in the grid. Nevertheless, the end user can't edit anything unless one setting is changed: the property NavigationStyle on the View must be changed to CellNavigation (by default it's RowNavigation). As soon as this setting is changed, editing becomes possible, since both the AllowEditing on the GridControl itself and the GridColumnView instance are True by default.

    Here's my minimum XAML snippet that results in an editable grid. I'm using the same country datasource that I was using in my first DXGrid blog post.

    <Window x:Class="DxGridEditing.Window1"

       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

       xmlns:local="clr-namespace:DxGridEditing"

       Title="Window1" Height="342" Width="534" xmlns:dxg="http://schemas.devexpress.com/winfx/2008/xaml/grid" xmlns:dxe="http://schemas.devexpress.com/winfx/2008/xaml/editors">

        <Window.Resources>

            <local:CountryInfoList x:Key="list" />

        </Window.Resources>

        <DockPanel>

            <dxg:GridControl Name="gridControl1" AutoPopulateColumns="True" DataSource="{StaticResource list}">

                <dxg:GridControl.View>

                    <dxg:GridColumnView NavigationStyle="CellNavigation" />

                </dxg:GridControl.View>

            </dxg:GridControl>

        </DockPanel>

    </Window>

    The default being what it is, the question is obviously how to go about changing that. The first thing I want to do is switch off editing for one or more columns. In my code, I've been using automatic column creation so far, and I have to switch over to pre-configured columns in order to do this. Easy enough to do – get rid of the AutoPopulateColumns attribute on the GridControl and add a few GridColumn instances instead (bring up the GUI editor for the GridControl.Columns property if you don't like to do the typing).

    <dxg:GridControl Name="gridControl1" DataSource="{StaticResource list}">

        <dxg:GridControl.Columns>

            <dxg:GridColumn FieldName="Name" />

            <dxg:GridColumn FieldName="AreaKM2" />

            <dxg:GridColumn FieldName="Population" />

        </dxg:GridControl.Columns>

        <dxg:GridControl.View>

            <dxg:GridColumnView NavigationStyle="CellNavigation" />

        </dxg:GridControl.View>

    </dxg:GridControl>

    From this point, it's easy to configure per-column settings, and that includes whether or not a certain column can actually be edited. The AllowEditing settings on the GridControl and the GridColumnView act as defaults, but each column has the final word when a decision about editing must be made.

    There are two settings that come in handy. First, the column also has an AllowEditing property, and it's default value is Default, meaning it takes its value from its parent. Setting this to False (assuming the parent default is True) disables editing for this column entirely. As an alternative, there's also a ReadOnly property, which I personally find very useful – in a situation where some grid columns are editable, it's often sensitive to allow the user access to the values in the other, non-editable columns, for copying to the clipboard. Leaving AllowEditing on its Default value, but setting ReadOnly to True has the result that the editor for a column is still available, but the value can't be changed.

    <dxg:GridControl.Columns>

        <dxg:GridColumn FieldName="Name" />

        <!-- Using both ReadOnly and AllowEditing at the same time

             doesn't really make too much sense, unless the idea is

             to override the parent default for AllowEditing. In

             this case I just left both in place for illustration

             purposes.

        -->

        <dxg:GridColumn FieldName="AreaKM2" ReadOnly="True" AllowEditing="False" />

        <dxg:GridColumn FieldName="Population" />

    </dxg:GridControl.Columns>

    The final thing I want to mention here is the configuration of the EditSettings property. The settings that are provided out of the box (TextEditSettings, CheckEditSettings, ComboBoxEditSettings and DateEditSettings) encapsulate the editor types that our grid supports at the time of writing. So it's important to be aware of this setting, because it defines which editor will be used.

    I was going to continue for a bit by mentioning a few of the customizations that can be made to the editing settings, but I stumbled upon a few issues that need research. I'll continue with more details a bit later.

    Here is the download of the sample project: DxGridEditing.zip

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

         

    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

  • Getting started with the DevExpress Grid Control for WPF - data binding 1

         

    I've started taking a more in-depth look at our WPF grid control, and I just thought I'd document some of the important steps I'm making for others to follow along. Please feel free to point me towards things that don't seem that easy to do - that's what I'm most interested in!

    For a start, I'm just going to drop a grid control on a form and try to hook it up to data. Since this is WPF, there are of course numerous options for this. For my first test, I have a simple static collection of data in my application, which consists of objects with data about countries. The collection derives from a List<T>.

    On my form, I've got a GridControl inside a DockPanel. I changed the standard layout panel (Grid) to a DockPanel, but otherwise I've left everything as it was after dropping the GridControl on the form. Here's the XAML code:

    <Window x:Class="DxGrid1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Window1" Height="342" Width="534" xmlns:dxg="http://schemas.devexpress.com/winfx/2008/xaml/grid">
        <DockPanel>
            <dxg:GridControl Name="gridControl1">
                <dxg:GridControl.Columns>
                    <dxg:GridColumn FieldName="Column1" />
                    <dxg:GridColumn FieldName="Column2" />
                </dxg:GridControl.Columns>
                <dxg:GridControl.View>
                    <dxg:GridColumnView />
                </dxg:GridControl.View>
            </dxg:GridControl>
        </DockPanel>
    </Window>

    Now I would like to hook up the grid to my data source. I've got a class called CountryInfoList which just needs to be instantiated. An easy way to do this is of course to create a resource section entry, so I add a namespace line to my Window instantiation:

    xmlns:local="clr-namespace:DxGrid1"

    Then I configure the DataSource property of the grid to bind to the list instance from the resources:

    DataSource="{StaticResource list}"

    I also set the AutoPopulateColumns property to True and I remove the existing two empty columns. This last step is important - if the current column definition collides with the columns that are actually in the data source, the grid currently (that's on the 8.3 version I'm using) throws an exception. (I've registered a bug for this here.)

    At this point the data from my list is already visible in the designer in Visual Studio. My complete XAML looks like this now:

    <Window x:Class="DxGrid1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:DxGrid1"
        Title="Window1" Height="342" Width="534" xmlns:dxg="http://schemas.devexpress.com/winfx/2008/xaml/grid">
        <Window.Resources>
            <local:CountryInfoList x:Key="list" />
        </Window.Resources>
        <DockPanel>
            <dxg:GridControl Name="gridControl1" AutoPopulateColumns="True" DataSource="{StaticResource list}">
                <dxg:GridControl.View>
                    <dxg:GridColumnView />
                </dxg:GridControl.View>
            </dxg:GridControl>
        </DockPanel>
    </Window>

    As you see, a hookup to a simple object data source is very easy to do. Of course I could achieve the same result by setting the DataSource property from code - this would allow more flexibility regarding the actual source of my data, since the collection could be constructed more dynamically than the WPF resource system allows. A broad feature set is already supported at this point - sorting, column configuration, grouping.

    Update: Here's the download of the source code for my project.

More from DevExpress
Live Chat
Have a pre-sales question?
Need assistance with your evaluation?
We are here to help.
Chat is one of the many ways you can contact members of the DevExpress Team. We are available Monday-Friday between 8:30am and 5:00pm Pacific Time.
If you need additional product information, require pre-sales assistance, or want help with your order, write to us at info@devexpress.com or call us at
+1 (818) 844-3383.