【问题标题】:C# DataGrid AutoGenerateColumns for dynamic Object inside WrapperC# DataGrid AutoGenerateColumns 用于 Wrapper 内的动态对象
【发布时间】:2017-10-26 09:10:10
【问题描述】:

我正在尝试在 WPF 中实现某种对象选择器。到目前为止,我已经创建了一个带有 DataGrid 的窗口,其中 ItemsSource 绑定到一个 ObservableCollection。我还将 AutoGenerateColumns 设置为“true”,因为要选择的 Item 可以是任何类型的 ob 对象。 集合中的对象被包装在一个 SelectionWrapper 中,其中包含一个 IsSelected 属性,以便选择它们。

class SelectionWrapper<T> : INotifyPropertyChanged
{
    // Following Properties including PropertyChanged
    public bool IsSelected { [...] }
    public T Model { [...] }
}

我还向 DataGrid.Columns 添加了一个 CustomColumn,以便像这样绑定 IsSelected 属性

<DataGrid AutoGenerateColumns="True" ItemsSource="{Binding SourceView}">
    <DataGrid.Columns>
        <DataGridCheckBoxColumn Header="Selected" Binding="{Binding IsSelected}" />
    </DataGrid.Columns>
</DataGrid>

我用这个解决方案得到的结果不是很令人满意,因为只有我定义的列'Selected'和两个GeneratedColumns'IsSelected'和'Model'。

有没有办法改变自动生成的目标以显示模型的所有属性? 此外,有必要将 AutoGeneratedColumns 设置为只读,因为没有人应该编辑显示的条目。

无法关闭 AutoGenerateColumns 并添加更多手动列,例如

<DataGridTextColumn Binding="{Binding Model.[SomeProperty]}"/>

因为模型可以是任何类型的对象。也许有办法将 AutoGeneration 的目标路由到模型属性?

提前致谢

编辑

接受@grek40 的回答后 我想出了以下内容

首先我创建了一个继承于SelectionProperty&lt;T&gt; 的通用类SelectionProperty。这里我实现了接口ICustomTypeDescriptor,最终看起来像:

public abstract class SelectionProperty : NotificationalViewModel, ICustomTypeDescriptor
{
    bool isSelected = false;
    public bool IsSelected
    {
        get { return this.isSelected; }
        set
        {
            if (this.isSelected != value)
            {
                this.isSelected = value;
                this.OnPropertyChanged("IsSelected");
            }
        }
    }

    object model = null;
    public object Model
    {
        get { return this.model; }
        set
        {
            if (this.model != value)
            {
                this.model = value;
                this.OnPropertyChanged("Model");
            }
        }
    }

    public SelectionProperty(object model)
    {
        this.Model = model;
    }
#region ICustomTypeDescriptor
[...]
    PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties()
    {
        return TypeDescriptor.GetProperties(this.Model.GetType());
    }

    object ICustomTypeDescriptor.GetPropertyOwner(PropertyDescriptor pd)
    {
        if (pd.DisplayName == "IsSelected")
            return this;

        return this.Model;
    }
#endregion

然后我创建了一个专门的 ObservableCollection

class SelectionPropertyCollection<T> : ObservableCollection<T>, ITypedList
    where T : SelectionProperty
{
    public SelectionPropertyCollection(IEnumerable<T> collection) : base(collection)
    {

    }

    public PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors)
    {
        return TypeDescriptor.GetProperties(typeof(T).GenericTypeArguments[0]);
    }

    public string GetListName(PropertyDescriptor[] listAccessors)
    {
        return null;
    }
}

嗯,最后一件事是 ViewModel。最重要的行是

