How to add a dynamic image and/or a dynamic button to a DataGrid row using a DataGridTemplateColumn and a DataTemplateSelector?
How to add a dynamic image and/or a dynamic button to a row using WPFToolKit DataGrid and DataGridTemplateColumns?
To start, I have a WPF project in Visual Studio 2008. I have installed the WPFToolKit and have added a reference to it in my project.
Often you want to display a DataGrid, but you don’t want to simply display it as is, you want to be able to enhance it and add functionality to it, such as adding an image to the start of each row or adding a button on each row.
Ok, so I have a table created using a DataTable that looks as follows:
IntVal | StrVal |
---|---|
0 | normal |
1 | warning |
2 | error |
I am passing this to a WFPToolKit DataGrid.
As I pass this to a Datagrid I want to add two columns:
- I want to add an image that is different if it is normal, warning, or error.
- I want to add a button only if it is warning or error.
So the visual would look as follows:
Image | IntVal | StrVal | Action |
---|---|---|---|
0 | normal | ||
1 | warning | ||
2 | error |
Step 1. Install prerequisites: Install Visual Studio 2008, and download and install the WPFToolkit.
You probably already have this done, and there are no steps for provided for these.
Step 2. Create a new WPF project in Visual studio 2008 and design the WPF interface
So once my project was created and the reference to WPFToolKit added, I then changed the XAML on my default Window1 class.
- I needed to add a reference to the toolkit here as well.
- I needed to add resources for my button.
- I needed to add three separate resources for my images.
- I needed to add a DataGrid.
Window1.xaml
<window x:Class="DataGridAddButtonAndImageColumns.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:wpftk="http://schemas.microsoft.com/wpf/2008/toolkit" Title="Window1" Height="300" Width="300"> <window.Resources> <dataTemplate x:Key="FixThisTemplate"> <button Name="mButtonFixThis" Click="ButtonFixThis_Click">Fix This</button> </dataTemplate> <dataTemplate x:Key="NormalTemplate"> </dataTemplate> <dataTemplate x:Key="StatusTemplateNormal" x:Name="mNormalImage"> <image Width="16" Height="16" Source="C:\Users\jbarneck\Documents\QuickTests\DataGridAddButtonAndImageColumns\DataGridAddButtonAndImageColumns\bin\Debug\Normal.png" /> </dataTemplate> <dataTemplate x:Key="StatusTemplateWarning" x:Name="mWarningImage"> <image Width="16" Height="16" Source="C:\Users\jbarneck\Documents\QuickTests\DataGridAddButtonAndImageColumns\DataGridAddButtonAndImageColumns\bin\Debug\Warning.png" /> </dataTemplate> <dataTemplate x:Key="StatusTemplateError" x:Name="mErrorImage"> <image Width="16" Height="16" Source="C:\Users\jbarneck\Documents\QuickTests\DataGridAddButtonAndImageColumns\DataGridAddButtonAndImageColumns\bin\Debug\Error.png" /> </dataTemplate> </window.Resources> <grid> <wpftk:DataGrid Name="mDataGrid" ItemsSource="{Binding}" CanUserAddRows="False" IsReadOnly="True"></wpftk:DataGrid> </grid> </window>
Step 3 – Create the data
The data can come from anywhere but for this basic example, I am just statically creating a DataTable in the Constructor. I also added a property for the DataTable and the DataTable.DefaultView.
Data.cs
using System.Data; namespace DataGridAddButtonAndImageColumns { public class Data { #region Member Variables private DataTable mTable; #endregion #region Constructors /* * The default constructor */ public Data() { mTable = new DataTable(); mTable.Columns.Add("IntVal", typeof(int)); mTable.Columns.Add("StrVal", typeof(string)); DataRow row0 = mTable.NewRow(); row0["IntVal"] = 0; row0["StrVal"] = "normal"; mTable.Rows.Add(row0); DataRow row1 = mTable.NewRow(); row1["IntVal"] = 1; row1["StrVal"] = "warning"; mTable.Rows.Add(row1); DataRow row2 = mTable.NewRow(); row2["IntVal"] = 2; row2["StrVal"] = "error"; mTable.Rows.Add(row2); } #endregion #region Properties public DataTable Table { get { return mTable; } set { mTable = value; } } public DataView View { get { return mTable.DefaultView; } } #endregion #region Functions #endregion #region Enums #endregion } }
Step 4 – Create a ViewModel that implements INotifyPropertyChanged.
So creating a ViewModel is not exactly required but there really is benefit to the Model-View-ViewModel design pattern, so I will attempt to follow it even though this is a simple example application.
- I created a new object called DataViewModel.
- I implemented the INotifyPropertyChanged interface (though for this small application it isn’t used, I don’t want to leave it out cause you might need it for your application.)
- I changed the constructor to take the Data object I designed in the previous step.
- I expose the Table and the Table’s view as properties.
DataViewModel.cs
using System; using System.ComponentModel; using System.Data; namespace DataGridAddButtonAndImageColumns { public class DataViewModel : INotifyPropertyChanged { #region Member Variables readonly Data mData; #endregion #region Constructors /* * The default constructor */ public DataViewModel(Data inData) { mData = inData; } #endregion #region Properties public DataView View { get { return mData.View; } } public DataTable Table { get { return mData.Table; } } #endregion #region Functions #endregion #region Enums #endregion #region INotifyPropertyChanged Members public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { if (this.PropertyChanged != null) this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } #endregion } }
Step 5 – Add code to pass the DataTable to the DataGrid
So in the Window1.xaml.cs file, I create a new DataViewModel object and pass it a new Data object. I then assign the DataTable to the DataGrid’s DataContext object. My class now looks as follows.
Window1.xaml.cs
using System.Windows; using System.Windows.Controls; namespace DataGridAddButtonAndImageColumns { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class Window1 : Window { #region Member Variables #endregion #region Contructor public Window1() { InitializeComponent(); DataViewModel model = new DataViewModel(new Data()); // It is ok to pass either the DataTable or the DataView // so both lines below work, however I am only using one: // // mDataGrid.DataContext = model.View; // mDataGrid.DataContext = model.Table; mDataGrid.DataContext = model.Table; } #endregion #region Functions private void ButtonFixThis_Click(object sender, RoutedEventArgs e) { // Do something here } #endregion #region Properties #endregion } }
Now I can compile and run see my simple DataGrid.
IntVal | StrVal |
---|---|
0 | normal |
1 | warning |
2 | error |
Step 6 – Create the DataTemplateSelectors
I am going to use two DataTemplateSelector and I want them to share a base class, so first, I am going to create a base class for them.
- I inherit DataTemplateSelector.
- I add a function to find the parent Window1 object.
BaseDataTemplateSelector.cs
using System.Windows; using System.Windows.Controls; using System.Windows.Media; namespace DataGridAddButtonAndImageColumns { public class BaseDataTemplateSelector : DataTemplateSelector { #region Member Variables #endregion #region Constructors /* * The default constructor */ public BaseDataTemplateSelector() { } #endregion #region Properties #endregion #region Functions protected Window1 GetWindow1(DependencyObject inContainer) { DependencyObject c = inContainer; while (true) { DependencyObject p = VisualTreeHelper.GetParent(c); if (c is Window1) { //mSectionControl = c; return c as Window1; } else { c = p; } } } #endregion } }
Now I create an ActionDataTemplateSelector and a StatusImageDataTemplateSelector.
The ActionDataTemplateSelector will overload the SelectTemplate function and correctly select the Fix button resource if the status is warning or error.
ActionDataTemplateSelector.cs
using System.Data; using System.Windows; namespace DataGridAddButtonAndImageColumns { public class ActionDataTemplateSelector : BaseDataTemplateSelector { #region Constructors /* * The default constructor */ public ActionDataTemplateSelector() { } #endregion #region Functions public override DataTemplate SelectTemplate(object inItem, DependencyObject inContainer) { DataRowView row = inItem as DataRowView; if (row != null) { Window1 w = GetWindow1(inContainer); if (row.DataView.Table.Columns.Contains("IntVal")) { if ((int)row["IntVal"] > 0) { return (DataTemplate)w.FindResource("FixThisTemplate"); } } return (DataTemplate)w.FindResource("NormalTemplate"); } return null; } #endregion } }
The StatusImageDataTemplateSelector also overloads the SelectTempate function and selects the correct image for the status.
StatusImageDataTemplateSelector .cs
using System.Data; using System.Windows; namespace DataGridAddButtonAndImageColumns { public class StatusImageDataTemplateSelector : BaseDataTemplateSelector { #region Constructors /* * The default constructor */ public StatusImageDataTemplateSelector() { } #endregion #region Functions public override DataTemplate SelectTemplate(object inItem, DependencyObject inContainer) { DataRowView row = inItem as DataRowView; if (row != null) { if (row.DataView.Table.Columns.Contains("IntVal")) { Window1 w = GetWindow1(inContainer); int status = (int)row["IntVal"]; if (status == 0) { return (DataTemplate)w.FindResource("StatusTemplateNormal"); } if (status == 1) { return (DataTemplate)w.FindResource("StatusTemplateWarning"); } if (status == 2) { return (DataTemplate)w.FindResource("StatusTemplateError"); } } } return null; } #endregion } }
Step 7 – Create functions that add the new columns and have the constructor call each function.
Each function must:
- Create a new DataGridTemplateColumn.
- Assign a string for the Header.
- Create a new DataTemplateSelector and assign it to the DataGridTemplateColumn’s CellTemplateSelector.
- Add the new DataGridTemplateColumn to the DataGrid.
public void CreateActionButtonColumn() { DataGridTemplateColumn actionColumn = new DataGridTemplateColumn { CanUserReorder = false, Width = 85, CanUserSort = true }; actionColumn.Header = "Action"; actionColumn.CellTemplateSelector = new ActionDataTemplateSelector(); mDataGrid.Columns.Add(actionColumn); } public void CreateStatusColumnWithImages() { DataGridTemplateColumn statusImageColumn = new DataGridTemplateColumn { CanUserReorder = false, Width = 85, CanUserSort = false };; statusImageColumn.Header = "Image"; statusImageColumn.CellTemplateSelector = new StatusImageDataTemplateSelector(); mDataGrid.Columns.Insert(0, statusImageColumn); }
Don’t forget to call the functions in the constructor.
public Window1() { InitializeComponent(); DataViewModel model = new DataViewModel(new Data()); // It is ok to pass either the DataTable or the DataView // so both lines below work, however I am only using one: // // mDataGrid.DataContext = model.View; // mDataGrid.DataContext = model.Table; mDataGrid.DataContext = model.Table; CreateActionButtonColumn(); CreateStatusColumnWithImages(); }
Ok, so now you are finished. This should be working for you if you compile and run the program.
Image | IntVal | StrVal | Action |
---|---|---|---|
0 | normal | ||
1 | warning | ||
2 | error |
Options for handling the images without using a static path
The images were called statically in the above example, however, that will be problematic in actual implementation as each program is installed in a different location and the install location can usually be chosen by a user. You have two options to resolve this, and I will show you how to do both:
- Embedding your images
- Using image files located in a relative path
Either option work. The second option makes branding a little easier as code doesn’t have to be recompiled with new images to change the images, because the image files can simply be replaced.
Embedding your images
So you can embed your images as resources and use the embedded resources instead. To embed them, do this:
- In Visual Studio under your project, create a folder called Images.
- Copy your images into that folder.
- In the XAML, change each of the image resource lines as shown
<image Width="16" Height="16" Source="Images\Warning.png" />
Using image files located in a relative path
I decided to NOT embed my images but instead solve this by using a relative path. My preference is for the images to come from actual files in an images directory that is relative to the directory from which the executable is launched:
\MyFolder\
\MyFolder\program.exe
\MyFolder\Images\
\MyFolder\Images\Normal.png
\MyFolder\Images\Warning.png
\MyFolder\Images\Error.png
So in order to use relative paths, I found that I could create another object that inherits IValueConverter.
Here is what I had to do to create this:
- Create a new class called PathConverter.
- Make it inherit IValueConverter
- Implement the IValueConverter interface.
- Add code to the Convert function.
- Cast the “value” parameter to a DataRowView as each DataRow will be the calling object that is passed in as “value”.
- Get the relative path using System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location).
- Get the status value from the DataRowView and add a couple of if statements that return the relative + the image file path.
using System; using System.Data; using System.Globalization; using System.Windows.Data; namespace DataGridAddButtonAndImageColumns { public class PathConverter : IValueConverter { #region Constructors /* * The default constructor */ public PathConverter() { } #endregion #region IValueConverter Members public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { DataRowView row = value as DataRowView; if (row != null) { if (row.DataView.Table.Columns.Contains("IntVal")) { String workingDirectory = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); int status = (int)row["IntVal"]; if (status == 0) { return workingDirectory + @"Images\Normal.png"; } if (status == 1) { return workingDirectory + @"Images\Warning.png"; } if (status == 2) { return workingDirectory + @"Images\Error.png"; } } } return null; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new System.NotImplementedException(); } #endregion } }
Ok, I am not done yet. I now needed to edit the XAML again. here is what we do to the XAML:
- I add an xmlns reference to load the local namespace.
- I add in the windows resources and instance of the PathConverter.
- I change the Image Source value to: Source=”{Binding Converter={StaticResource ImagePathConverter}}”
<window x:Class="DataGridAddButtonAndImageColumns.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:wpftk="http://schemas.microsoft.com/wpf/2008/toolkit" xmlns:local="clr-namespace:DataGridAddButtonAndImageColumns" Title="Window1" Height="300" Width="300"> <window.Resources> <local:PathConverter x:Key="ImagePathConverter" /> <dataTemplate x:Key="FixThisTemplate"> <button Name="mButtonFixThis" Click="ButtonFixThis_Click">Fix This</button> </dataTemplate> <dataTemplate x:Key="NormalTemplate"> </dataTemplate> <dataTemplate x:Key="StatusTemplateNormal" x:Name="mNormalImage"> <image Width="16" Height="16" Margin="3,0" Source="{Binding Converter={StaticResource ImagePathConverter}}" /> <!--<image Width="16" Height="16" Source="Images\Normal.png" />--><!-- Embedded --> </dataTemplate> <dataTemplate x:Key="StatusTemplateWarning" x:Name="mWarningImage"> <image Width="16" Height="16" Margin="3,0" Source="{Binding Converter={StaticResource ImagePathConverter}}" /> <!--<image Width="16" Height="16" Source="Images\Warning.png" />--><!-- Embedded --> </dataTemplate> <dataTemplate x:Key="StatusTemplateError" x:Name="mErrorImage"> <image Width="16" Height="16" Margin="3,0" Source="{Binding Converter={StaticResource ImagePathConverter}}" /> <!--<image Width="16" Height="16" Source="Images\Error.png" />--><!-- Embedded --> </dataTemplate> </window.Resources> <grid> <wpftk:DataGrid Name="mDataGrid" ItemsSource="{Binding}" CanUserAddRows="False" IsReadOnly="True"></wpftk:DataGrid> </grid> </window>
Ok, now we should be done.
Make sure to create the Images folder and add the images in the location where you exectuable runs. You may have to add the images folder to both the debug and release directories or otherwise resolve this, else you will get an exception when the images are not found.
Note: I wrote an improved version of this article and published it here:
http://www.codeproject.com/KB/WPF/AddImageToColumnDynamicly.aspx
Copyright ® Rhyous.com – Linking to this page is allowed without permission and as many as ten lines of this page can be used along with this link. Any other use of this page is allowed only by permission of Rhyous.com.
There is one problem. If you add code to the ButtonFixThis_Click function, such that it changes IntVal to 0, the “Fix This” button does not go away as we would expect. Instead it stays there and seems to indicate that the DataTemplate selection is not being reevaluated when the underlying data changes. Put otherwise, I can sit there and click “Fix This” all day, but even though behind the scenes I am indeed changing the value of IntVal, it does not change the interface as I would expect it to.
Is there a way to fix that?
Change the DataViewModel to add an event for when the DataTable changes, like this.
Now I can simulate a fix of a row like this:
Running AcceptChanges() causes the new even to run and the event is called which runs NotifyPropertyChanged(“Table”);