I have extended the XamDataGrid to support DynamicColumn generation using a DependencyProperty called ColumnSource. Thus the gird now will generate columns dynamically based on a dependency property called "ColumnSource". this idea was inspired from the DevExpress WPF Grid I had used before.
Having said that, I need to mention that I am using Field (not UnBoundField) inside the extended control to generate the columns and binding them to the ViewModel objects. It has worked fine for requirement I had till now.
Now I have a situation where I have a ViewModel that needs to have dynamic properties. Obviously I have ICustomTypeDescriptor in mind.I just am curious is it possible to view data in the XamDataGrid with the following limitations:
- .Net 4.0
- ICustomTypeDescriptor
- Use of field and not UnboundField class for column generations.
- Data shown should be two way bindable, that is change in cell data should change appropriate ViewModel property.
I am pasting the Extended control's code here. It is very long so I will try to curtail the code responsible for other functionalities.
public class AdvancedXamDataGrid : XamDataGrid
{
#region Static Constructor
static AdvancedXamDataGrid()
{
//Dependency properties overrides if any to be done here.
DataSourceProperty.OverrideMetadata(typeof(AdvancedXamDataGrid), new FrameworkPropertyMetadata(null, DataSourcePropetyChanged));
}
#endregion
#region Dependency Properties
/// <summary>
/// Dependency proeprty for Columns List shown in the Grid Header
/// </summary>
public static readonly DependencyProperty ColumnsSourceProperty = DependencyProperty.Register("ColumnsSource", typeof(IEnumerable),
typeof(AdvancedXamDataGrid), new FrameworkPropertyMetadata(null, OnColumnsSourceChanged));
/// <summary>
/// Gets or sets the <see cref="ColumnsSource"/>.
/// This is a Dependency Property.
/// </summary>
public IEnumerable ColumnsSource
{
get { return GetValue(ColumnsSourceProperty) as IEnumerable; }
set { SetValue(ColumnsSourceProperty, value); }
}
#endregion
#region Dependency Property Property Changed Handlers (static).
/// <summary>
/// The handler is fired when the <see cref="ColumnsSource"/> is changed.
/// </summary>
/// <param name="sender">The dependency object that raises the event.</param>
/// <param name="e">The event argument</param>
private static void OnColumnsSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var control = sender as AdvancedXamDataGrid;
if (null != control)
{
if (null != control._fieldAdornerSettings)
control.DetachAdorner();
control._fieldAdornerSettings = new FieldAdornerSettings();
control._fieldAdornerList = new List<FieldAdorner>();
var oldValue = e.OldValue as IEnumerable;
var newValue = e.NewValue as IEnumerable;
if (BindingOperations.IsDataBound(sender, ColumnsSourceProperty))
control.ColumnsSourceChanged(oldValue, newValue);
}
}
/// <summary>
/// This handler is fired when the data source property changes.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static void DataSourcePropetyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var control = sender as AdvancedXamDataGrid;
if (null != control)
{
var dataSource = e.NewValue as IEnumerable;
control.DataSource = dataSource;
}
}
#endregion
#region Instance Properties and Event Handlers
/// <summary>
/// Handles when the <see cref="ColumnsSource"/> is changed.
/// </summary>
/// <param name="oldValue"></param>
/// <param name="newValue"></param>
private void ColumnsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
if (null != oldValue)
//I could never figure out why I need this check. But this check is requred for consistent laytout for first time load.Funny I know!
FieldLayouts.Clear(); //Clear the existing columns.
var oldColSource = oldValue as INotifyCollectionChanged;
if (null != oldColSource)
{
oldColSource.CollectionChanged -= oldColSource_CollectionChanged;
//Remove the columns first.
foreach (IGridColumn column in oldValue)
{
RemoveField(column);
}
}
var newColSource = newValue as INotifyCollectionChanged;
if (null != newColSource)
{
newColSource.CollectionChanged += oldColSource_CollectionChanged;
}
if (null != newValue)
{
var fieldLayout = new FieldLayout {IsDefault = true, Key = Convert.ToString(Guid.NewGuid())};
FieldLayouts.Add(fieldLayout);
foreach (IGridColumn col in newValue)
{
AddField(col);
}
DefaultFieldLayout = fieldLayout;
}
}
/// <summary>
/// Fires when the ColumnsSource Collection changes.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void oldColSource_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
//Remove old Items.
foreach (IGridColumn col in e.OldItems)
{
RemoveField(col);
}
//Add new items.
foreach (IGridColumn col in e.NewItems)
{
AddField(col);
}
}
/// <summary>
/// Adds a Field to the wrapped grids FiledCollection.
/// </summary>
/// <param name="column"></param>
private void AddField(IGridColumn column)
{
if (FieldLayouts.Count > 0)
{
var fieldLayout = FieldLayouts[0];
var field = new Field {Name = column.Name, Label = column.DisplayName.ToUpper(), ToolTip = column.ToolTip};
switch (column.ColumnType)
{
// case GridColumnType.Text:
// field.DataType = typeof(string);
// break;
case GridColumnType.Boolean:
var style = new Style(typeof (XamCheckEditor));
style.Setters.Add(new Setter(XamCheckEditor.IsCheckedProperty,
new Binding()
{
Path = new PropertyPath(string.Concat("DataItem.", column.Name)),
UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
Mode = BindingMode.TwoWay
}));
field.Settings.EditorType = typeof (XamCheckEditor);
field.Settings.EditorStyle = style;
break;
}
if (column.ColumnType == GridColumnType.Combo)
{
var style = new Style(typeof (XamComboEditor));
style.Setters.Add(new Setter(XamComboEditor.ItemsSourceProperty,
new Binding() {Path = new PropertyPath(column.ItemsSource)}));
style.Setters.Add(new Setter(XamComboEditor.SelectedItemProperty,
new Binding(column.SelectedItemPropertyName) {Mode = BindingMode.TwoWay}));
style.Setters.Add(new Setter(XamComboEditor.DisplayMemberPathProperty, column.DisplayMemberPath));
style.Setters.Add(new Setter(XamComboEditor.ValuePathProperty, column.ValueMemberPath));
field.Settings.EditorType = typeof (XamComboEditor);
field.Settings.EditorStyle = style;
}
if (column.IsReadOnly)
field.Settings.AllowEdit = false;
if (!column.IsVisible)
field.Visibility = Visibility.Collapsed;
fieldLayout.Fields.Add(field);
if (!string.IsNullOrEmpty(column.TemplateKey))
_fieldAdornerList.Add(new FieldAdorner()
{
Name = column.Name,
BindToParentSource = column.BindToParent,
TemplateKey = column.TemplateKey
});
//Register to the property changed notofication.
var propertyNotifier = column as INotifyPropertyChanged;
propertyNotifier.PropertyChanged += propertyNotifier_PropertyChanged;
}
}
/// <summary>
/// Removes a field
/// </summary>
/// <param name="column"></param>
private void RemoveField(IGridColumn column)
{
if (FieldLayouts.Count > 0)
{
var fieldLayout = FieldLayouts[0];
var field = fieldLayout.Fields.FirstOrDefault(f => f.Name.Equals(column.Name));
if (null != field)
fieldLayout.Fields.Remove(field);
var propertyNotifier = column as INotifyPropertyChanged;
propertyNotifier.PropertyChanged -= propertyNotifier_PropertyChanged;
}
}
/// <summary>
/// Event handler for handling property notification.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void propertyNotifier_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
var column = sender as IGridColumn;
if (null != column)
{
var fieldLayout = FieldLayouts[0];
var field = fieldLayout.Fields.FirstOrDefault(f => f.Name.Equals(column.Name));
if (e.PropertyName.Equals("IsVisible"))
{
if (field != null)
field.Visibility = column.IsVisible ? Visibility.Visible : Visibility.Collapsed;
}
if (e.PropertyName.Equals("IsReadOnly"))
{
if (field != null)
field.Settings.AllowEdit = !column.IsReadOnly;
}
}
}
#endregion
}
Here is the IGridColumn contract:
/// <summary>
/// A contract that need to be implemented by an item that needs to participate in ColumnSource binding.
/// </summary>
public interface IGridColumn : INotifyPropertyChanged
{
/// <summary>
/// Gets or sets the PropertyName to which the Column would bind.
/// </summary>
string Name { get; set; }
/// <summary>
/// Gets or sets the Display Text that will be visible in the column header.
/// </summary>
string DisplayName { get; set; }
/// <summary>
/// Gets the type of the property that gets bound to this column.
/// </summary>
GridColumnType ColumnType { get; }
/// <summary>
/// Gets or sets if the column is read-only.
/// </summary>
bool IsReadOnly { get; set; }
/// <summary>
/// Gets or sets if the column is visible.
/// </summary>
bool IsVisible { get; set; }
#region For Combo Columns
/// <summary>
/// Gets or sets the Items source of the combo editor.
/// </summary>
string ItemsSource { get; set; }
/// <summary>
/// Gets or sets the SelectedItem propertyName.
/// </summary>
string SelectedItemPropertyName { get; set; }
/// <summary>
/// Gets or sets the name of the property that be the display item of the combo.
/// </summary>
string DisplayMemberPath { get; set; }
/// <summary>
/// Gets or sets the name of the property that be the value item of the combo.
/// </summary>
string ValueMemberPath { get; set; }
/// <summary>
/// Gets or sets the tool tip on the column.
/// </summary>
string ToolTip { get; set; }
/// <summary>
/// Gets or sets the Template Key for the adorner.
/// </summary>
string TemplateKey { get; set; }
/// <summary>
/// Gets or sets if the smart tag, would be bound to the view model of the grid.
/// <remarks>
/// Note: By default it would be bound to an item of the grid.
/// </remarks>
/// </summary>
bool BindToParent { get; set; }
/// <summary>
/// Gets or sets the caption for the smart tag.
/// </summary>
string SmartTagCaption { get; set; }
#endregion
}
/// <summary>
/// An enumeration offering various types of Grid Column types.
/// </summary>
public enum GridColumnType
{
Text=0,
Boolean,
Integer,
Double,
Decimal,
Combo
} ;
I had the plan to populating the ColumnSource for the Grid and bind them to ICustomTypeDescriptor instances of ViewModels whose Dynamic property names would match with the IGridColumn Names.