使用ReactiveUI ViewModels

时间:2016-12-29 07:12:03

标签: wpf xaml mvvm datagrid reactiveui

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

问题集

我有一个包含用户列表的DataSet。每个用户都有一个字符串Id和一组与之关联的唯一标识的数据字段,可以表示为一组字符串键值对。 DataSet中的所有用户将具有相同的字段集,但不同的DataSet可能具有不同的字段。例如,一个DataSet中的所有用户可能具有“Name”,“Age”和“Address”字段;而另一个DataSet中的用户可能有“Badge#”和“Job Title”字段。

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

我的工作原理(但不是我想要的)

我将数据分解为ReactiveUI ViewModels。 (省略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始终存在于Users中,因此我在此处添加了第一个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);
        }
    }

当添加或删除Fields时,将在DataSetViewModel中更新UserViewModel,并调用InitHeaders以重新创建列。生成的DataGridCells绑定到它们各自的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);
        }
    }

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

最终结果是DataGrid填充了正确的视图,但DataContext始终是UserViewModel而不是相应的FieldViewModel派生类型。从未设置ViewModel,并且VM未正确绑定。我不确定我还缺少什么,并希望得到任何建议或见解。

1 个答案:

答案 0 :(得分:0)

我找到了一个有效的答案,尽管它可能不是最好的答案。而不是绑定到我的视图中的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;
        });
}