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 1)

4.97/5 (12 votes)
7 Aug 2015CPOL9 min read 101K   3.7K  
This article describes the dynamic insertion and removal of columns in a WPF datagrid.

Introduction

Datagrid controls are great for displaying data that is stored in a table. One row in the database table equals one row in the datagrid. When data is stored in multiple tables, say table A and B, and the row A has a one-to-many (also called 1:N, parent-child, or master-detail) relation to a row in table B, then row A can reference multiple rows in table B. This type of data can be shown in a master-detail data view type. Another type of data relation is the type many-to-many (also called N:M relation). The row of table A can have multiple references to rows in table B. But in addition to the previous case, a row of table B can be referenced by multiple rows of Table A.

This article describes a method many-to-many relations can be displayed and modified in a WPF datagrid control. The rows and columns can be added, removed and modified by editing the rows of the A and/or the B table.

This article is split in two parts. In this first part, I focus on the solution of handling dynamic columns. In order to simplify the solution, I broke an architectural constraint, which is that objects of a top layer should not be used in lower layers (in this case, the grid columns that are a part of the GUI layer, and not the view model layer). The second part of this article fixes this constraint.

Using the Code

The Application

The sample code implements a user administration form in which users, roles and the user-role assignment can be administered. The roles and the users are displayed in two data grids. The user role assignment is done in the user data grid. Therefore this grid has the dynamic contents, displaying each role as a separate check box column. The user-role assignment is done by checking the respective check box.

Image 1

The Data Model

