WPF Data Grid with extensible columns

Almost another year has passed again. And here’s my next post. I’m sure I didn’t start this blog thinking that it would be an annual thing. But it sure seems like that’s what it is now. I just didn’t have enough time in the day or a week to commit. Actually that’s a lie that I tell my self to justify the delay, but it’s not, I had plenty of time, just that procrastination is my worst enemy.

When working with WPF, lot of people just adopt the MVVM pattern in making above average scale applications. Mainly with the support the Prism platform gives in order to build MVVM applications using WPF, and the many support and forums are on using that, it’s a no brainer that lot of WPF people turn to that pattern. If I say a few words on Prism briefly, it has been a simple yet effective framework to be used in WPF applications, to adopt the MVVM architecture in building extensible loosely coupled applications which are easy to build up and easy to maintain.

This blog post hopes to shed some light on one of the issues that I’ve faced during the development with WPF and Prism. Actually, it’s a problem that I’ve faced with WPF, and how I used Prism to get out of that hole. There’s quite a number of problems and solutions that I’ve thought about sharing through this space, if time and motivation permits, all will be shared in due time. Also I will assume that whoever actually stumbles upon this in their searches already has some idea about Prism and MVVM pattern, because unfortunately I will not delve much in to those explanations here. But I will try to make it all self explanatory as much as possible. So please bear with me.

The Problem

In the project I was working on there came a requirement to build a dashboard in the form of a datagrid. My immediate choice of datagrid was to use the xceed data grid community edition, because it was used elsewhere in the same application, and so I have gained the skills to apply it and style it the way I want. In this data grid, as the standard data grid is, we define the various columns in the xaml it self and define the bindings to those columns. Our project used many other modules where they insert their components in to the existing backbone using Prism MVVM pattern. So naturally I thought it would be awesome to do that with the dashboard too, giving any module to insert it’s columns to the data grid. But obviously I won’t be able to use the default prism regions to implement this behavior. So a custom region adapter was required. But when I was implementing this, the biggest problem I faced was communicating the data context of each row to the cells in the column.

When I implemented the custom region adapter for the columns collection, I’m able to pass in the whole data grids data context to the column, but that doesn’t help me when defining the bindings of the individual cells, as those cells should need the ability to access the data context of the row it’s in. So I came up with a solution to solve that multidimensional problem.

I will go through each step as best I can. This will be a memory exercise for me too, as it’s been a while since I tackled this problem.

Xceed DataGrid

There are many capabilities of this datagrid. Usually I prefer using default stuff provided by whatever technology I’m using. But I went ahead and used this because it was already used in the application, and styles has already been defined for it so it would look uniform when I use it for my dashboard.

Defining columns for the datagrid is pretty straightforward, as it is for the default WPF datagrid.

Each column can be bound to properties in the DataGridCOntrol data context. But for my purposes, I desired to have separate view models for each column so they would be separate from the base DataGridControl. So The columns would not be bound straight to the Data Grid control’s data context, rather they would be bound to a separate view model using the (Column.CellContentTemplate) property. Each of the separate Columns look like this :

<xcdg:UnboundColumn  
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:xcdg="http://schemas.xceed.com/wpf/xaml/datagrid"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             FieldName="Location" Title="Location"
             ReadOnly="True"
             Width="100"
             MaxWidth="210">
    <xcdg:UnboundColumn.CellContentTemplate>
        <DataTemplate>
            <StackPanel Loaded="Cell_Loaded" Style="{StaticResource UpToDateStyle}" Unloaded="Cell_Unloaded">
                <TextBlock Text="{Binding Location}"/>
            </StackPanel>
        </DataTemplate>
    </xcdg:UnboundColumn.CellContentTemplate>
</xcdg:UnboundColumn>
 

The columns are Unbound Columns. Each column has a cellContentTemplate defined and we bind data to the dataContext of the element inside the template. And the data context is fully taken care by the View Model we bind the column to.

In order to do this, we should have the ability to make the column collection a Prism region, so that we can have separate Column user controls backed by it’s own view models, and we can insert those Columns to the Prism region we define. Other modules we add to the project can then also define their own columns, and add to the column region. That’s basically the gist of what I tried to achieve.

Column Region Adapter

We can’t use the default Prism Collection Region Adapter for the Columns collection, because the Columns are not inheriting from Items Control. So a custom region adapter was required. At this point I haven’t build a custom region adapter, so it’s another opportunity to build one and master that area in Prism too. So it was a welcome requirement. I implemented it as an All Active Region.

public class DataGridControlRegionAdapter : RegionAdapterBase
{
    public DataGridControlRegionAdapter(IRegionBehaviorFactory regionBahaviorFactory) : base(regionBahaviorFactory){}

