I am using EF to return a List of Processes, with one to many flags. Flags are unique, they may increase or decrease depending on requirements. The data structure roughly translates to:
public enum FlagTypes
{
OnlyOnWeekends,
OnlyOnHolidays
}
public class Process
{
public DateTime Date { get; set; }
public String Description { get; set; }
public Dictionary<FlagTypes, Flag> Flags { get; set; }
}
public class Flag
{
public FlagTypes Type { get; set; }
public bool Enabled { get; set; }
}
I would like to display this in a DataGridView like so:
Date | Description | OnlyOnWeekends | OnlyOnHolidays [|... more flags as needed]
.. while also making it editable.
I was able to work around the limits of DataGridView to display the table using a custom column and cell
public class EnumerationColumn : DataGridViewColumn
{
public FlagTypes EnumerationType { get; set; }
public EnumerationColumn(FlagTypes enumerationType)
: base(new EnumerationCell())
{
EnumerationType = enumerationType;
}
public override DataGridViewCell CellTemplate
{
get
{
return base.CellTemplate;
}
set
{
// Ensure that the cell used for the template is a EnumerationCell.
if (value != null &&
!value.GetType().IsAssignableFrom(typeof(EnumerationCell)))
{
throw new InvalidCastException("Must be a EnumerationCell");
}
base.CellTemplate = value;
}
}
public class EnumerationCell : DataGridViewCheckBoxCell
{
private EnumerationColumn Parent
{
get
{
var parent = base.OwningColumn as EnumerationColumn;
if(parent == null)
{
throw new NullReferenceException("EnumerationCell must belong to a EnumerationColumn");
}
return parent;
}
}
private Dictionary<FlagTypes, Flag> GetFlags(int rowIndex)
{
var flags = base.GetValue(rowIndex) as Dictionary<FlagTypes, Flag>;
return flags ?? new Dictionary<FlagTypes, Flag>();
}
protected override object GetValue(int rowIndex)
{
var flags = GetFlags(rowIndex);
if (flags.ContainsKey(Parent.EnumerationType))
{
return flags[Parent.EnumerationType].Enabled;
}
return false;
}
}
}
And creating the columns
grid.AutoGenerateColumns = false;
grid.DataSource = processes; // List<Process>
var dateCol = new CalendarWidgetColumn();
dateCol.DataPropertyName = "Date";
dateCol.HeaderText = "Date";
var descCol = new DataGridViewTextBoxColumn();
descCol.DataPropertyName = "Description";
descCol.HeaderText = "Description";
grid.Columns.Add(dateCol);
grid.Columns.Add(descCol);
foreach(string name in Enum.GetNames(typeof(FlagTypes)))
{
FlagTypes flag;
if(FlagTypes.TryParse(name, out flag))
{
var enumCol = new EnumerationColumn(flag);
enumCol.DataPropertyName = "Flags";
enumCol.HeaderText = String.Format("{0}?", name);
grid.Columns.Add(enumCol);
}
}
I cannot figure out how to intercept the call to save to the DataSource, so it is throwing an Exception trying to set a bool (the checkbox value) to a Dictionary (the Flags DataProperty). I have looked at the CellValuePushed event, but that fires after the fact. Any ideas?
Or perhaps an easier way to approach this all together?
(Eventually I want to wrap the processes list with a BindingList so I can also create new rows directly from the DataGridView)
Solution (as suggested by @Erez Robinson below)
Step 1: Modify DataStructure to allow easy access to underlying dictionary
public enum FlagType
{
OnlyOnWeekends,
OnlyOnHolidays
}
public class Process
{
public DateTime Date { get; set; }
public String Description { get; set; }
public Dictionary<FlagType, Flag> Flags { get; set; }
public bool this[FlagType flagType]
{
get
{
if(!Flags.ContainsKey(flagType))
{
Flags.Add(flagType, new Flag(flagType, false));
}
return Flags[flagType].Enabled;
}
set
{
Flags[flagType].Enabled = value;
}
}
}
public class Flag
{
public FlagType Type { get; set; }
public bool Enabled { get; set; }
public Flag(FlagType flagType, bool enabled)
{
Type = flagType;
Enabled = enabled;
}
}
Step 2: Create a container that derives from ITypedList
class ProcessCollection : List<Process>, ITypedList
{
protected IProcessViewBuilder _viewBuilder;
public ProcessCollection(IProcessViewBuilder viewBuilder)
{
_viewBuilder = viewBuilder;
}
#region ITypedList Members
protected PropertyDescriptorCollection _props;
public PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors)
{
if (_props == null)
{
_props = _viewBuilder.GetView();
}
return _props;
}
public string GetListName(PropertyDescriptor[] listAccessors)
{
return ""; // was used by 1.1 datagrid
}
#endregion
}
Step 3: Move dynamic property creation to a ViewBuilder
public interface IProcessViewBuilder
{
PropertyDescriptorCollection GetView();
}
public class ProcessFlagView : IProcessViewBuilder
{
public PropertyDescriptorCollection GetView()
{
List<PropertyDescriptor> props = new List<PropertyDescriptor>();
props.Add(new DynamicPropertyDescriptor(
typeof(Process),
"Date",
typeof(DateTime),
delegate(object p)
{
return ((Process)p).Date;
},
delegate(object p, object newPropVal)
{
((Process)p).Date = (DateTime)newPropVal;
}
));
props.Add(new DynamicPropertyDescriptor(
typeof(Process),
"Description",
typeof(string),
delegate(object p)
{
return ((Process)p).Description;
},
delegate(object p, object newPropVal)
{
((Process) p).Description = (string) newPropVal;
}
));
foreach (string name in Enum.GetNames(typeof(FlagType)))
{
FlagType flag;
if (FlagType.TryParse(name, out flag))
{
props.Add(new DynamicPropertyDescriptor(
typeof (Process),
name,
typeof (bool),
delegate(object p)
{
return ((Process) p)[flag];
},
delegate(object p, object newPropVal)
{
((Process) p)[flag] = (bool) newPropVal;
}
));
}
}
PropertyDescriptor[] propArray = new PropertyDescriptor[props.Count];
props.CopyTo(propArray);
return new PropertyDescriptorCollection(propArray);
}
}
Step 4: Bind the collection to the DataGridView
ProcessCollection processes = new ProcessCollection(new ProcessFlagView());
// Add some dummy data ...
processes.Add( ... );
// If you want a custom DataGridViewColumn, bind it before you bind the DataSource
var dateCol = new CalendarColumn();
dateCol.DataPropertyName = "Date";
dateCol.HeaderText = "Date";
grid.Columns.Add(dateCol);
grid.DataSource = processes;
You can derive from DataGridView and shadow the DataSource property with the
newkeyword.But I think that you need to change your concept.
I would not touch the datagridview.
Exposes these Flags as seperate Properties. Add these classes to a binding list.
You can dynamicaly create properties using reflection.
Please read this article, it will point you in the right direction: ITypedList