Click here to Skip to main content
15,883,901 members
Articles / Desktop Programming / WPF

Dynamic Columns in a WPF DataGrid Control (Part 1)

Rate me:
Please Sign up or sign in to vote.
4.97/5 (12 votes)
7 Aug 2015CPOL9 min read 97K   3.7K   39   10
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)


Written By
Technical Lead Seven-Air Gebr. Meyer AG
Switzerland Switzerland
I am a senior Program Manager, working for Seven-Air Gebr. Meyer AG since September 2022.
I started off programming in C++ and the MFC library, and moved on to the .Net and C# world with .Net Framework 1.0 and further. My projects consist of standalone, client-server applications and web applications.

Comments and Discussions

 
QuestionForeignKeyConstraint FK_User_UserRole Pin
JoeH6525-Feb-17 12:55
JoeH6525-Feb-17 12:55 
AnswerRe: ForeignKeyConstraint FK_User_UserRole Pin
Jeroen Richters28-Feb-17 4:41
professionalJeroen Richters28-Feb-17 4:41 
QuestionHow does the binding Path = new PropertyPath(".") work? Pin
Trong Thuy Dinh18-Jan-17 21:26
Trong Thuy Dinh18-Jan-17 21:26 
AnswerRe: How does the binding Path = new PropertyPath(".") work? Pin
Jeroen Richters20-Jan-17 2:46
professionalJeroen Richters20-Jan-17 2:46 
GeneralRe: How does the binding Path = new PropertyPath(".") work? Pin
Trong Thuy Dinh23-Jan-17 14:29
Trong Thuy Dinh23-Jan-17 14:29 
QuestionHow to save to a SQL Server database Pin
Member 1096078918-Oct-15 20:16
Member 1096078918-Oct-15 20:16 
AnswerRe: How to save to a SQL Server database Pin
Jeroen Richters19-Oct-15 4:51
professionalJeroen Richters19-Oct-15 4:51 
Hi there,
I am glad that I can help you a bit with getting to grips with WPF application programming.
I will show you how to persist the data in to SQL Server database below.
I will explain how to modify the example instead of posting the finished project to, hopefully, get most of the learning curve.

First of all I assume the following:
- You are using Visual Studio 2012
- SQL Express is used as the SQL Server
- The database file will reside in the "bin/Debug/Database" directory (You have to add the Database directory by hand)

Okay, here we go:

1) Add a new folder to the DataModel project called "DatabaseScripts"
2) Add a new file to the "DatabaseScripts" folder called "CreateDatabase.sql"
3) Copy the SQL Statements from below into the "CreateDatabase.sql" file

USE [master]
GO
/****** Object:  Database [UserRoleDatabase]    Script Date: 10/19/2015 13:33:52 ******/
CREATE DATABASE [UserRoleDatabase] ON  PRIMARY 
( NAME = N'UserRoleDatabase', FILENAME = N'[SOLUTION_PATH]\Bin\Debug\Database\UserRoleDatabase.mdf' , SIZE = 2304KB , MAXSIZE = UNLIMITED, FILEGROWTH = 1024KB )
 LOG ON 
