对于使用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未正确绑定。我不确定我还缺少什么,并希望得到任何建议或见解。
答案 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;
});
}