【问题标题】:Dynamic Row and Column Creation using WPF and MVVM使用 WPF 和 MVVM 动态创建行和列
【发布时间】:2017-09-18 09:34:52
【问题描述】:

注意:我正在使用 MVVM Light ToolkitMahApps.Metro

我已经检查了答案,但似乎其中任何一个都与我的问题无关。

我有一个 Grid,它的列和标题应该是动态创建的。列数和值无法查看,行数无法查看。

列、行和行中的数据代表一个数据库表。所有数据都存在于 ViewModel 中。

我的 ViewModel 中有一个 ObservableCollection<ServerRow> ServerRows;

Server Row 对象是一个如下所示的模型:

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

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

这是一个ServerColumn 类:

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

想法是将DataGrid绑定到ObservableCollection&lt;ServerRow&gt; ServerRows;,然后根据ServerRow对象生成列,该对象具有ServerColumns,而Name(应该是列的标题),@987654331 @ 为列数据的数据类型,Value 为每行/列应表示的值。

我的 XAML 非常简单(因为它不完整,当然 - 不工作)

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

如何正确编写 XAML 以实现我想要做的事情?

这是结果,这是有道理的,因为 Grid 试图在单个 Column 中显示对象集合并调用其 ToString() 方法。

【问题讨论】:

  • 您期望什么输出,即您希望为每个 ServerRow 显示多少列?这些列是在哪里定义的?

标签: wpf xaml mvvm data-binding mvvm-light


【解决方案1】:

我以前也遇到过这个问题。

如果你看看这里做了什么:

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

当底层结构实际上是 DynamicGridCell 类型时,您将可迭代集合提供为 ObservableCollection&lt;object&gt;,它使用可在

找到的 DynamicGridCellDescriptor

动态网格单元:

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

动态网格单元描述符

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

只要确保您绑定到的属性是 ObservableCollection&lt;object&gt; 类型 - 否则对我来说自动网格列生成不起作用。

【讨论】:

  • 我试过查看代码,但我不确定我是否理解发生了什么。您有一个名为 DynamicGridCell 的自定义控件?我不明白如何使用它。我已经绑定了一个 ObservableCollection,我可以将它的访问类型更改为对象。
  • 它不是自定义控件。仅仅是一个自定义结构,允许您定义无限数量的列。像 cell["column1"] = "value1";单元格["column2"] = "value2";每个索引器都会生成一个列,并将在您的网格中呈现。您创建的每个单元格对象都会变成一行
  • 抱歉没有回答,但周末有很多工作。是的,这就是我的答案!非常感谢!但是,我仍然不明白它是如何工作的,这让我很烦恼。您是否有机会分享一些您想出如何获得解决方案的资源?将不胜感激:)
  • 老实说,如果我没记错的话,我正在检查一个 JsonObject,并且想知道为什么我可以像表格或其他东西一样在调试器中查看它。所以我查找了源代码并在此基础上构建了我的课程。仍然让我感到困惑的是,如果您绑定的属性是 ObservableCollection,它就不起作用。我有点困惑,似乎没有一个像这样工作的内置类。我想这是一个并不少见的场景。
  • 我的想法完全正确。它确实看起来并不罕见,但没有支持它。无论如何,再次感谢您的回答。 :)
【解决方案2】:

如果两行列确实需要动态,你最好的选择是使用两个嵌套的ItemControls,外层代表行,内层代表列:

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

这允许您通过定义一个可能类似于以下内容的模板选择器来显示不同类型的列:

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

...根据每列的类型,将引用不同的模板(所有这些模板显然都需要在某处定义并引用为StaticResource。(这甚至允许轻松创建可更改的(非只读)网格。)

请注意,您当然可以使用ListView 或派生自ItemsControl 的任何其他控件来代替外部ItemsControl。例如,如果您需要自动滚动,使用 ListView 可能会很有用。

【讨论】:

    【解决方案3】:

    你有一些逻辑问题。

    当您设置DataGridItemsSource 时,绑定集合将用于创建行,如果您不更改它,属性AutoGenerateColumns 将设置为true。在这种情况下,DataGrid 将为绑定集合中的每个属性生成一个列,这正是您的示例中发生的情况。 您使用属性“列”绑定了一个实例,并获得了一个名为“列”的 DataGrid 列。并且您获得的行数与此属性中显示为“(集合)”的条目一样多,因为 ServerColumn 继承自 ObservableObject

    您可以将AutoGenerateColumns设置为false,并且必须自己创建列;通常在 xaml => 硬编码中。

    如果您真的想动态生成列,您必须编写自己的逻辑来创建和绑定列。我做过一次,如果你想让它通用,那就太痛苦了。 如果您想要一个带有动态列的DataGrid,用户可以在其中更改值,那么它比只读的更棘手。

    一种方法可能是使用ObservableCollection&lt;string&gt; 作为列名,另一种方法是使用ObservableCollection 存储每一行​​的 ViewModel。

    【讨论】:

    • 用户不会改变任何东西,网格只是为了表示一个结果(它是只读的)。此外,如果它可以减轻我的设计工作,我不会回避后面的代码行。但它必须保持动态。
    • 就像我说的:如果你想让它动态化,你必须自己添加列。例如在 Behavior 或 AttachedProperty 中。您需要 DataGrid 的实例来添加列及其绑定。编辑我的答案给你一个想法。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-09-14
    • 2011-07-13
    • 1970-01-01
    • 2021-10-06
    • 1970-01-01
    • 2011-03-05
    • 1970-01-01
    相关资源
    最近更新 更多