    protected override void Adapt(IRegion region, DataGridControl regionTarget)
    {
        if (region == null) throw new ArgumentNullException("region");
        if (regionTarget == null) throw new ArgumentNullException("regionTarget");
        region.Views.CollectionChanged += (s, e) =>
        {
            if (e.Action == NotifyCollectionChangedAction.Add)
            {
                foreach (ColumnBase col in e.NewItems)
                {
                    var columns = col as ColumnBase;
                    if (columns != null)
                        regionTarget.Columns.Add(columns);
                }
            }
            if (e.Action == NotifyCollectionChangedAction.Remove)
            {
                 foreach (ColumnBase col in e.OldItems)
                 {
                     var columns = col as ColumnBase;
                     if (columns != null && 
                         regionTarget.Columns.Any(co => co.Title == columns.Title))
                             regionTarget.Columns.Remove(columns);
                 }
            }
        };
    }
    protected override IRegion CreateRegion()
    {
        return new AllActiveRegion();
    }
}

Then in DataGridControl we can just insert the region like this :

prism:RegionManager.RegionName="{x:Static mod:ModuleRegionNames.GeneralDashboardColumnRegion}"
prism:RegionManager.RegionContext="{Binding}"

Data Context Binding

A column view model helper class takes care of the data binding and getting the correct model to our views. We have to use that because Prism wouldn’t do that for us in this case as it normally would. Each datacell view model inherits from a DataCellViewModelBase. and each column view model is typed according to column type.

public class ColumnViewModelHelper where T : DataCellViewModelBase
{
    private readonly IUnityContainer _container;
    public ColumnViewModelHelper(IUnityContainer container)
    {
        _container = container;
    }
    public T SetupDataContext(FrameworkElement cellContent)
    {
        var templatedFE = cellContent.TemplatedParent as FrameworkElement;
        var dataGridDataCell = templatedFE.Parent as FrameworkElement;
        var parent = dataGridDataCell.Parent as FrameworkElement;
        var cellDataContext = dataGridDataCell.DataContext;
        if (!(cellDataContext is T))
        {
            var dataRowViewModel = cellDataContext as DashboardEntityViewModel;
            var viewModel =
                _container.Resolve(new ParameterOverride("device,
                                                        dataRowViewModel.Device));
            viewModel.SetRowModel(dataRowViewModel);
            dataGridDataCell.DataContext = viewModel;
            cellContent.DataContext = viewModel;
            return viewModel;
        }
        else
        {
        if (cellContent.DataContext == null)
        {
            cellContent.DataContext = cellDataContext;
            var dataRowViewModel = parent.DataContext as DashboardEntityViewModel;
            // if current row context doesn't match new dataContext, replace
            // dataContext
        if (dataRowViewModel.Device.Id != (cellDataContext as T).RowModel.Device.Id)
            {
                var viewModel = _container.Resolve(new ParameterOverride("device",
                                                        dataRowViewModelDevice));
                viewModel.SetRowModel(dataRowViewModel);
                dataGridDataCell.DataContext = viewModel;
                cellContent.DataContext = viewModel;
                return viewModel;
            }
        }
        return cellDataContext as T;
    }
}

So in the column control cs code, we implement the Loaded method, which calls the view model’s ViewLoaded method as usually we do in MVVM. We pass in the element to the view loaded which gets passed in to the Column model helper’s SetupDataContext method. There we extract the cell data context. If the cell data context is not bound before, we bind it to the custom cell data context we create. If it’s not already bound to the dataCellViewModel subtype, then that means it’s bound to the element corresponding to that row, where that cell is. So we get the row data context from that. Using that row data context we resolve it to a cell data context corresponding to the the column type, using unity container. Each of the cell view models has a reference to the row view model too. So we set that up. Then we replace the cell’s data context with the cell view model we just resolved.

In some instances the cellContent data context would become null, then we have to reset it to the cell data context anyway. In these cases the row view model which the cell belonged to might’ve changed, So we check if that has happened by comparing the current cell data context’s row model with the parent data context row view model. If this is not the same we have to reset the cell data context to the parent row view models data.

This process ensures that each cell will have it’s own view model which takes care of it’s data binding. We take care of potential replacement of data contexts which the datagrid control might do in the SetupDataContext method. So far this has not failed me in having a extensible data grid control where with specific control on each cell. Possibilities are endless for this because we can basically have anything in the cell with cell content templates and they can be bound to anything we want too, using the method described here. So hope this helps someone and hope somebody would be able to extend this to even more capabilities.

Advertisements
This entry was posted in .Net, Design Patterns and tagged , , , , , , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s