( NAME = N'UserRoleDatabase_log', FILENAME = N'[SOLUTION_PATH]\Bin\Debug\Database\UserRoleDatabase.ldf' , SIZE = 1024KB , MAXSIZE = 2048GB , FILEGROWTH = 10%)
GO
ALTER DATABASE [UserRoleDatabase] SET COMPATIBILITY_LEVEL = 100
GO
IF (1 = FULLTEXTSERVICEPROPERTY('IsFullTextInstalled'))
begin
EXEC [UserRoleDatabase].[dbo].[sp_fulltext_database] @action = 'enable'
end
GO
ALTER DATABASE [UserRoleDatabase] SET ANSI_NULL_DEFAULT ON
GO
ALTER DATABASE [UserRoleDatabase] SET ANSI_NULLS ON
GO
ALTER DATABASE [UserRoleDatabase] SET ANSI_PADDING ON
GO
ALTER DATABASE [UserRoleDatabase] SET ANSI_WARNINGS ON
GO
ALTER DATABASE [UserRoleDatabase] SET ARITHABORT ON
GO
ALTER DATABASE [UserRoleDatabase] SET AUTO_CLOSE OFF
GO
ALTER DATABASE [UserRoleDatabase] SET AUTO_CREATE_STATISTICS ON
GO
ALTER DATABASE [UserRoleDatabase] SET AUTO_SHRINK OFF
GO
ALTER DATABASE [UserRoleDatabase] SET AUTO_UPDATE_STATISTICS ON
GO
ALTER DATABASE [UserRoleDatabase] SET CURSOR_CLOSE_ON_COMMIT OFF
GO
ALTER DATABASE [UserRoleDatabase] SET CURSOR_DEFAULT  LOCAL
GO
ALTER DATABASE [UserRoleDatabase] SET CONCAT_NULL_YIELDS_NULL ON
GO
ALTER DATABASE [UserRoleDatabase] SET NUMERIC_ROUNDABORT OFF
GO
ALTER DATABASE [UserRoleDatabase] SET QUOTED_IDENTIFIER ON
GO
ALTER DATABASE [UserRoleDatabase] SET RECURSIVE_TRIGGERS OFF
GO
ALTER DATABASE [UserRoleDatabase] SET  DISABLE_BROKER
GO
ALTER DATABASE [UserRoleDatabase] SET AUTO_UPDATE_STATISTICS_ASYNC OFF
GO
ALTER DATABASE [UserRoleDatabase] SET DATE_CORRELATION_OPTIMIZATION OFF
GO
ALTER DATABASE [UserRoleDatabase] SET TRUSTWORTHY OFF
GO
ALTER DATABASE [UserRoleDatabase] SET ALLOW_SNAPSHOT_ISOLATION OFF
GO
ALTER DATABASE [UserRoleDatabase] SET PARAMETERIZATION SIMPLE
GO
ALTER DATABASE [UserRoleDatabase] SET READ_COMMITTED_SNAPSHOT OFF
GO
ALTER DATABASE [UserRoleDatabase] SET HONOR_BROKER_PRIORITY OFF
GO
ALTER DATABASE [UserRoleDatabase] SET  READ_WRITE
GO
ALTER DATABASE [UserRoleDatabase] SET RECOVERY FULL
GO
ALTER DATABASE [UserRoleDatabase] SET  MULTI_USER
GO
ALTER DATABASE [UserRoleDatabase] SET PAGE_VERIFY CHECKSUM
GO
ALTER DATABASE [UserRoleDatabase] SET DB_CHAINING OFF
GO
USE [UserRoleDatabase]
GO
/****** Object:  Table [dbo].[User]    Script Date: 10/19/2015 13:33:53 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[User](
	[Id] [int] NOT NULL,
	[FirstName] [nvarchar](50) NULL,
	[LastName] [nvarchar](50) NULL,
PRIMARY KEY CLUSTERED 
(
	[Id] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
/****** Object:  Table [dbo].[Role]    Script Date: 10/19/2015 13:33:53 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Role](
	[Id] [int] NOT NULL,
	[Name] [nvarchar](50) NULL,
PRIMARY KEY CLUSTERED 
(
	[Id] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
/****** Object:  Table [dbo].[UserRole]    Script Date: 10/19/2015 13:33:53 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[UserRole](
	[UserRoleId] [int] NOT NULL,
	[UserId] [int] NOT NULL,
	[RoleId] [int] NOT NULL,
 CONSTRAINT [PK_UserRole_1] PRIMARY KEY CLUSTERED 
(
	[UserRoleId] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
/****** Object:  ForeignKey [FK_UserRole_ToRole]    Script Date: 10/19/2015 13:33:53 ******/
ALTER TABLE [dbo].[UserRole]  WITH CHECK ADD  CONSTRAINT [FK_UserRole_ToRole] FOREIGN KEY([RoleId])
REFERENCES [dbo].[Role] ([Id])
GO
ALTER TABLE [dbo].[UserRole] CHECK CONSTRAINT [FK_UserRole_ToRole]
GO
/****** Object:  ForeignKey [FK_UserRole_ToUser]    Script Date: 10/19/2015 13:33:53 ******/
ALTER TABLE [dbo].[UserRole]  WITH CHECK ADD  CONSTRAINT [FK_UserRole_ToUser] FOREIGN KEY([UserId])
REFERENCES [dbo].[User] ([Id])
GO
ALTER TABLE [dbo].[UserRole] CHECK CONSTRAINT [FK_UserRole_ToUser]
GO


4) Modify the script by replacing the [SOLUTION_PATH] placeholder in the SQL script to the path in which the "DataGridDynamicColumnsStd.sln" file resides
5) Run the script from Visual Studio, this should create the database in the "bin/Debug/Database" directory