class ObjectPickerViewModel<ObjectType> : BaseViewModel
{
    public ICollectionView SourceView { get; set; }
    SelectionPropertyCollection<SelectionProperty<ObjectType>> source = null;
    public SelectionPropertyCollection<SelectionProperty<ObjectType>> Source
    {
        get { return this.source; }
        set
        {
            if (this.source != value)
            {
                this.source = value;
                this.OnPropertyChanged("Source");
            }
        }
    }
    // [...]
    this.Source = new SelectionPropertyCollection<SelectionProperty<ObjectType>>(source.Select(x => new SelectionProperty<ObjectType>(x)));
    this.SourceView = CollectionViewSource.GetDefaultView(this.Source);
}

这里的好处是,我仍然可以在 XAML 中添加更多列,而且还具有包装对象的所有公共属性!

【问题讨论】:

  • 请参阅Binding DynamicObject to a DataGrid with automatic column generation 了解可能的方法...会写一个答案,但它既不简单也不完整;)
  • 我认为这是错误的方式(也许)。问题是,SelectionWrapper 中的属性“模型”可以是任何类型,即元组、日期时间等。如果我离开包装器并直接绑定到 T 集合,所有属性都会按预期自动构建,但我错过了“IsSelected”列。

标签: c# wpf datagrid


【解决方案1】:

按照Binding DynamicObject to a DataGrid with automatic column generation? 的过程,以下应该在一定程度上起作用,但我不太确定我是否会在生产中使用类似的东西:

创建一个实现ITypedListIList 的集合。将使用来自ITypedListGetItemProperties。期望列表类型实现ICustomTypeDescriptor

public class TypedList<T> : List<T>, ITypedList, IList
    where T : ICustomTypeDescriptor
{
    public PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors)
    {
        if (this.Any())
        {
            return this[0].GetProperties();
        }
        return new PropertyDescriptorCollection(new PropertyDescriptor[0]);
    }

    public string GetListName(PropertyDescriptor[] listAccessors)
    {
        return null;
    }
}

SelectionWrapper&lt;T&gt; 实现为DynamicObject 并实现ICustomTypeDescriptor(至少是PropertyDescriptorCollection GetProperties() 方法)

public class SelectionWrapper<T> : DynamicObject, INotifyPropertyChanged, ICustomTypeDescriptor
{
    private bool _IsSelected;
    public bool IsSelected
    {
        get { return _IsSelected; }
        set { SetProperty(ref _IsSelected, value); }
    }


    private T _Model;
    public T Model
    {
        get { return _Model; }
        set { SetProperty(ref _Model, value); }
    }

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        if (Model != null)
        {
            var prop = typeof(T).GetProperty(binder.Name);
            // indexer member will need parameters... not bothering with it
            if (prop != null && prop.CanRead && prop.GetMethod != null && prop.GetMethod.GetParameters().Length == 0)
            {
                result = prop.GetValue(Model);
                return true;
            }
        }
        return base.TryGetMember(binder, out result);
    }

    public override IEnumerable<string> GetDynamicMemberNames()
    {
        // not returning the Model property here
        return typeof(T).GetProperties().Select(x => x.Name).Concat(new[] { "IsSelected" });
    }

    public PropertyDescriptorCollection GetProperties()
    {
        var props = GetDynamicMemberNames();
        return new PropertyDescriptorCollection(props.Select(x => new DynamicPropertyDescriptor(x, GetType(), typeof(T))).ToArray());
    }

    // some INotifyPropertyChanged implementation

    public event PropertyChangedEventHandler PropertyChanged;
    protected void RaisePropertyChangedEvent([CallerMemberName]string prop = null)
    {
        var handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(prop));
    }

    protected bool SetProperty<T2>(ref T2 store, T2 value, [CallerMemberName]string prop = null)
    {
        if (!object.Equals(store, value))
        {
            store = value;
            RaisePropertyChangedEvent(prop);
            return true;
        }
        return false;
    }

    // ... A long list of interface method implementations that just throw NotImplementedException for the example
}

DynamicPropertyDescriptor 破解了一种访问包装器和被包装对象属性的方法。

