【发布时间】: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