The data model of this sample consists of a User and a Role table, and a UserRole table that is the correlation table between the other two tables. An entry in the UserRole table means that the user (referenced by its user id) has a role assigned (referenced by the role's id). If there is no entry for a certain user-role combination, then that means that the user in question does not have the corresponding role assigned.

Image 2

The data model is implemented using the .NET DataSet. It is a good in-memory database with referential integrity, and it contains built-in notification delegates that publish the insertion, removal and modification of data rows. Its contents can be stored to an XML file, which is used as a persistence mechanism in this example.

The Component and Class Diagram

The next component diagram shows the application's layering:

  • Application: contains the GUI elements
  • ViewModel: contains the business logic
  • DataModel: contains the data definition, and persistency

Image 3

Application

  • MainWindow: the GUI definition, written in XAML
  • DataGridColumnsBehavior: an attached behavior that allows the modification of the columns of the attached DataGrid control.
  • UserRoleValueConverter: The value converter implementation that defines what happens, when the user checks or unchecks the checkbox

ViewModel

  • MainViewModel: contains the display data table properties for the view and the data logic for the dynamic column handling
  • ColumnTag: attached property for tagging objects to instances that derive from DependencyObject, in this case DataGridColumn

DataModel

  • DatabaseContext: singleton instance that contains the UserRoleDataSet
  • UserRoleDataSet: the database implementation, based on DataSet

Implementation

Data Binding

The application is written using the MVVM design pattern. This means that the main window is bound to the main view model, and the view controls are bound to the main view model's properties.

Reference View Control Property ViewModel Property
1 MainWindow:DataGridRoles.ItemsSource MainViewModel.Roles
2

MainWindow:DataGridUsers.ItemsSource

MainViewModel.Users
3 MainWindow:DataGridUsers.Column MainViewModel.UserRoleColumns

Ad 1: Binds the database role table to the roles data grid control

Ad 2: Binds the database user table to the users data grid control

Ad 3: Binds the column's observable collection to the users grid control's columns property. The dynamic column behavior is achieved via this property, because the logic in the view model adds and removes the columns from and to this collection.

The data grid control's column property is declared as read-only, so it cannot be bound to a view model property. The DataGridColumnsBehavior is an attached behavior that overcomes this limitation. The original article and source can be found here.

XML
<Window x:Class="Application.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:attachedBehaviors="clr-namespace:Application.AttachedBehaviors"
        xmlns:viewModel="clr-namespace:ViewModel;assembly=ViewModel"
        Title="User Administration" Height="350" Width="525">

    <Window.DataContext>
        <viewModel:MainViewModel/>
    </Window.DataContext>

    <DockPanel LastChildFill="True">
        <ToolBar DockPanel.Dock="Top">
            <Button Content="Save" Command="{Binding SaveCommand}"/>
        </ToolBar>

        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="146*"/>
                <RowDefinition Height="147*"/>
            </Grid.RowDefinitions>

            <GroupBox x:Name="UsersGroupBox"
                      Grid.Column="0"
                      Header="User Role Assignment">
                <DataGrid x:Name="DataGridUsers"
                          ItemsSource="{Binding Users}"
                          attachedBehaviors:DataGridColumnsBehavior.BindableColumns=
                              "{Binding UserRoleColumns}"
                          AutoGenerateColumns="False"
                          EnableRowVirtualization="False"/>
            </GroupBox>

            <GroupBox x:Name="RolesGroupBox"
                      Grid.Row="1" Grid.Column="0"
                      Header="Roles">
                <DataGrid x:Name="DataGridRoles"
                          ItemsSource="{Binding Roles}"
                          AutoGenerateColumns="False">
                    <DataGrid.Columns>
                        <DataGridTextColumn Header="Name"
                                            Binding="{Binding Name}"/>
                    </DataGrid.Columns>
                </DataGrid>
            </GroupBox>
        </Grid>
    </DockPanel>
</Window>

Data Handling

The data is kept in three tables in the UserRoleDataSet (Role, User and UserRole). The Role and User tables are bound to the data grid controls via a DataView. The DataView allows the modification, insertion and removal of rows and the prevention of these actions. Filtering and sorting can be setup on the DataView as well. The data grid control can handle the data manipulation using the DataView. Rows can be inserted, modified and removed in the data grid control (there is a new item row at the bottom of the grid, and rows are be removed when the delete key is pressed) and the data tables are directly updated through the DataView.

C#
public class MainViewModel
{
    public MainViewModel()
    {
        --- Code omitted ---
        this.UserRoleColumns = new ObservableCollection<DataGridColumn>();
        --- Code omitted ---
    }

    public DataView Users
    {
        get
        {
            return this.dataContext.DataSet.User.DefaultView;
        }
    }

    public DataView Roles
    {
        get
        {
            return this.dataContext.DataSet.Role.DefaultView;
        }
    }

    public ObservableCollection<DataGridColumn> UserRoleColumns { get; private set; }
}

The DataSet can be used together with database connections to store and retrieve data from SQL servers, etcetera. In this application, I use the persistence mechanism to store to and retrieve data from an XML file.

Every DataSet table has a set of events that can be used to get notified on data modifications.. This mechanism is used to add, remove and update the dynamic columns when the role table is modified.

C#
public class MainViewModel
{
    public MainViewModel()
    {
        --- Code omitted ---
        this.dataContext = DatabaseContext.Instance;
        this.dataContext.DataSet.Role.RoleRowChanged += this.RoleOnRowChanged;
        this.dataContext.DataSet.Role.RoleRowDeleted += this.RoleOnRoleRowDeleted;
        --- Code omitted ---
    }

    private void RoleOnRowChanged(object sender,
                                  UserRoleDataSet.RoleRowChangeEvent roleRowChangeEvent)
    {
        switch (roleRowChangeEvent.Action)
        {
            case DataRowAction.Change:
                this.UpdateRoleColumn(roleRowChangeEvent.Row);
                break;
            case DataRowAction.Add:
                this.AddRoleColumn(roleRowChangeEvent.Row);
                break;
        }
    }

    private void RoleOnRoleRowDeleted(object sender,
                                      UserRoleDataSet.RoleRowChangeEvent roleRowChangeEvent)
    {
        if (roleRowChangeEvent.Action == DataRowAction.Delete)
        {
            this.DeleteRoleColumn(roleRowChangeEvent.Row);
        }
    }
}

Business Logic

Default Column Definition

The user data grid column definition is stored in the UserRolesColumns collection. This means that the default columns, the user's first and last name, have to be in this collection too. Two DataGridTextColumns are instantiated for the first and the last name, and the cell content are bound to the data row through the binding to the row's respective fields.

C#
public class MainViewModel
{
    public MainViewModel()
    {
        this.GenerateDefaultColumns();
        --- Code omitted ---
    }

    private void GenerateDefaultColumns()
    {
        this.UserRoleColumns.Add(new DataGridTextColumn
        {
            Header = "First Name", Binding = new Binding("FirstName")
        });
        this.UserRoleColumns.Add(new DataGridTextColumn
        {
            Header = "Last Name", Binding = new Binding("LastName")
        });
    }
}

Dynamic Column Definition

The dynamic column handling is separated into the 3 operation types:

  • AddRoleColumn: is called when a role is added to the Role table. It instantiates a new DataGridCheckBoxColumn, assigns the CheckBoxColumnStyle and the UserRoleValueConverter. The latter implements the user-role assignment logic (see below). The column is tagged with the role instance, so that the assignment logic can work. The column's header is set to the role name.
  • UpdateRoleColumn: is called when the contents of a role row is modified. The logic scans the dynamic column collection for the column that is tagged with the role instance that is modified. Once found, the column's header is updated with the role name. The binding mechanism automatically updates the column header in the data grid.
  • DeleteRole: is called when a role is removed from the Role table. The logic scans the dynamic column collection for the column that is tagged with the role instance that was deleted and removes the column.
C#
public class MainViewModel
{
    private void AddRoleColumn(UserRoleDataSet.RoleRow role)
    {
        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 = role.Name,
                                             Binding = binding,
                                             IsThreeState = false,
                                             CanUserSort = false,
                                             ElementStyle = checkBoxColumnStyle,
                                         };

        ObjectTag.SetTag(dataGridCheckBoxColumn, role);
        this.UserRoleColumns.Add(dataGridCheckBoxColumn);
    }

    private void UpdateRoleColumn(UserRoleDataSet.RoleRow role)
    {
        if (role != null)
        {
            foreach (var userRoleColumn in this.UserRoleColumns)
            {
                var roleScan = ColumnTag.GetTag(userRoleColumn) as UserRoleDataSet.RoleRow;
                if (roleScan == role)
                {
                    userRoleColumn.Header = role.Name;
                    break;
                }
            }
        }
    }

    private void DeleteRoleColumn(UserRoleDataSet.RoleRow role)
    {
        if (role != null)
        {
            foreach (var userRoleColumn in this.UserRoleColumns)
            {
                var roleScan = ColumnTag.GetTag(userRoleColumn) as UserRoleDataSet.RoleRow;
                if (roleScan == role)
                {
                    this.UserRoleColumns.Remove(userRoleColumn);
                    break;
                }
            }
        }
    }
}

