在C#中将复杂对象绑定到DataTable单元格

5

我有一个包含复杂对象的DataTable

例如,

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;
    }
}

现在我想将DataTable中的单元格绑定到ComplexDataWrapper对象上,所以我尝试了以下代码:

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

但是,我只想绑定一个属性,例如Name。 在网格视图中(DataTable是此视图的数据源),我想要编辑此属性(Name)。

var complexDataWrapper = row[column] as ComplexDataWrapper;

complexDataWrapper始终等于NULL。

我知道我错过了什么。

所以我的问题是:如何将我的DataTable单元格绑定到复杂对象?另外,在网格视图中,我想要编辑复杂对象的一个属性。

谢谢。希望一切都清楚明白。


那么你的“Data”包含什么?这个类是否正确地初始化了? - Chibueze Opata
另外,我假设您正在数据网格上显示此内容。您考虑直接绑定到DataGrid了吗? - Chibueze Opata
我正在使用DevExpress的BandedGridView来显示DataTable中的数据。因此,将DataTable绑定到BandedGridView。 - ZuTa
数据从哪里来?看起来你加了一个不必要的包装器。 - Ňɏssa Pøngjǣrdenlarp
你考虑过使用Entity Framework吗?我认为这可能更符合你将“复杂”类映射到数据库的愿望。 - user862319
2个回答

3
所以我的问题是:我如何将我的DataTable的单元格绑定到复杂对象?另外在网格视图中,我想要编辑复杂对象的一个属性。您需要的是能够绑定到所谓的“属性路径”(例如obj.Prop1.Prop2)的能力。不幸的是,WinForms对此的支持有限 - 它支持“简单”的数据绑定(如control.DataBindings.Add(...)),但不支持用于DataGridView控件等的“列表”数据绑定。幸运的是,通过一些编码(大多数情况下是微不足道的),仍然可以做到这一点,因为数据绑定是围绕一个抽象称为PropertyDescriptor构建的。默认情况下,它是通过反射实现的,但没有什么阻止您创建自己的实现并在其中执行任何您喜欢的操作。这使您可以做许多使用反射不可能做到的事情,特别是模拟实际不存在的“属性”。
在这里,我们将利用这种可能性创建一个“属性”,该属性实际上从原始属性的子属性获取/设置其值,而从外部看起来仍然像是单个属性,因此允许数据绑定到它:
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;
    }
}

这段代码并不小,但它只是将调用委托给属性描述符链中表示从原始属性到子属性的路径对应方法。此外,请注意许多PropertyDescriptor的方法仅在设计时使用,因此创建自定义运行时PropertyDescriptor通常只需要实现ComponentTypePropertyTypeGetValueSetValue(如果支持)。
到目前为止一切都很好。这只是谜题的第一部分。我们可以创建一个“属性”,现在我们需要一种让数据绑定使用它的方法。
为了实现这个目标,我们将利用另一个与数据绑定相关的接口,称为 ITypedList
提供了发现可绑定列表的模式的功能,其中可用于绑定的属性与要绑定到的对象的公共属性不同。
换句话说,它允许我们为列表元素提供“属性”。但是如何做到呢?如果我们正在实现数据源列表,那么很容易。但是在这里,我们希望对我们不事先知道的列表进行操作(我试图保持解决方案的通用性)。
解决方案是将原始列表包装在另一个列表中,该列表将通过委托所有调用到底层列表来实现 IList(列表数据绑定的最低要求),但通过实现 ITypedList 来控制用于绑定的属性:
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(); }
    }
}

实际上,正如您所看到的,我已经为其他数据源接口添加了包装器,比如IBindingListIBindingListView。再次强调,代码并不是很小,但只是将调用委托给底层对象(在为具体数据创建一个对象时,通常会从List<T>BiundingList<T>中继承,并仅实现两个ITypedList成员)。关键部分是GetItemProperties方法的实现,以及与propertyMapper lambda一起允许您替换一个属性为另一个属性。
有了这些准备,解决帖子中的特定问题只需要包装DataTable并将Complex属性映射到Complex.Name属性即可。
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;
    }
}

简而言之,自定义的PropertyDescriptorITypedList可以让您创建无限类型的数据视图,然后可以被任何支持数据绑定的控件使用。

2

我认为你的架构对于你想要实现的目标来说存在缺陷。

如果你正在使用gridView来编辑复杂类型中的单个属性,则没有必要将整个类型绑定到作为网格数据源的datatable中。

相反,你应该只绑定你想要编辑的属性,并且当数据返回时,只需将其分配到正确位置的复杂类型中即可。


让我来给你出个难题,昨天我偶然发现了这个帖子,希望能找到解决我的问题的答案。我有一个对象,它有一个字符串名称和一个IEnumerable集合的项。我必须使用项目列表创建列。所以图像是Thing的名称| Column1(日期作为标题)| Column2 | Column3。因此,列对应于项目的日期,然后行将是项目。你会怎么做呢?我尝试使用数据透视表,但无法按照我希望的方式工作。最终我创建了一个自定义控件来显示数据。 - Kevin B Burns
2
@KevinBBurns 从你的解释来看,似乎你应该发布自己的问题来描述你的问题。这个问题太具体了(双向绑定到单个嵌套属性),所以即使它被解决了,我怀疑解决方案也不适用于你的情况。 - Ivan Stoev
1
@KevinBBurns。我同意Ivan的观点。你的问题显然是一个独立的问题,它是一个不同的用例。 - HBomb
1
@KevinBBurns。然而,我可以为您提供一些答案。您将使用GridView,并且不会自动绑定项目。相反,您只需使用循环迭代集合属性(未定义但暗示为DateTime类型),并在每个项目上创建一个列,然后在再次循环之前进行绑定。如果您想要更详细的答案,请提出一个带有更详细信息的单独问题。 - HBomb
@KevinBBurns 我同意,你不应该需要一个中间的 DataTable。它是一种不代表你复杂类型结构的结构,所以不要使用它。 - HBomb
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接