wpf - 使用 ReactiveUI ViewModels 将不同对象的动态集合绑定(bind)到 WPF 数据网格

标签 wpf xaml mvvm datagrid reactiveui

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

问题集

我有一个包含用户列表的数据集。每个用户都有一个字符串 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 中更新 UserViewModels,并调用 InitHeaders 以重新创建列。生成的 DataGridCell 绑定(bind)到它们各自的 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生成正确的 View ,然后将索引的 FieldViewModel 绑定(bind)到 ViewModel 依赖项属性。我还尝试将转换器添加到从基本 VM 转换为正确派生类型的绑定(bind)。

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

最佳答案

我想出了一个可行的答案,尽管它可能不是最好的。我没有绑定(bind)到 View 中的 ViewModel 属性,而是直接绑定(bind)到 DataContext:

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

在我看来,我添加了一些样板代码来监听 DataContext,设置 ViewModel 属性,并执行我的 ReactiveUI 绑定(bind):
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;
        });
}

关于wpf - 使用 ReactiveUI ViewModels 将不同对象的动态集合绑定(bind)到 WPF 数据网格,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/41374842/

相关文章:

c# - 如何使用 1 个计时器入队并使用另一个计时器出队

c# - 调用 MediaCapture.StartPreviewAsync 时为 "InvalidOperationException: A method was called at an unexpected time"

WPF 工具包颜色选择器编辑模板现在没有可用的颜色

c# - 应用范围内的 Observable 集合

c# - MVVM 使用工厂方法处理窗口管理

wpf - Prism MVVM - 在 WPF 中的按钮单击命令上显示 View 模型中的弹出窗口

c# - WPF DateTimeUpDown 绑定(bind)问题

c# - 如何使 WPF Slider Thumb 从任意点跟随光标

c# - 重新加载 ItemsSource 时如何在 ListBox 中保留选择

c# - WPF,MVVM,导航,保持依赖注入(inject)不变