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.
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.
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:
- Add text column, used for adding the text columns to the grid (first and last name columns)
- Add dynamic column, used for adding new roles to the grid
- Change dynamic column, used for updating the role column (role name change)
- 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.
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.
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.
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.
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.
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.