Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
686 views
in Technique[技术] by (71.8m points)

wpf - C# DataGrid AutoGenerateColumns for dynamic Object inside Wrapper

I'm trying to implement some kind of Object Picker in WPF. So far I've created the Window with a DataGrid which ItemsSource is bound to an ObservableCollection. I also set AutoGenerateColumns to 'true' due to the fact that the Item to be picked can be any kind ob object. The Objects inside the Collection are wrapped in a SelectionWrapper< T> which contains an IsSelected Property in order to select them.

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

I also added a CustomColumn to the DataGrid.Columns in order to bind the IsSelected Property like so

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

The result I get with this solution is not very satisfying because there is just my defined Column 'Selected' and two GeneratedColumns 'IsSelected' and 'Model'.

Is there a way to change the target for the AutoGeneration to display all Properties of Model instead? Also it is necessary to make the AutoGeneratedColumns ReadOnly because no one should edit the displayed Entries.

It is no option to turn off AutoGenerateColumns and add some more manual Columns like

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

because the Model can be of any kind of Object. Maybe there is a way to route the target for AutoGeneration to the Model Property?

Thanks in Advance

Edit

After accepting the Answer of @grek40 I came up with the following

First I created a general class of SelectionProperty which is inherited in SelectionProperty<T>. Here I implement the Interface ICustomTypeDescriptor which finally looked like:

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

Then I created a specialized 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;
    }
}

Well and the last thing is the ViewModel. The most significant lines are

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

The good thing here is, that I can still add more Columns in the XAML but also have all public Properties of the Wrapped Object!

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Following the course of Binding DynamicObject to a DataGrid with automatic column generation?, the following should work to some extent but I'm not quite sure if I would ever use something like it in production:

Create a collection that implements ITypedList and IList. GetItemProperties from ITypedList will be used. Expect the list type to implement 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;
    }
}

Implement SelectionWrapper<T> as DynamicObject and implement ICustomTypeDescriptor (at least the PropertyDescriptorCollection GetProperties() method)

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
}

The DynamicPropertyDescriptor hacks a way to access the properties of the wrapper and the wrapped object.

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

Now if you bind some TypedList<SelectionWrapper<ItemViewModel>> to your datagrid itemssource, it should populate the columns for IsSelected and for the properties of ItemViewModel.

Let me say it again - the whole approach is a bit hacky and my implementation here is far from being stable.

As I think about it some more, there is probably no real need for the whole DynamicObject stuff as long as the TypedList is used to define the columns and some DynamicPropertyDescriptor to access the properties from wrapper and model.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...