User-Role Assignment

The DataGridCheckBoxColumn binds the check box control to a (nullable) boolean property of the data in the row that it is displaying. In this case, it would be a boolean property in the user data row, which represents the user to role assignment. Since there is no such property in the UserTable definition, another solution has to be implemented. Instead of binding to the check box control, a value converter is instantiated and bound to the DataGridCell that will contain the CheckBox control. The Binding definition in the AddRoleColumn method shown above contains an assignment to the value converter. The relative source of the bound control is set to the DataGridCell, found as an ancestor of the CheckBox control (the binding is defined on the CheckBox level).

The value converter's Convert method is called, every time the DataGrid cell is initially modified or lost its focus. In both cases, the user and the role roles are retrieved and the conversion result (if the user has the role assigned or not) is returned. The user row is fetched from the DataGridCell's DataContext, which contains the DataRowView instance that has the user row in its Row property. The role is retrieved from the ColumnTag that is assigned to the column when it was added.

The CheckBox control's Checked event is subscribed to when the DataGridCell is in editing mode, and unsubscribed when not.

C#
public class UserRoleValueConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        bool result = false;
        var dataGridCell = value as DataGridCell;
        if (dataGridCell != null)
        {
            var dataRowView = dataGridCell.DataContext as DataRowView;
            if (dataRowView != null)
            {
                var user = dataRowView.Row as UserRoleDataSet.UserRow;
                var role = ColumnTag.GetTag(dataGridCell.Column) as UserRoleDataSet.RoleRow;

                if (user != null && role != null)
                {
                    var checkBox = dataGridCell.Content as CheckBox;
                    if (checkBox != null)
                    {
                        if (dataGridCell.IsEditing)
                        {
                            checkBox.Checked += this.CheckBoxOnChecked;
                        }
                        else
                        {
                            checkBox.Checked -= this.CheckBoxOnChecked;
                        }
                    }

                    result =
                        DatabaseContext.Instance.DataSet.UserRole.Any(
                            x => x.UserRow == user && x.RoleRow == role);
                }
            }
        }

        return result;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }

The CheckedBoxOnChecked method is called whenever the check box state is modified. The logic searches for the CheckBox's DataGridCell and gets the user and role instances that belong to it. It will add or delete the user-role entry depending on the CheckBox.IsChecked state and whether a UserRoleRow is already present.

C#
    private void CheckBoxOnChecked(object sender, RoutedEventArgs routedEventArgs)
    {
        var checkBox = sender as CheckBox;
        var dataGridCell = ControlHelper.FindVisualParent<DataGridCell>(checkBox);
        if (dataGridCell != null)
        {
            var dataRowView = dataGridCell.DataContext as DataRowView;
            if (checkBox != null && dataRowView != null)
            {
                var user = dataRowView.Row as UserRoleDataSet.UserRow;
                var role = ObjectTag.GetTag(dataGridCell.Column) as UserRoleDataSet.RoleRow;

                if (user != null && role != null)
                {
                    if (checkBox.IsChecked == true
                        && DatabaseContext.Instance.DataSet.UserRole.Any(
                            x => x.UserRow == user && x.RoleRow == role) == false)
                    {
                        DatabaseContext.Instance.DataSet.UserRole.AddUserRoleRow(user, role);
                    }
                    else
                    {
                        var userRole =
                            DatabaseContext.Instance.DataSet.UserRole.FirstOrDefault(
                                x => x.UserRow == user && x.RoleRow == role);
                        if (userRole != null)
                        {
                            userRole.Delete();
                        }
                    }
                }
            }
        }
    }
}

Points of Interest

Checkbox Column Style Handling

As an added bonus (and to prevent extra state logic) I added the functionality that the CheckBox control is not shown in the user data grid new item row. The DataGridCheckBoxColumn style has to be modified, and the Visibility flag of the CheckBox has to be set, depending on the contents of the DataGridCell. If the data row is the new item row, then it has a NewItemPlaceHolder. A converter is used to get this information and it is mapped to the CheckBox's Visibility flag. The solution to this problem can be found here.

The CheckBox style is defined in the Style.xaml file in the Application layer. It is appended to the application's resource in a merged dictionary. A helper class in the ViewModel layer called ResourceDictionaryResolver iterates through the dictionaries in the merged dictionary container and searches for the dictionary with the given name (the name is in the dictionary Source property). The check box style can then be extracted from the resource dictionary through its key name.

Object Tagging

The standard WPF DataColumn doesn't allow object tagging. Object tagging is the functionality that allows objects to be tagged to a control. This can be used in situations where the control is available, but an object cannot be accessed using standard application logic. In the case of this sample, the available control is the CheckBox in the DataGridCell and the required object is the role that corresponds to the column. The role is tagged to the column and can be retrieved at a later time. The ObjectTag itself is a DependencyProperty that can be attached to any type of control that is derived from DependencyObject. The solution to this problem can be found here.

Database Save Handling

As second small bonus, I implemented the database saving, when the data is modified. The command is bound to the save toolbar button, and checks the database context for changes. The DataSet has built-in functionality that tests its contents for modifications. The button is enabled when the DataSet has changes, otherwise it is disabled. The Command's CanExecuteChanged is connected to CommandManager.RequerySuggested when it is subscribed to. The button state is then automatically checked when the application is idle by calling the CanExecute method in the main thread context.

Conclusion

This article shows an implementation of dynamic column handling for WPF DataGrid controls. It is a straight forward MVVM implementation where the dynamic column handling is done in the view-model layer. The drawback of this solution is that GUI components spilled over into the ViewModel layer. In the next article, I will show a solution that implements the same application, but with a more strict separation of business logic and GUI controls into their respective layers.

License

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