Dynamic Row and Column Creation using WPF and MVVM

*爱你&永不变心* 提交于 2020-01-05 04:41:11

问题


Note: I'm using MVVM Light Toolkit and MahApps.Metro.

I've checked the answers but it doesn't seem like any of them relate to my question.

I have a Grid whose columns and header should be dynamically created. The number and value of columns is unknown to view, and the number of rows is unknown to view.

Columns, rows and data in the rows represent a Database Table. All data is present in the ViewModel.

I have an ObservableCollection<ServerRow> ServerRows; in my ViewModel.

Server Row object is a Model that looks like this:

    public class ServerRow : ObservableObject
    {
         private ObservableCollection<ServerColumn> _columns;

         public ObservableCollection<ServerColumn> Columns
         {
             get { return _columns; }
             set { Set(() => Columns, ref _columns, value); }
         }
     }

This is a ServerColumn class :

    public class ServerColumn : ObservableObject
    {
         private string _name;
         private string _type;
         private string _value;

         public string Name
         {
             get { return _name; }
             set { Set(() => Name, ref _name, value); }
         }

         public string Type
         {
             get { return _type; }
             set { Set(() => Type, ref _type, value); }
         }

         public string Value
         {
             get { return _value; }
             set { Set(() => Value, ref _value, value); }
         }
}

The Idea was to Bind DataGrid to ObservableCollection<ServerRow> ServerRows;, and then generate the Columns depending on the ServerRow object which has ServerColumns which in turn have Name (should be a header of the column), Type as the datatype of column data, and Value as the value which should be represented in every row/column.

My XAML is pretty simple (because it's not complete, and of course- not working)

<DataGrid AutoGenerateColumns="True" ItemsSource="{Binding ServerRows}"/>

How do I write the XAML properly to achieve what I'm trying to do?

This is the result, which makes sense because Grid is trying to show a collection of objects inside a single Column and calling its ToString() method.


回答1:


I've had this problem before too.

If you look at what is done here:

https://github.com/taori/WMPR/blob/0a81bc6a6a4c6fc36edc4cbc99f0cfa8a2b8871c/src/WMPR/WMPR.Client/ViewModels/Sections/ReportEvaluationViewModel.cs#L503

You provide the iteratable collection as ObservableCollection<object> when the underlying structure is actually of type DynamicGridCell, which uses a DynamicGridCellDescriptor which can be found at

DynamicGridCell:

public class DynamicGridCell : DynamicObject, ICustomTypeDescriptor, IDictionary<string, object>
{
    private readonly Dictionary<string, object> _values = new Dictionary<string, object>();

    AttributeCollection ICustomTypeDescriptor.GetAttributes()
    {
        return new AttributeCollection();
    }

    string ICustomTypeDescriptor.GetClassName()
    {
        return nameof(DynamicGridCell);
    }

    string ICustomTypeDescriptor.GetComponentName()
    {
        return null;
    }

    TypeConverter ICustomTypeDescriptor.GetConverter()
    {
        return null;
    }

    EventDescriptor ICustomTypeDescriptor.GetDefaultEvent()
    {
        return null;
    }

    PropertyDescriptor ICustomTypeDescriptor.GetDefaultProperty()
    {
        return null;
    }

    object ICustomTypeDescriptor.GetEditor(Type editorBaseType)
    {
        return null;
    }

    EventDescriptorCollection ICustomTypeDescriptor.GetEvents()
    {
        return null;
    }

    EventDescriptorCollection ICustomTypeDescriptor.GetEvents(Attribute[] attributes)
    {
        return null;
    }

    private PropertyDescriptor[] CreatePropertyDescriptors()
    {
        var result = new List<PropertyDescriptor>();
        foreach (var pair in _values)
        {
            result.Add(new DynamicGridCellDescriptor(pair.Key));
        }

        return result.ToArray();
    }

    PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties()
    {
        var result = new PropertyDescriptorCollection(CreatePropertyDescriptors());
        return result;
    }

    PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties(Attribute[] attributes)
    {
        var result = new PropertyDescriptorCollection(CreatePropertyDescriptors());
        return result;
    }

    object ICustomTypeDescriptor.GetPropertyOwner(PropertyDescriptor pd)
    {
        return this;
    }

    public IEnumerator GetEnumerator()
    {
        return _values.GetEnumerator();
    }

    IEnumerator<KeyValuePair<string, object>> IEnumerable<KeyValuePair<string, object>>.GetEnumerator()
    {
        return _values.GetEnumerator();
    }

    void ICollection<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> item)
    {
        _values.Add(item.Key, item.Value);
    }

    void ICollection<KeyValuePair<string, object>>.Clear()
    {
        _values.Clear();
    }

    bool ICollection<KeyValuePair<string, object>>.Contains(KeyValuePair<string, object> item)
    {
        return _values.Contains(item);
    }

    void ICollection<KeyValuePair<string, object>>.CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
    {
    }

    bool ICollection<KeyValuePair<string, object>>.Remove(KeyValuePair<string, object> item)
    {
        if (_values.ContainsKey(item.Key))
        {
            _values.Remove(item.Key);
            return true;
        }

        return false;
    }

    public int Count => _values.Count;

    bool ICollection<KeyValuePair<string, object>>.IsReadOnly => false;

    public bool ContainsKey(string key)
    {
        return _values.ContainsKey(key);
    }

    public void Add(string key, object value)
    {
        _values.Add(key, value);
    }

    bool IDictionary<string, object>.Remove(string key)
    {
        return _values.Remove(key);
    }

    public bool TryGetValue(string key, out object value)
    {
        return _values.TryGetValue(key, out value);
    }

    public object this[string key]
    {
        get { return _values[key]; }
        set
        {
            if (_values.ContainsKey(key))
            {
                _values[key] = value;
            }
            else
            {
                _values.Add(key, value);
            }
        }
    }

    public ICollection<string> Keys => _values.Keys;
    public ICollection<object> Values => _values.Values;
}

