Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

Dynamic Columns in a WPF DataGrid Control (Part 2)

4.83/5 (8 votes)
12 Nov 2015CPOL5 min read 27.5K   1K  
Dynamic columns in a WPF DataGrid Control (part 2), correcting an architectural constraint using the interaction request framework

Introduction

This article extends the first "Dynamic Columns in a WPF DataGrid Control (Part 1)" article. In this article, I describe how to add dynamic columns to a WPF DataGrid control. This second part focuses on an architectural constraint which I  violated in part 1. The solution is based on my previous article "ViewModel to View Interaction Request". It will use functionality from the "small application framework" which is already introduced in that article.

The next article "MVVM Data Validation" extends the application framework with a base class for data validation and shows how the values that are entered into the data grid can be validated against data rules.

Background

As mentioned in the introduction, this article is about fixing an architectural constraint. So what is broken that has to be fixed? In the original solution, I placed the dynamic columns collection in the MainViewModel to minimize code complexity.

Because of this architectural decision, I had to reference the PresentationFramework library in the ViewModel assembly. This means that a part of the user interface layer crept into the view-model layer. This is a breach of the layered architecture pattern.

Using the Code

The Code Changes

The next class diagram shows the old situation in which the view model has an observable collection containing data grid columns. The data grid column collection has been removed in the new situation. The library references have been cleaned and the PresentationCore, PresentationFramework, System.Xaml and WindowsBase references have been removed.

Image 1

The ObjectTag class has also moved to the GUI layer. This class is used to create a dependency property called 'Tag'. This property allows instances (in this case, the role row) to be tagged to the corresponding grid column.

Small Application Framework

This library is introduced in my article about ViewModel to View Interaction Requests. It describes a similar situation in which logic in the view model layer has to be decoupled from the GUI layer. The previous article described how to deal with file open and save message boxes, and here I use the same mechanism to manage the dynamic columns in the data grid. I recommend you read this article first if you are not familiar with it.

The Data Model

The data model has stayed the same.

Image 2

The View Model

In the new system, the columns are managed by the grid control. The mutation of the columns is requested by the view model via notifications to the GUI layer. There are four notification types:

  1. Add text column, used for adding the text columns to the grid (first and last name columns)
  2. Add dynamic column, used for adding new roles to the grid
  3. Change dynamic column, used for updating the role column (role name change)
  4. Delete dynamic column, used for removing the role from the grid

The DataColumnService class contains the interaction request classes that are used to pass the notification from the logic to the GUI layer.

Image 3

The MainViewModel contains the database context and the logic to initialize the grid. It subscribes to the DataSet's data events and passes the data operations events (add, remove and modify roles) on to the GUI layer, using the interaction request notifications.

The Application

The MainWindow in the Application layer has the same  functionality and implementation as the previous version. The difference to the previous solution is that the attached behavior DataGridColumnsBehavior was removed. This class was responsible for the monitoring of the roles data view, and the update of the grid when the roles changed.

Image 4

The new version uses Triggers from the Interaction library to get the problems solved. There are four triggers: One for inserting a text column and three for inserting, removing and modifying the role columns. Each trigger calls an action implementation, that performs the actual task. All actions are associated to the main window. Each action has access to the main window and its child controls, so it can modify the grid's column collection.

An additional trigger is an event trigger that subscribes to the window's load event and calls the OnLoad method of the view model. This call is used for the initialization of the grid columns.

The Business Logic

Application Initialization

The application is initialized by routing the Loaded event to the view model, calling the OnLoaded method. The first task is to initialize the standard grid columns, the first and last name columns. The second task is adding the existing role columns.

C#
public void OnLoaded()
{
    this.GenerateDefaultColumns();
    this.InitializeRolesColumns();
}

private void GenerateDefaultColumns()
{
    this.AddTextColumn("First Name", "FirstName");
    this.AddTextColumn("Last Name", "LastName");
}

private void InitializeRolesColumns()
{
    foreach (var role in this.databaseContext.DataSet.Role)
    {
        this.AddRoleColumn(role);
    }
}

private void AddTextColumn(string header, string binding)
{
    var addTextColumnNotification = new AddTextColumnNotification
    {
        Header = header,
        Binding = binding
    };
    DataColumnService.Instance.AddTextColumn.Raise(addTextColumnNotification);
}

private void AddRoleColumn(UserRoleDataSet.RoleRow role)
{
    var notification = new AddDynamicColumnNotification { Role = role };
    DataColumnService.Instance.AddDynamicColumn.Raise(notification);
}

The interaction request framework takes care of the execution of the AddTextColumnAction instance for the insertion of the first and last name columns.

C#
public class AddTextColumnAction : TriggerActionBase<AddTextColumnNotification>
{
    protected override void ExecuteAction()
    {
        var mainWindow = this.AssociatedObject as MainWindow;
        if (mainWindow != null)
        {
            mainWindow.DataGridUsers.Columns.Add(
                new DataGridTextColumn
                    {
                        Header = this.Notification.Header,
                        Binding = new Binding(this.Notification.Binding)
                    });
        }
    }
}

Insertion of a Role Column

A new role column is added in the same way as a text column. The framework calls the AddDynamicColumnAction class that creates the column, and assigns the binding. The binding consists of:

  • Converter, a value converter. It handles the assignment of the roles to a user
  • RelativeSource, is the DataGridCell containing the checkbox control
  • Path, path to the columns bound object
  • Mode, two way binding for updates to and from the grid cell control

The column is a DataGridCheckBoxColumn which is created with the header from the role, binding, and the element style. The element style prevents the display of the checkbox when the control is a new item row (see part one).

Finally, the column is added to the grid control's column list.

C#
public class AddDynamicColumnAction : TriggerActionBase<AddDynamicColumnNotification>
{
    protected override void ExecuteAction()
    {
        var resourceDictionary = 
        ResourceDictionaryResolver.GetResourceDictionary("Styles.xaml");
        var userRoleValueConverter = 
        resourceDictionary["UserRoleValueConverter"] as IValueConverter;
        var checkBoxColumnStyle = resourceDictionary["CheckBoxColumnStyle"] as Style;
        var binding = new Binding
                          {
                              Converter = userRoleValueConverter,
                              RelativeSource =
                                  new RelativeSource
                                  (RelativeSourceMode.FindAncestor, typeof(DataGridCell), 1),
                              Path = new PropertyPath("."),
                              Mode = BindingMode.TwoWay
                          };
        var dataGridCheckBoxColumn = new DataGridCheckBoxColumn
                                         {
                                             Header = this.Notification.Role.Name,
                                             Binding = binding,
                                             IsThreeState = false,
                                             CanUserSort = false,
                                             ElementStyle = checkBoxColumnStyle,
                                         };
        ObjectTag.SetTag(dataGridCheckBoxColumn, this.Notification.Role);
        var mainWindow = this.AssociatedObject as MainWindow;
        if (mainWindow != null)
        {
            mainWindow.DataGridUsers.Columns.Add(dataGridCheckBoxColumn);
        }
    }
}

Role Assignment

The role assignment is done in the UserRoleValueConverter. The logic is the same and is explained in the previous article.

Points of Interest

One of the first reactions I usually get is: "Why bother? The old solution works, so let it be". I understand that this article has a huge software evangelism potential. But I think that an architecture with clean layers is more manageable. It is worth the extra mile that one has to go for it.

Acknowledgements

I would like to thank my colleague and friend Thomas Britschgi for his inputs and the review of the article.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)