public class DynamicPropertyDescriptor : PropertyDescriptor
{
    private Type ObjectType;
    private PropertyInfo Property;
    public DynamicPropertyDescriptor(string name, params Type[] objectType) : base(name, null)
    {
        ObjectType = objectType[0];
        foreach (var t in objectType)
        {
            Property = t.GetProperty(name);
            if (Property != null)
            {
                break;
            }
        }
    }

    public override object GetValue(object component)
    {
        var prop = component.GetType().GetProperty(Name);
        if (prop != null)
        {
            return prop.GetValue(component);
        }
        DynamicObject obj = component as DynamicObject;
        if (obj != null)
        {
            var binder = new MyGetMemberBinder(Name);
            object value;
            obj.TryGetMember(binder, out value);
            return value;
        }
        return null;
    }

    public override void SetValue(object component, object value)
    {
        var prop = component.GetType().GetProperty(Name);
        if (prop != null)
        {
            prop.SetValue(component, value);
        }
        DynamicObject obj = component as DynamicObject;
        if (obj != null)
        {
            var binder = new MySetMemberBinder(Name);
            obj.TrySetMember(binder, value);
        }
    }

    public override Type PropertyType
    {
        get { return Property.PropertyType; }
    }

    public override bool IsReadOnly
    {
        get { return !Property.CanWrite; }
    }

    public override bool CanResetValue(object component)
    {
        return false;
    }

    public override Type ComponentType
    {
        get { return typeof(object); }
    }

    public override void ResetValue(object component)
    {
    }

    public override bool ShouldSerializeValue(object component)
    {
        return false;
    }
}

public class MyGetMemberBinder : GetMemberBinder
{
    public MyGetMemberBinder(string name)
        : base(name, false)
    {

    }
    public override DynamicMetaObject FallbackGetMember(DynamicMetaObject target, DynamicMetaObject errorSuggestion)
    {
        throw new NotImplementedException();
    }
}
public class MySetMemberBinder : SetMemberBinder
{
    public MySetMemberBinder(string name)
        : base(name, false)
    {

    }
    public override DynamicMetaObject FallbackSetMember(DynamicMetaObject target, DynamicMetaObject value, DynamicMetaObject errorSuggestion)
    {
        throw new NotImplementedException();
    }
}

现在,如果您将一些 TypedList&lt;SelectionWrapper&lt;ItemViewModel&gt;&gt; 绑定到您的数据网格项目源,它应该填充 IsSelected 的列和 ItemViewModel 的属性。

让我再说一遍 - 整个方法有点老套,我在这里的实现远非稳定。

当我再想一想时,可能没有真正需要整个 DynamicObject 的东西,只要 TypedList 用于定义列,而一些 DynamicPropertyDescriptor 用于从包装器和模型访问属性.

【讨论】:

  • 我接受了您的回答,因此我将其分解为最低限度并根据我的需要进行了调整。我将编辑我的帖子。非常感谢!
  • @Fresch 好吧,尝试让它工作很有趣,这对我来说也是一个新话题,所以我没有创建 最好的 解决方案,只是一些表明它可以做到:)
【解决方案2】:

有没有办法改变自动生成的目标以显示模型的所有属性?

简答:没有。

对于您设置为ItemsSourceIEnumerable&lt;T&gt; 类型为T 的每个公共属性,只会创建一个列。

您应该考虑将 AutoGenerateColumns 属性设置为 false 并以编程方式创建列,而不是在 XAML 标记中硬编码。

【讨论】:

  • 这可能会破坏 MVVM 模式,不是吗(我想我忘了提到我使用那个模式)?但也许我错了。一些实现思路/代码?
  • 不,为什么会这样?您正在同一视图类中创建列,不是吗? MVVM 并不是要从视图中消除代码。事实上,您可以在不使用 XAML 且不破坏 MVVM 模式的情况下以编程方式定义整个视图。
猜你喜欢
  • 1970-01-01
  • 2015-08-07
  • 2012-03-31
  • 2018-02-01
  • 1970-01-01
  • 2011-06-06
  • 1970-01-01
  • 2017-07-19
  • 1970-01-01
相关资源
最近更新 更多