DynamicGridCellDescriptor

public class DynamicGridCellDescriptor : PropertyDescriptor
    {
        public DynamicGridCellDescriptor(string name) : base(name, null)
        {
        }

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

        public override object GetValue(object component)
        {
            return ((DynamicGridCell) component)[Name];
        }

        public override void ResetValue(object component)
        {
            ((DynamicGridCell) component)[Name] = null;
        }

        public override void SetValue(object component, object value)
        {
            ((DynamicGridCell) component)[Name] = value;
        }

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

        public override Type ComponentType => typeof(DynamicGridCell);
        public override bool IsReadOnly => false;
        public override Type PropertyType => typeof(object);
    }

Just make sure that the property you bind to is of type ObservableCollection<object> anyways - otherwise for me automatic grid column generation did not work.




回答2:


You have some logical issues.

When you set the ItemsSource of a DataGrid the bound collection will be used to create rows and if you don't change it the property AutoGenerateColumns is set to true. In this case the DataGrid will generate a column for each property in the bound collection and this is exactly what is happening in your sample. You bound an instance with a property 'Columns' and get a DataGrid column which is named 'Columns'. And you get as much rows as you have entries in this property displayed as '(Collection)' because ServerColumn inherits from ObservableObject.

You can set AutoGenerateColumns to false and have to create the columns by your own; normally in xaml => hard coded.

If you really want to have dynamically generate the columns you have to write your own logic to create and bind the columns. I've done that once and it's pain in the ass if you want to have it generic. If you want a DataGrid with dynamic columns where the user can change values it's more tricky then a read only one.

One approach could be having a ObservableCollection<string> for the column names and another one ObservableCollection which stores your ViewModels for each row.




回答3:


If both rows and columns really need to be dynamic, your best choice is to use two nested ItemControls, the outer one representing rows, the inner one columns:

<ItemsControl ItemsSource="{Binding Rows}">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <ItemsControl ItemsSource="{Binding Columns}" ItemTemplateSelector="{StaticResource ColumnTemplateSelector}">
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <StackPanel Orientation="Horizontal"/>
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
            </ItemsControl>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

This allows you to display different type of columns, by defining a template selector that might look somewhat similar to the following:

public class ColumnTemplateSelector : DataTemplateSelector
{
    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        var column = item as ServerColumn;
        switch (column.Type)
        {
            case "Text":
                return TextTemplate;
            case "Number":
                return NumberTemplate;
            case "Image":
                return ImageTemplate;
        }
        // Fallback
        return TextTemplate;
    }

    public DataTemplate TextTemplate { get; set; }
    public DataTemplate NumberTemplate { get; set; }
    public DataTemplate ImageTemplate { get; set; }
}

...based on each column's type, a different template would be referenced (all of these template obviously need to be defined somewhere and referenced as StaticResource. (This even allows easy creation of changeable (not read-only) grids.)

Note that, instead of the outer ItemsControl, you can of course use ListView or any other control that is derived from ItemsControl. Using a ListView might be useful if you need automatic scrolling, for example.



来源:https://stackoverflow.com/questions/43541742/dynamic-row-and-column-creation-using-wpf-and-mvvm

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!