【问题标题】:Binding a dynamic collection of varying objects to a WPF datagrid with ReactiveUI ViewModels使用 ReactiveUI ViewModels 将不同对象的动态集合绑定到 WPF 数据网格
【发布时间】:2017-05-13 11:48:22
【问题描述】:

我有一个奇怪的 WPF DataGrid 用例,它通过 ReactiveUI 使用 MVVM,它不完全适合我迄今为止找到的任何其他解决方案。

问题集

我有一个包含用户列表的数据集。每个用户都有一个字符串 ID 和一组与之关联的唯一标识的数据字段,这些字段可以表示为一组字符串键值对。 DataSet 中的所有用户都将具有相同的字段集,但不同的 DataSet 可能具有不同的字段。例如,一个 DataSet 中的所有用户可能有“姓名”、“年龄”和“地址”字段;而另一个数据集中的用户可能具有“徽章编号”和“职位”字段。

我想在 WPF DataGrid 中显示数据集,其中列可以动态填充。我还想向字段添加一些元数据,以识别存储在那里的数据类型,并根据该元数据在 DataGrid 单元格中显示不同的控件:纯文本字段应该使用 TextBox,图像文件路径字段应该有一个可以输入的 TextBox一个路径和一个按钮来弹出一个文件选择对话框等。

我有什么工作(但不是我想要的)

我将数据分解为 ReactiveUI ViewModel。 (为了简洁,省略 RaisePropertyChanged() 调用)

public class DataSetViewModel : ReactiveObject
{
    public ReactiveList<UserViewModel> Users { get; }
    public UserViewModel SelectedUser { get; set; }
};

public class UserViewModel : ReactiveObject
{
    public string Id { get; set; }
    public ReactiveList<FieldViewModel> Fields { get; }

    public class FieldHeader
    {
         public string Key { get; set; }
         public FieldType FType { get; set; } // either Text or Image
    }
    public ReactiveList<FieldHeader> FieldHeaders { get; }
};

public class FieldViewModel : ReactiveObject
{
    public string Value { get; set; } // already knows how to update underlying data when changed
}

我在 DataSetView 中显示所有这些。由于 Id 始终存在于用户中,因此我在此处添加了第一个 DataGridTextColumn。为了更简洁,省略了不必要的 XAML。

<UserControl x:Class="UserEditor.UI.DataSetView"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:local="clr-namespace:UserEditor.UI"
         x:Name="DataSetControl">
    <DataGrid Name="UserDataGrid"
              SelectionMode="Single" AutoGenerateColumns="False"
              HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
              DataContext="{Binding Path=ViewModel.Users, ElementName=DataSetControl}">
        <DataGrid.Columns>
            <DataGridTextColumn Header="Id" Binding="{Binding Id}" MinWidth="60" Width="SizeToCells"/>
        </DataGrid.Columns>
    </DataGrid>
</UserControl>

我在代码隐藏中创建了额外的列,省略了样板:

public partial class DataSetView : UserControl, IViewFor<DataSetViewModel>
{
    // ViewModel DependencyProperty named "ViewModel" declared here

    public DataSetView()
    {
        InitializeComponent();

        this.WhenAnyValue(_ => _.ViewModel).BindTo(this, _ => _.DataContext);
        this.OneWayBind(ViewModel, vm => vm.Users, v => v.UserDataGrid.ItemsSource);
        this.Bind(ViewModel, vm => vm.SelectedUser, v => v.UserDataGrid.SelectedItem);
    }