This script creates the example database on the SQL server. There is one small difference in this database: the UserRole correlation table has a new primary key column. This column is required because the DataSet's table adapter deletes a row by its Id.

6) Delete the existing tables in the UserRoleDataSet designer
7) Right mouse click in the DataSet designer and select the "Add/Table Adapter..." menu item
8) The table adapter configuration wizard appears, click the "New Connection" button
9) In the server name combo box, enter the name to the SQL Express server
10) At the bottom of the dialog, select the "Attach a database file:" and use the "Browse..." button to select the newly created database file from the "bin/Debug/Database" directory. It is possible that you get the message that the file cannot be connected to because it is already in use. This the SQL Express server. Open the "Services" panel from the "Administrative Tools" and restart the SQL Express Server instance.
11) In the logical name text box, enter "UserRoleDatabase" and press the OK button
12) Press "Next" on the Table adapter connection wizard dialog after which a message box will ask you if you want to copy the database to the project. Answer with "No".
13) Again press the "Next" button to save the connection string. This string is required for the opening of the SQL connections.
14) Press "Next" to select the "Use SQL statements". This means that the data adapter (the instance that loads and saves the data from and to the database) will use standard SQL statements for the operations
15) Select the "Query Builder..." button to open the Query Builder dialog
16) In the topmost "Add Table" dialog, select the User table and press the "Add" and then the "Close" button. This is the first data table that we will add to the data set
17) In the Query Builder dialog in the table, select all items except the (*(All Columns)) and finally the "OK" button
18) Either click the "Next" button to navigate through the wizard to see what it will do, or press the "Finish" button to close the wizard and add the table adapter to the data set
19) Repeat steps 7 to 18 to add the "Role" and the "UserRole" tables. Use the existing connection string (press Next) and again for using the SQL statements.

20) At this stage the DataSet should look like original one, but some modifications have to be made
21) In the data set designer select the relation between the User and the UserRole table, press the right mouse button to "Edit Relation...". This displays the Relation dialog
22) The three radio buttons define the relation type. The "Relation Only" type defines that there is a relation between both tables, but nothing else. The "Foreign Key Constraint" type defines that parent child relations are checked on key constraints. So parent objects must be added before child objects. The "Both Relation and Foreign Key Constraint" is the relation type that must be selected. The Rule combo boxes will be enabled, and the "Delete Rule" must be set to "Cascade", which means that child objects are automatically deleted when its parent objects is deleted. If this is not set, then the child object has to be explicitly deleted in code
23) Repeat step 22 for the Role-UserRole relation
24) From the User table, select the "Id" column and open the column's property view. Set the property "AutoIncrement" to true. With this option set to true, new rows will automatically get a new Id value. This will only work when the Column type is integer.
25) Repeat step 24 for the "UserRoleId" column in the UserRole table and the "Id" column in the Role table

Now the UserRoleDataSet is defined and the solution should compile.
The last step is to modify the DatabaseContext to use the data adapters

26) Open the DatabaseContext class and modify the constructor as follows.
- the SQL connection is opened with the connection string that the wizard stored in the project's settings
- The data is loaded with the data adapter's Fill() method
- If the database is empty then demo data is created

C#
private DatabaseContext()
{
    try
    {
        this.DataSet = new UserRoleDataSet();

        string connectionString = Settings.Default.UserRoleDatabaseConnectionString;
        using (SqlConnection sqlConnection = new SqlConnection(connectionString))
        {
            sqlConnection.Open();

            // Data loading must be performed top/down, or first the parent and then the child tables.
            using (UserTableAdapter userTableAdapter = new UserTableAdapter())
            {
                userTableAdapter.Connection = sqlConnection;
                userTableAdapter.Fill(this.DataSet.User);
            }

            using (RoleTableAdapter roleTableAdapter = new RoleTableAdapter())
            {
                roleTableAdapter.Connection = sqlConnection;
                roleTableAdapter.Fill(this.DataSet.Role);
            }

            using (UserRoleTableAdapter userRoleTableAdapter = new UserRoleTableAdapter())
            {
                userRoleTableAdapter.Connection = sqlConnection;
                userRoleTableAdapter.Fill(this.DataSet.UserRole);
            }

            this.DataSet.AcceptChanges();
        }

        if (this.DataSet.User.Rows.Count == 0 && this.DataSet.Role.Rows.Count == 0
            && this.DataSet.UserRole.Rows.Count == 0)
        {
            this.CreateDemoData();
        }
    }
    catch (Exception exception)
    {
        // Handle exception correctly -> at least give user feedback.
        Console.WriteLine(exception);
    }
}


