Silverlight Dynamic Datagrid or Datagrid with dynamic columns
with editable fields
I’ve done a similar post about DataGrid with Dynamic columns not too long ago which you can refer to. The last post is mainly about displaying data. This post is a bit more sophisticated as far as code and logic concern because we have to deal with editable fields.
Yes, based on the datatype, which we will decide to display TextBox, TextBlock, DatePicker, or ComboBox. Once you know this basic, I’m sure you can simply add more control to it as you wish.
Once again, here is my scenario, and what I’m trying to accomplish. I need a Silverlight Datagrid that will do the following:
1) Display a list of dataItem (row)
2) The dataItem (row) will display dynamic columns
(meaning columns are different based on client tables or for whatever reason)
3) Instead of wait for user to click on a field and show the editable fields (
which from cellItemTemplate to cellEditingItemTemplate)
4) Make the datagrid works like WPF datagrid,
which always have a empty row at the bottom, which allow user to add new dataItem
- Each time an empty row is filled, a new empty row should be automatically added.
HOW DO WE ACCOMPLISH THIS?
There are 2 problems we must solve:
1) Dynamically create DataGrid:
- This means creating a datagrid in code behind and DYNAMICALLY BIND datagrid controls to objects accordingly
2) Retrieve and store dynamic columns as a list
Get and store each dynamic column, its data, its datatype, its reference object list and ... in a WORKABLE FORMAT.
Let’s slowly go through each problem, break it down and solve it
1) DYNAMICALLY CREATE DATAGRID:
A regular DataGrid would look like this in XAML:
<sdk:DataGrid x:Name="dataGrid_DiTranItems"
AutoGenerateColumns="False"
RowHeight="25"
Width="600"
ItemsSource="{Binding Path=DitranItemList}" >
<sdk:DataGrid.Columns>
<sdk:DataGridTemplateColumn Header="State">
<sdk:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=State}"
Style="{StaticResource textBLock_Value}"/>
</DataTemplate>
</sdk:DataGridTemplateColumn.CellTemplate>
<sdk:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding StateList}"
SelectedItem="{Binding State, Mode=TwoWay}" />
</DataTemplate>
</sdk:DataGridTemplateColumn.CellEditingTemplate>
</sdk:DataGridTemplateColumn>
<sdk:DataGridTemplateColumn Header="Moved In Date">
<sdk:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Date}"
Style="{StaticResource textBLock_Value}"/>
</DataTemplate>
</sdk:DataGridTemplateColumn.CellTemplate>
<sdk:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<sdk:DatePicker Text="{Binding Date, Mode=TwoWay}" />
</DataTemplate>
</sdk:DataGridTemplateColumn.CellEditingTemplate>
</sdk:DataGridTemplateColumn>
<sdk:DataGridTemplateColumn Header="My Name">
<sdk:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Name}"
Style="{StaticResource textBLock_Value}"/>
</DataTemplate>
</sdk:DataGridTemplateColumn.CellTemplate>
<sdk:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<TextBox Text="{Binding Path=Name, Mode=TwoWay}" />
</DataTemplate>
</sdk:DataGridTemplateColumn.CellEditingTemplate>
</sdk:DataGridTemplateColumn>
</sdk:DataGrid.Columns>
</sdk:DataGrid>
Code behind view of the same datagrid:
DataGrid theDataGrid = new DataGrid();
theDataGrid.Columns.Add(
this.createNewTemplateColumn(item.Name, item.Name));
//Below is just an example of adding a textbox control to the cellEditingTemplate, but you get the idea. You can put int any control you like
private DataGridTemplateColumn createNewTemplateColumn(string headerName, string fieldName)
{
DataGridTemplateColumn newCol = new DataGridTemplateColumn();
newCol.Header = headerName;
StringBuilder CellTemp = new StringBuilder();
CellTemp.Append("<DataTemplate ");
CellTemp.Append("xmlns='http://schemas.microsoft.com/winfx/");
CellTemp.Append("2006/xaml/presentation' ");
CellTemp.Append("xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' ");
CellTemp.Append("xmlns:basics='clr-namespace:System.Windows.Controls;");
CellTemp.Append("assembly=System.Windows.Controls' >");
CellTemp.Append("<TextBlock ");
CellTemp.Append("Text='{Binding " + fieldName + ", Mode=TwoWay}' />");
CellTemp.Append("</DataTemplate>");
StringBuilder CellETemp = new StringBuilder();
CellETemp.Append("<DataTemplate ");
CellETemp.Append("xmlns='http://schemas.microsoft.com/winfx/");
CellETemp.Append("2006/xaml/presentation' ");
CellETemp.Append("xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' ");
CellETemp.Append("xmlns:basics='clr-namespace:System.Windows.Controls;");
CellETemp.Append("assembly=System.Windows.Controls' >");
CellETemp.Append("<TextBox ");
CellETemp.Append("Text='{Binding " + fieldName + ", Mode=TwoWay}' />");
CellETemp.Append("</DataTemplate>");
//newCol.CellTemplate = (DataTemplate)XamlReader.Load(CellTemp.ToString());
newCol.CellTemplate = (DataTemplate)XamlReader.Load(CellETemp.ToString());
newCol.CellEditingTemplate = (DataTemplate)XamlReader.Load(CellETemp.ToString());
return newCol;
}
2) RETRIEVE AND STORE DYNAMIC COLUMNs
- What we need here is a list of items for the datagrid, and in each item object should contain a list of dynamic columns and its value for us to display to the UI.
Normally, we create static properties within an item class and bind each property to the datagrid control, by there’s no such thing as dynamic property. Property has to be created and bind at pre-runtime of the code.
To accomplish this, we will make use of something as Dictionary<String, object>.
Yes, it’s similar to my old post, but instead of just a regular object, I’ll explicitly define the object to be a CustomField object such as Dictionary<String, CustomField>.
CustomField Class will look like this:
public class CustomField : ModelBase
{
private string _name;
private string _displayName;
private string _dataType;
private CustomFieldOptionItem _selectedItem;
private ObservableCollection<CustomFieldOptionItem> _optionItemList;
public CustomField()
{
}
public string Name
{
get { return this._name; }
set
{
if (this._name != value)
{
this._name = value;
this.RaisePropertyChanged("Name");
}
}
}
public string DisplayName
{
get { return this._displayName; }
set
{
if (this._displayName != value)
{
this._displayName = value;
this.RaisePropertyChanged("DisplayName");
}
}
}
public string DataType
{
get { return this._dataType; }
set
{
if (this._dataType != value)
{
this._dataType = value;
this.RaisePropertyChanged("DataType");
}
}
}
public ObservableCollection<CustomFieldOptionItem> OptionItemList
{
get { return this._optionItemList; }
set
{
if (this._optionItemList != value)
{
this._optionItemList = value;
this.RaisePropertyChanged("OptionItemList");
}
}
}
public CustomFieldOptionItem SelectedItem
{
get { return this._selectedItem; }
set
{
if (this._selectedItem != value)
{
this._selectedItem = value;
this.RaisePropertyChanged("SelectedItem");
}
}
}
}
CustomField class will include everything about a field such as: Name, DataType, and … But also a OptionItemList and SelectedItem properties. If the field is datatype such as ComboBox then it will have this list to populate the ComboBox, the SelectedItem to bind to.
You probably already noticed, there’s no Value property, because SelectedItem will be the object that hold the value for the CustomField. Yes, SelectedItem is type CustomFieldOptionItem so the class is defined as below:
public class CustomFieldOptionItem : ModelBase, IDataErrorInfo
{
private int _id;
private string _name;
private string _value;
private string _desc;
private string _parentFieldName;
private Func<string,string,string> _validationFunc;
#region Constructors
//Constructors--------------------
public CustomFieldOptionItem()
{
this._id = 0;
this._name = "";
this._value = "";
this._desc = "";
}
public CustomFieldOptionItem(int id, string name, string value, string desc, string parentFieldName, Func<string,string,string> theValidationFunc)
{
this._id = id;
this._name = name;
this._value = value;
this._desc = desc;
this._parentFieldName = parentFieldName;
this._validationFunc = theValidationFunc;
}
#endregion //End Constructor--------------
#region Properties
//properties-----------------------
public int ID
{
get { return this._id; }
set
{
if (this._id != value)
{
this._id = value;
this.RaisePropertyChanged("ID");
}
}
}
public string Name
{
get { return this._name; }
set
{
if (this._name != value)
{
this._name = value;
this.RaisePropertyChanged("Name");
}
}
}
public string Value
{
get { return this._value; }
set
{
if (this._value != value)
{
this._value = value;
this.RaisePropertyChanged("Value");
}
}
}
public string Desc
{
get { return this._desc; }
set
{
if (this._desc != value)
{
this._desc = value;
this.RaisePropertyChanged("Desc");
}
}
}
public string ParentFieldName
{
get { return this._parentFieldName; }
set
{
if (this._parentFieldName != value)
{
this._parentFieldName = value;
this.RaisePropertyChanged("ParentFieldName");
}
}
}
#endregion //End properties
#region VALIDATION
//For Validation
private string _errors;
private const string _errorsText = "Error in data.";
//---------VALIDATION : IDataErrorInfo IMPLEMENTATION ----------------------------------
public string Error
{
get { return this._errors; }
}
public string this[string propertyName]
{
get
{
this._errors = null;
switch (propertyName)
{
case "Value":
//Dynamically validate data
if (this._validationFunc != null)
{
this._errors = _errorsText;
string theErrorMsg = "";
theErrorMsg = this._validationFunc.Invoke(this.Value, this.ParentFieldName);
if (theErrorMsg.Trim().Length > 0)
return theErrorMsg;
}
////Hardcoded sample
//if (string.IsNullOrEmpty(this.Value))
//{
// this._errors = _errorsText;
// return "Value is required!";
//}
break;
}
return null;
}
}
//------------------END OF VALIDATION--------------------------------
#endregion
}
This class is pretty straight forward as it only hold a constructor and a few properties. The focus on this class is the validation part at the bottom of the class. Yes, the class also do validation, and at this point it only does it on the Value property.
The validation logic is not in the class itself, but it will be imported as a function (Func<string,string,string> _validationFunc) from the instantiation wherever it maybe later. This functiontion will take two parameters (one is the value to validate, and the other is the CustomField Name), and return a string as error if there’s any.
PUT THEM ALL TOGETHER IN A SAMPLE
Ok now, we know how it works a little bit. Let’s put them all together.
//So we have a list of dictionary as below
List<Dictionary<String, CustomField>> customFieldsList =
new List<Dictionary<string, CustomField>>();
// We will loop through them and create a dynamic datagrid with it. Assuming this customFieldsList is filled up with data for now.
CustomField theCustomField;
foreach (KeyValuePair<string, CustomField> item in CustomFieldsList)
{
theCustomField = item.Value as CustomField;
theDataGrid.Columns.Add(
this.createNewTemplateColumnForDictionary(
theCustomField.DisplayName,
customFieldsDictName,
item.Key.ToString(),
theCustomField)
);
}
//To create dynamic dataGridTemplate columns for Dictionary items, I need to modified the createnewTemplateColmn slightly and make it a bit different than the method above and named it CreateNewTemplatecolumnForDictionary() as below:
private DataGridTemplateColumn createNewTemplateColumnForDictionary(string headerName, string dictionaryName, string fieldName, CustomField theCustomField)
{
DataGridTemplateColumn newCol = new DataGridTemplateColumn();
newCol.Header = headerName;
StringBuilder CellTemp = new StringBuilder();
CellTemp.Append("<DataTemplate ");
CellTemp.Append("xmlns='http://schemas.microsoft.com/winfx/");
CellTemp.Append("2006/xaml/presentation' ");
CellTemp.Append("xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' ");
CellTemp.Append("xmlns:sdk='http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk' ");
CellTemp.Append("xmlns:basics='clr-namespace:System.Windows.Controls;");
CellTemp.Append("assembly=System.Windows.Controls' >");
CellTemp.Append("<TextBlock ");
CellTemp.Append("Text='{Binding " + dictionaryName + "[" + fieldName + "].SelectedItem.Value, Mode=TwoWay}' />");
CellTemp.Append("</DataTemplate>");
StringBuilder CellETemp = new StringBuilder();
CellETemp.Append("<DataTemplate ");
CellETemp.Append("xmlns='http://schemas.microsoft.com/winfx/");
CellETemp.Append("2006/xaml/presentation' ");
CellETemp.Append("xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' ");
CellETemp.Append("xmlns:sdk='http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk' ");
CellETemp.Append("xmlns:basics='clr-namespace:System.Windows.Controls;");
CellETemp.Append("assembly=System.Windows.Controls' >");
if (theCustomField.DataType.ToUpper() == "COMBOBOX")
{
//Note: The listed order of ItemsSource before SelectedItem binding is the must
CellETemp.Append("<ComboBox DisplayMemberPath='Value' ");
CellETemp.Append("ItemsSource='{Binding " + dictionaryName + "[" + fieldName + "].OptionItemList, Mode=TwoWay}' ");
CellETemp.Append("SelectedItem='{Binding " + dictionaryName + "[" + fieldName + "].SelectedItem, Mode=TwoWay, ValidatesOnDataErrors=True, NotifyOnValidationError=True}' />");
}
else if (theCustomField.DataType.ToUpper() == "DATEPICKER")
{
CellETemp.Append("<sdk:DatePicker ");
CellETemp.Append("Text='{Binding " + dictionaryName + "[" + fieldName + "].SelectedItem.Value, Mode=TwoWay, ValidatesOnDataErrors=True, NotifyOnValidationError=True}' />");
}
else
{
CellETemp.Append("<TextBox ");
CellETemp.Append("Text='{Binding " + dictionaryName + "[" + fieldName + "].SelectedItem.Value, Mode=TwoWay, ValidatesOnDataErrors=True, NotifyOnValidationError=True}' />");
}
CellETemp.Append("</DataTemplate>");
//newCol.CellTemplate = (DataTemplate)XamlReader.Load(CellTemp.ToString());
newCol.CellTemplate = (DataTemplate)XamlReader.Load(CellETemp.ToString());
newCol.CellEditingTemplate = (DataTemplate)XamlReader.Load(CellETemp.ToString());
return newCol;
}
THERE YOU HAVE IT. I hope it helps some of you. A working sample code is included which is coded in a better fashion than what I explained here. HAPPY CODING.