    // this gets called when the ViewModel is set, and when I detect fields are added or removed
    private void InitHeaders(bool firstInit)
    {
        // remove all columns except the first, which is reserved for Id
        while (UserDataGrid.Columns.Count > 1)
        {
            UserDataGrid.Columns.RemoveAt(UserDataGrid.Columns.Count - 1);
        }

        if (ViewModel == null)
            return;

        // using all DataGridTextColumns for now
        for (int i = 0; i < ViewModel.FieldHeaders.Count; i++)
        {
            DataGridColumn column;
            switch (ViewModel.FieldHeaders[i].Type)
            {
                case DataSet.UserData.Field.FieldType.Text:
                    column = new DataGridTextColumn
                    {
                        Binding = new Binding($"Fields[{i}].Value")
                    };
                    break;

                case DataSet.UserData.Field.FieldType.Image:
                    column = new DataGridTextColumn
                    {
                        Binding = new Binding($"Fields[{i}].Value")
                    };
                    break;
            }

            column.Header = ViewModel.FieldHeaders[i].Key;
            column.Width = firstInit ? DataGridLength.SizeToCells : DataGridLength.SizeToHeader;

            UserDataGrid.Columns.Add(column);
        }
    }

添加或删除字段时,DataSetViewModel 中的 UserViewModel 会更新,并调用 InitHeaders 以重新创建列。生成的 DataGridCell 绑定到它们各自的 FieldViewModel 并且一切正常。

我正在尝试做的事情(但不起作用)

我想将 FieldViewModel 分解为两个派生类,TextFieldViewModel 和 ImageFieldViewModel。每个都有各自的 TextFieldView 和 ImageFieldView 以及它们自己的 ViewModel 依赖属性。 UserViewModel 仍然包含一个 ReactiveList。我的新InitHeaders() 看起来像这样:

    private void InitHeaders(bool firstInit)
    {
        // remove all columns except the first, which is reserved for Id
        while (UserDataGrid.Columns.Count > 1)
        {
            UserDataGrid.Columns.RemoveAt(UserDataGrid.Columns.Count - 1);
        }

        if (ViewModel == null)
            return;

        for (int i = 0; i < ViewModel.FieldHeaders.Count; i++)
        {
            DataGridTemplateColumn column = new DataGridTemplateColumn();
            DataTemplate dataTemplate = new DataTemplate();
            switch (ViewModel.FieldHeaders[i].Type)
            {
                case DataSet.UserData.Field.FieldType.Text:
                    {
                        FrameworkElementFactory factory = new FrameworkElementFactory(typeof(TextFieldView));
                        factory.SetBinding(TextFieldView.ViewModelProperty, 
                            new Binding($"Fields[{i}]"));
                        dataTemplate.VisualTree = factory;
                        dataTemplate.DataType = typeof(TextFieldViewModel);
                    }
                    break;

                case DataSet.UserData.Field.FieldType.Image:
                    {
                        FrameworkElementFactory factory = new FrameworkElementFactory(typeof(ImageFieldView));
                        factory.SetBinding(ImageFieldView.ViewModelProperty, 
                            new Binding($"Fields[{i}]"));
                        dataTemplate.VisualTree = factory;
                        dataTemplate.DataType = typeof(ImageFieldViewModel);
                    }
                    break;
            }

            column.Header = ViewModel.FieldHeaders[i].Key;
            column.Width = firstInit ? DataGridLength.SizeToCells : DataGridLength.SizeToHeader;
            column.CellTemplate = dataTemplate;

            UserDataGrid.Columns.Add(column);
        }
    }

我的想法是创建一个DataGridTemplateColumn,它生成正确的视图,然后将索引的 FieldViewModel 绑定到 ViewModel 依赖属性。我还尝试将 Converter 添加到从基本 VM 转换为正确派生类型的 Bindings。

最终结果是 DataGrid 填充了正确的视图,但 DataContext 始终是 UserViewModel 而不是适当的 FieldViewModel 派生类型。 ViewModel 从未设置,VM 未正确绑定。我不确定我还缺少什么,如果有任何建议或见解,我将不胜感激。

【问题讨论】:

    标签: wpf xaml mvvm datagrid reactiveui


    【解决方案1】:

    我找到了一个可行的答案,尽管它可能不是最好的。我没有绑定到视图中的 ViewModel 属性,而是直接绑定到 DataContext:

    factory.SetBinding(DataContextProperty, new Binding($"Fields[{i}]"));
    

    在我看来,我添加了一些样板代码来监听 DataContext、设置 ViewModel 属性并执行我的 ReactiveUI 绑定:

    public TextFieldView()
    {
        InitializeComponent();
    
        this.WhenAnyValue(_ => _.DataContext)
            .Where(context => context != null)
            .Subscribe(context =>
            {
                // other binding occurs as a result of setting the ViewModel
                ViewModel = context as TextFieldViewModel;
            });
    }
    

    【讨论】:

      猜你喜欢
      • 2013-04-15
      • 2013-10-05
      • 1970-01-01
      • 1970-01-01
      • 2013-11-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多