Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Bind complex object to cell of DataTable in C#

I have a DataTable with complex objects.

For example,

class ComplexDataWrapper
{
    public string Name{ get; set; }

    public ComplexData Data{ get; set; }

    public ComplexDataWrapper(ComplexData data)
    {
        this.Data = data;
        this.Name = "Something";
    }

    public override string ToString()
    {
        return Name;
    }
}

And now I want to bind cells from DataTable to objects of ComplexDataWrapper So, I try something like this :

...
var column = new DataColumn() { ColumnName = columnName, DataType = typeof(ComplexDataWrapper)};
row[column] = new ComplexDataWrapper(data);

But, I want to bind for only one property, for example, Name. And in the gridview (DataTable is a data source for this view) I want to edit this property(Name).

var complexDataWrapper = row[column] as ComplexDataWrapper;

complexDataWrapper always equals to NULL.

I know that I miss something.

So my questions : How I can bind my cell of DataTable to complex object? Plus in grid view I want to edit exactly one property of complex object.

Thanks. Hopefully, everything is clear.

like image 947
ZuTa Avatar asked Jul 09 '12 10:07

ZuTa


1 Answers

So my questions : How I can bind my cell of DataTable to complex object? Plus in grid view I want to edit exactly one property of complex object.

What you need here is the ability to bind to a so called property path (e.g. obj.Prop1.Prop2). Unfortunately WinForms has limited support for that - it's supported for simple data binding (like control.DataBindings.Add(...)) but not for list data binding which is used by DataGridView control and similar.

Fortunately it's still doable with some (most of the time trivial) coding, because the data binding is build around an abstraction called PropertyDescriptor. By default it is implemented via reflection, but nothing prevents you to create your own implementation and do whatever you like inside it. That allows you to do many things that are not possible with reflection, in particular to simulate "properties" that actually do not exist.

Here we will utilize that possibility to create a "property" that actually gets/sets its value from a child property of the original property, while from outside it still looks like a single property, thus allowing to data bind to it:

public class ChildPropertyDescriptor : PropertyDescriptor
{
    public static PropertyDescriptor Create(PropertyDescriptor sourceProperty, string childPropertyPath, string displayName = null)
    {
        var propertyNames = childPropertyPath.Split('.');
        var propertyPath = new PropertyDescriptor[1 + propertyNames.Length];
        propertyPath[0] = sourceProperty;
        for (int i = 0; i < propertyNames.Length; i++)
            propertyPath[i + 1] = propertyPath[i].GetChildProperties()[propertyNames[i]];
        return new ChildPropertyDescriptor(propertyPath, displayName);
    }
    private ChildPropertyDescriptor(PropertyDescriptor[] propertyPath, string displayName)
        : base(propertyPath[0].Name, null)
    {
        this.propertyPath = propertyPath;
        this.displayName = displayName;
    }
    private PropertyDescriptor[] propertyPath;
    private string displayName;
    private PropertyDescriptor RootProperty { get { return propertyPath[0]; } }
    private PropertyDescriptor ValueProperty { get { return propertyPath[propertyPath.Length - 1]; } }
    public override Type ComponentType { get { return RootProperty.ComponentType; } }
    public override bool IsReadOnly { get { return ValueProperty.IsReadOnly; } }
    public override Type PropertyType { get { return ValueProperty.PropertyType; } }
    public override bool CanResetValue(object component) { var target = GetTarget(component); return target != null && ValueProperty.CanResetValue(target); }
    public override object GetValue(object component) { var target = GetTarget(component); return target != null ? ValueProperty.GetValue(target) : null; }
    public override void ResetValue(object component) { ValueProperty.ResetValue(GetTarget(component)); }
    public override void SetValue(object component, object value) { ValueProperty.SetValue(GetTarget(component), value); }
    public override bool ShouldSerializeValue(object component) { var target = GetTarget(component); return target != null && ValueProperty.ShouldSerializeValue(target); }
    public override AttributeCollection Attributes { get { return ValueProperty.Attributes; } }
    public override string Category { get { return ValueProperty.Category; } }
    public override TypeConverter Converter { get { return ValueProperty.Converter; } }
    public override string Description { get { return ValueProperty.Description; } }
    public override bool IsBrowsable { get { return ValueProperty.IsBrowsable; } }
    public override bool IsLocalizable { get { return ValueProperty.IsLocalizable; } }
    public override string DisplayName { get { return displayName ?? RootProperty.DisplayName; } }
    public override object GetEditor(Type editorBaseType) { return ValueProperty.GetEditor(editorBaseType); }
    public override PropertyDescriptorCollection GetChildProperties(object instance, Attribute[] filter) { return ValueProperty.GetChildProperties(GetTarget(instance), filter); }
    public override bool SupportsChangeEvents { get { return ValueProperty.SupportsChangeEvents; } }
    public override void AddValueChanged(object component, EventHandler handler)
    {
        var target = GetTarget(component);
        if (target != null)
            ValueProperty.AddValueChanged(target, handler);
    }
    public override void RemoveValueChanged(object component, EventHandler handler)
    {
        var target = GetTarget(component);
        if (target != null)
            ValueProperty.RemoveValueChanged(target, handler);
    }
    private object GetTarget(object source)
    {
        var target = source;
        for (int i = 0; target != null && target != DBNull.Value && i < propertyPath.Length - 1; i++)
            target = propertyPath[i].GetValue(target);
        return target != DBNull.Value ? target : null;
    }
}