27) Modify the Save() method
- again the SQL connection is opened with the connection string that the wizard stored in the project's settings
- A transaction is created to ensure database table consistency
- First the delete records are save to the database, bottom/up
- Second the inserted data is saved, top/down
- Third the modified data is saved, top/down
- The transaction is committed when no exceptions occurred

C#
public void Save()
{
    string connectionString = Settings.Default.UserRoleDatabaseConnectionString;
    using (SqlConnection sqlConnection = new SqlConnection(connectionString))
    {
        sqlConnection.Open();
        using (SqlTransaction sqlTransaction = sqlConnection.BeginTransaction())
        {
            try
            {
                this.UpdateUserRoleTable(sqlConnection, sqlTransaction, DataViewRowState.Deleted);
                this.UpdateUserTable(sqlConnection, sqlTransaction, DataViewRowState.Deleted);
                this.UpdateRoleTable(sqlConnection, sqlTransaction, DataViewRowState.Deleted);

                this.UpdateUserTable(sqlConnection, sqlTransaction, DataViewRowState.Added);
                this.UpdateRoleTable(sqlConnection, sqlTransaction, DataViewRowState.Added);
                this.UpdateUserRoleTable(sqlConnection, sqlTransaction, DataViewRowState.Added);

                this.UpdateUserTable(sqlConnection, sqlTransaction, DataViewRowState.ModifiedCurrent);
                this.UpdateRoleTable(sqlConnection, sqlTransaction, DataViewRowState.ModifiedCurrent);
                this.UpdateUserRoleTable(sqlConnection, sqlTransaction, DataViewRowState.ModifiedCurrent);

                sqlTransaction.Commit();
                this.DataSet.AcceptChanges();
            }
            catch (Exception exception)
            {
                // Handle exception correctly -> at least give user feedback.
                Console.WriteLine(exception);
                sqlTransaction.Rollback();
            }
        }
    }
}


28) Add the following helper methods:

C#
private void UpdateUserTable(SqlConnection sqlConnection, SqlTransaction sqlTransaction, DataViewRowState dataViewRowState)
{
    using (UserTableAdapter userTableAdapter = new UserTableAdapter())
    {
        userTableAdapter.Connection = sqlConnection;
        userTableAdapter.Transaction = sqlTransaction;
        userTableAdapter.Update(this.DataSet.User.Select(null, null, dataViewRowState));
    }
}

private void UpdateRoleTable(SqlConnection sqlConnection, SqlTransaction sqlTransaction, DataViewRowState dataViewRowState)
{
    using (RoleTableAdapter roleTableAdapter = new RoleTableAdapter())
    {
        roleTableAdapter.Connection = sqlConnection;
        roleTableAdapter.Transaction = sqlTransaction;
        roleTableAdapter.Update(this.DataSet.Role.Select(null, null, dataViewRowState));
    }
}

private void UpdateUserRoleTable(SqlConnection sqlConnection, SqlTransaction sqlTransaction, DataViewRowState dataViewRowState)
{
    using (UserRoleTableAdapter userRoleTableAdapter = new UserRoleTableAdapter())
    {
        userRoleTableAdapter.Connection = sqlConnection;
        userRoleTableAdapter.Transaction = sqlTransaction;
        userRoleTableAdapter.Update(this.DataSet.UserRole.Select(null, null, dataViewRowState));
    }
}


29) Run the application

I hope that this small tutorial helps you to load and save from your database.
But, keep in mind that there are alternatives around. You might also look into other solutions like Entity Framework, which can be easier to use.
Kind regards,
Jeroen
GeneralRe: How to save to a SQL Server database Pin
Member 1096078919-Oct-15 11:57
Member 1096078919-Oct-15 11:57 
QuestionThe checked results will be changed in UI with the scrolling up/down Pin
Pippen Liu30-Jul-15 16:54
Pippen Liu30-Jul-15 16:54 
AnswerRe: The checked results will be changed in UI with the scrolling up/down Pin
Jeroen Richters7-Aug-15 0:19
professionalJeroen Richters7-Aug-15 0:19 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.