The code is not so small, but all it does is basically delegating the calls to the corresponding methods of the property descriptor chain representing the path from the original property to the child property. Also please note that many methods of the PropertyDescriptor are used only during the design time, so creating a custom concrete runtime property descriptor usually needs only to implement ComponentType, PropertyType, GetValue and SetValue (if supported).

So far so good. This is just the first part of the puzzle. We can create a "property", now we need a way to let data binding use it.

In order to do that, we'll utilize another data binding related interface called ITypedList:

Provides functionality to discover the schema for a bindable list, where the properties available for binding differ from the public properties of the object to bind to.

In other words, it allows us to provide "properties" for the list elements. But how? If we were implementing the data source list, it would be easy. But here we want to do that for a list that we don't know in advance (I'm trying the keep the solution generic).

The solutions is to wrap the original list in onother one that will implement IList (the minimum requirement for list data binding) by delegating all the calls to the underlying list, but by implementing ITypedList will control the properties used for binding:

public static class ListDataView
{
    public static IList Create(object dataSource, string dataMember, Func<PropertyDescriptor, PropertyDescriptor> propertyMapper)
    {
        var source = (IList)ListBindingHelper.GetList(dataSource, dataMember);
        if (source == null) return null;
        if (source is IBindingListView) return new BindingListView((IBindingListView)source, propertyMapper);
        if (source is IBindingList) return new BindingList((IBindingList)source, propertyMapper);
        return new List(source, propertyMapper);
    }

    private class List : IList, ITypedList
    {
        private readonly IList source;
        private readonly Func<PropertyDescriptor, PropertyDescriptor> propertyMapper;
        public List(IList source, Func<PropertyDescriptor, PropertyDescriptor> propertyMapper) { this.source = source; this.propertyMapper = propertyMapper; }
        // IList
        public object this[int index] { get { return source[index]; } set { source[index] = value; } }
        public int Count { get { return source.Count; } }
        public bool IsFixedSize { get { return source.IsFixedSize; } }
        public bool IsReadOnly { get { return source.IsReadOnly; } }
        public bool IsSynchronized { get { return source.IsSynchronized; } }
        public object SyncRoot { get { return source.SyncRoot; } }
        public int Add(object value) { return source.Add(value); }
        public void Clear() { source.Clear(); }
        public bool Contains(object value) { return source.Contains(value); }
        public void CopyTo(Array array, int index) { source.CopyTo(array, index); }
        public IEnumerator GetEnumerator() { return source.GetEnumerator(); }
        public int IndexOf(object value) { return source.IndexOf(value); }
        public void Insert(int index, object value) { source.Insert(index, value); }
        public void Remove(object value) { source.Remove(value); }
        public void RemoveAt(int index) { source.RemoveAt(index); }
        // ITypedList
        public string GetListName(PropertyDescriptor[] listAccessors) { return ListBindingHelper.GetListName(source, listAccessors); }
        public PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors)
        {
            var properties = ListBindingHelper.GetListItemProperties(source, listAccessors);
            if (propertyMapper != null)
                properties = new PropertyDescriptorCollection(properties.Cast<PropertyDescriptor>()
                    .Select(propertyMapper).Where(p => p != null).ToArray());
            return properties;
        }
    }

    private class BindingList : List, IBindingList
    {
        private IBindingList source;
        public BindingList(IBindingList source, Func<PropertyDescriptor, PropertyDescriptor> propertyMapper) : base(source, propertyMapper) { this.source = source; }
        private ListChangedEventHandler listChanged;
        public event ListChangedEventHandler ListChanged
        {
            add
            {
                var oldHandler = listChanged;
                if ((listChanged = oldHandler + value) != null && oldHandler == null)
                    source.ListChanged += OnListChanged;
            }
            remove
            {
                var oldHandler = listChanged;
                if ((listChanged = oldHandler - value) == null && oldHandler != null)
                    source.ListChanged -= OnListChanged;
            }
        }
        private void OnListChanged(object sender, ListChangedEventArgs e)
        {
            var handler = listChanged;
            if (handler != null)
                handler(this, e);
        }
        public bool AllowNew { get { return source.AllowNew; } }
        public bool AllowEdit { get { return source.AllowEdit; } }
        public bool AllowRemove { get { return source.AllowRemove; } }
        public bool SupportsChangeNotification { get { return source.SupportsChangeNotification; } }
        public bool SupportsSearching { get { return source.SupportsSearching; } }
        public bool SupportsSorting { get { return source.SupportsSorting; } }
        public bool IsSorted { get { return source.IsSorted; } }
        public PropertyDescriptor SortProperty { get { return source.SortProperty; } }
        public ListSortDirection SortDirection { get { return source.SortDirection; } }
        public object AddNew() { return source.AddNew(); }
        public void AddIndex(PropertyDescriptor property) { source.AddIndex(property); }
        public void ApplySort(PropertyDescriptor property, ListSortDirection direction) { source.ApplySort(property, direction); }
        public int Find(PropertyDescriptor property, object key) { return source.Find(property, key); }
        public void RemoveIndex(PropertyDescriptor property) { source.RemoveIndex(property); }
        public void RemoveSort() { source.RemoveSort(); }
    }

    private class BindingListView : BindingList, IBindingListView
    {
        private IBindingListView source;
        public BindingListView(IBindingListView source, Func<PropertyDescriptor, PropertyDescriptor> propertyMapper) : base(source, propertyMapper) { this.source = source; }
        public string Filter { get { return source.Filter; } set { source.Filter = value; } }
        public ListSortDescriptionCollection SortDescriptions { get { return source.SortDescriptions; } }
        public bool SupportsAdvancedSorting { get { return source.SupportsAdvancedSorting; } }
        public bool SupportsFiltering { get { return source.SupportsFiltering; } }
        public void ApplySort(ListSortDescriptionCollection sorts) { source.ApplySort(sorts); }
        public void RemoveFilter() { source.RemoveFilter(); }
    }
}

Actually as you can see, I've added wrappers for other data source interfaces like IBindingList and IBindingListView. Again, the code is not so small, but it's just delegating the calls to the underlying objects (when creating one for your concrete data, you usually would inherit from List<T> or BiundingList<T> and implement only the two ITypedList members). The essential part is the GetItemProperties method implementation which along with the propertyMapper lambda allows you to replace one property with another.

With all that in place, solving the specific problem from the post is simple a matter of wrapping the DataTable and mapping the Complex property to the Complex.Name property:

class ComplexData
{
    public int Value { get; set; }
}

class ComplexDataWrapper
{
    public string Name { get; set; }
    public ComplexData Data { get; set; } = new ComplexData();
}

static class Program
{
    [STAThread]
    static void Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        var form = new Form();
        var gridView = new DataGridView { Dock = DockStyle.Fill, Parent = form };
        gridView.DataSource = ListDataView.Create(GetData(), null, p =>
        {
            if (p.PropertyType == typeof(ComplexDataWrapper))
                return ChildPropertyDescriptor.Create(p, "Name", "Complex Name");
            return p;
        });
        Application.Run(form);
    }

    static DataTable GetData()
    {
        var dt = new DataTable();
        dt.Columns.Add("Id", typeof(int));
        dt.Columns.Add("Complex", typeof(ComplexDataWrapper));
        for (int i = 1; i <= 10; i++)
            dt.Rows.Add(i, new ComplexDataWrapper { Name = "Name#" + i, Data = new ComplexData { Value = i } });
        return dt;
    }
}

To recap, custom PropertyDescriptor and ITypedList allow you to create unlimited types of views of your data, which then can be used by any data bound aware control.

like image 154
Ivan Stoev Avatar answered Sep 19 '22 18:09

Ivan Stoev