MVVM 动态添加字段到视图
MVVM Dynamically add fields into View
我正在使用 caliburn.micro MVVM 框架开发 WPF 应用程序..
为了开发搜索屏幕,我需要根据模型属性将字段动态加载到视图中。
考虑以下视图和视图模型:
- SearchViewModel
- 搜索视图
假设 T 是下面示例中的一种产品。
public class SearchViewModel<T>
{
public T Item{get;set;}
}
public class Product
{
public int Id{get;set;}
public string Name{get;set;}
public string Description{get;set;}
}
我有一个名为 SearchView.xaml 的用户控件,上面没有任何内容。
每当加载视图时,都应将新字段添加到视图中,并且字段应绑定到属性。
根据上面的代码示例,Product class 中有 3 个 public 属性,因此应该向视图动态添加 3 个 TextBox。当用户在文本字段中输入数据时,应更新相应的 属性。
这可能吗?
任何专家都可以通过提供一些示例来帮助我实现这一目标吗?
这是一个基本示例,说明如何使用反射在控件中的 T
每个 public 属性 生成 TextBox
。
SearchView.xaml:
<Window x:Class="WpfApplication4.SearchView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication4"
mc:Ignorable="d"
Title="SearchView" Height="300" Width="300">
<StackPanel x:Name="rootPanel">
</StackPanel>
</Window>
SearchView.xaml.cs:
public partial class SearchView : UserControl
{
public SearchView()
{
InitializeComponent();
DataContextChanged += SearchView_DataContextChanged;
DataContext = new SearchViewModel<Product>();
}
private void SearchView_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue != null)
{
Type genericType = e.NewValue.GetType();
//check the DataContext was set to a SearchViewModel<T>
if (genericType.GetGenericTypeDefinition() == typeof(SearchViewModel<>))
{
//...and create a TextBox for each property of the type T
Type type = genericType.GetGenericArguments()[0];
var properties = type.GetProperties();
foreach(var property in properties)
{
TextBox textBox = new TextBox();
Binding binding = new Binding(property.Name);
if (!property.CanWrite)
binding.Mode = BindingMode.OneWay;
textBox.SetBinding(TextBox.TextProperty, binding);
rootPanel.Children.Add(textBox);
}
}
}
}
}
另一种选择显然是为每种类型的 T
创建一个 "static" 视图,并像往常一样在 XAML 标记中定义 TextBox
元素。
我建议以不同的方式解决这个问题。我不会考虑向视图/模型动态添加属性,而是考虑将有关 这些属性的 信息添加到视图模型的列表中。然后,该列表将绑定到 ItemsControl
,模板看起来像 TextBox
。
因此您的视图模型上会有一个 属性 用于您要检查的 "thing"。在这个 属性 的 setter 中,使用反射枚举你感兴趣的属性,并添加某种 FieldInfo
class(你创建的)的实例到具有绑定的属性列表。
这样做的好处是让一切都与 MVVM 兼容,并且无需使用您自己的代码动态创建控件。
下面的示例使用我自己的 MVVM 库(作为 nuget 包)而不是 caliburn.micro,但它应该足够相似以遵循基本思想。示例的完整源代码可以从 this BitBucket repo.
下载
正如您在随附的屏幕截图中所见,搜索字段是在视图中动态创建的,视图中没有任何代码。一切都在视图模型上完成。这也使您可以轻松访问用户输入的数据。
视图模型:
namespace DynamicViewExample
{
class MainWindowVm : ViewModel
{
public MainWindowVm()
{
Fields = new ObservableCollection<SearchFieldInfo>();
SearchableTypes = new ObservableCollection<Type>()
{
typeof(Models.User),
typeof(Models.Widget)
};
SearchType = SearchableTypes.First();
}
public ObservableCollection<Type> SearchableTypes { get; }
public ObservableCollection<SearchFieldInfo> Fields { get; }
private Type _searchType;
public Type SearchType
{
get { return _searchType; }
set
{
_searchType = value;
Fields.Clear();
foreach (PropertyInfo prop in _searchType.GetProperties())
{
var searchField = new SearchFieldInfo(prop.Name);
Fields.Add(searchField);
}
}
}
private ICommand _searchCommand;
public ICommand SearchCommand
{
get { return _searchCommand ?? (_searchCommand = new SimpleCommand((obj) =>
{
WindowManager.ShowMessage(String.Join(", ", Fields.Select(f => $"{f.Name}: {f.Value}")));
})); }
}
}
}
SearchFieldInfo
class:
namespace DynamicViewExample
{
public class SearchFieldInfo
{
public SearchFieldInfo(string name)
{
Name = name;
}
public string Name { get; }
public string Value { get; set; } = "";
}
}
观点:
<Window
x:Class="DynamicViewExample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:DynamicViewExample"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="MainWindow"
Width="525"
Height="350"
d:DataContext="{d:DesignInstance local:MainWindowVm}"
mc:Ignorable="d">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ComboBox
Grid.Row="0"
ItemsSource="{Binding Path=SearchableTypes}"
SelectedItem="{Binding Path=SearchType}" />
<ItemsControl Grid.Row="1" ItemsSource="{Binding Path=Fields}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Name}" />
<TextBox Width="300" Text="{Binding Path=Value}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button Grid.Row="2" Command="{Binding Path=SearchCommand}">Search</Button>
</Grid>
</Window>
型号class是:
class User
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string PhoneNumber { get; set; }
public string Id { get; set; }
}
class Widget
{
public string ModelNumber { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
我正在使用 caliburn.micro MVVM 框架开发 WPF 应用程序.. 为了开发搜索屏幕,我需要根据模型属性将字段动态加载到视图中。
考虑以下视图和视图模型:
- SearchViewModel
- 搜索视图
假设 T 是下面示例中的一种产品。
public class SearchViewModel<T>
{
public T Item{get;set;}
}
public class Product
{
public int Id{get;set;}
public string Name{get;set;}
public string Description{get;set;}
}
我有一个名为 SearchView.xaml 的用户控件,上面没有任何内容。 每当加载视图时,都应将新字段添加到视图中,并且字段应绑定到属性。
根据上面的代码示例,Product class 中有 3 个 public 属性,因此应该向视图动态添加 3 个 TextBox。当用户在文本字段中输入数据时,应更新相应的 属性。
这可能吗? 任何专家都可以通过提供一些示例来帮助我实现这一目标吗?
这是一个基本示例,说明如何使用反射在控件中的 T
每个 public 属性 生成 TextBox
。
SearchView.xaml:
<Window x:Class="WpfApplication4.SearchView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication4"
mc:Ignorable="d"
Title="SearchView" Height="300" Width="300">
<StackPanel x:Name="rootPanel">
</StackPanel>
</Window>
SearchView.xaml.cs:
public partial class SearchView : UserControl
{
public SearchView()
{
InitializeComponent();
DataContextChanged += SearchView_DataContextChanged;
DataContext = new SearchViewModel<Product>();
}
private void SearchView_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue != null)
{
Type genericType = e.NewValue.GetType();
//check the DataContext was set to a SearchViewModel<T>
if (genericType.GetGenericTypeDefinition() == typeof(SearchViewModel<>))
{
//...and create a TextBox for each property of the type T
Type type = genericType.GetGenericArguments()[0];
var properties = type.GetProperties();
foreach(var property in properties)
{
TextBox textBox = new TextBox();
Binding binding = new Binding(property.Name);
if (!property.CanWrite)
binding.Mode = BindingMode.OneWay;
textBox.SetBinding(TextBox.TextProperty, binding);
rootPanel.Children.Add(textBox);
}
}
}
}
}
另一种选择显然是为每种类型的 T
创建一个 "static" 视图,并像往常一样在 XAML 标记中定义 TextBox
元素。
我建议以不同的方式解决这个问题。我不会考虑向视图/模型动态添加属性,而是考虑将有关 这些属性的 信息添加到视图模型的列表中。然后,该列表将绑定到 ItemsControl
,模板看起来像 TextBox
。
因此您的视图模型上会有一个 属性 用于您要检查的 "thing"。在这个 属性 的 setter 中,使用反射枚举你感兴趣的属性,并添加某种 FieldInfo
class(你创建的)的实例到具有绑定的属性列表。
这样做的好处是让一切都与 MVVM 兼容,并且无需使用您自己的代码动态创建控件。
下面的示例使用我自己的 MVVM 库(作为 nuget 包)而不是 caliburn.micro,但它应该足够相似以遵循基本思想。示例的完整源代码可以从 this BitBucket repo.
下载正如您在随附的屏幕截图中所见,搜索字段是在视图中动态创建的,视图中没有任何代码。一切都在视图模型上完成。这也使您可以轻松访问用户输入的数据。
视图模型:
namespace DynamicViewExample
{
class MainWindowVm : ViewModel
{
public MainWindowVm()
{
Fields = new ObservableCollection<SearchFieldInfo>();
SearchableTypes = new ObservableCollection<Type>()
{
typeof(Models.User),
typeof(Models.Widget)
};
SearchType = SearchableTypes.First();
}
public ObservableCollection<Type> SearchableTypes { get; }
public ObservableCollection<SearchFieldInfo> Fields { get; }
private Type _searchType;
public Type SearchType
{
get { return _searchType; }
set
{
_searchType = value;
Fields.Clear();
foreach (PropertyInfo prop in _searchType.GetProperties())
{
var searchField = new SearchFieldInfo(prop.Name);
Fields.Add(searchField);
}
}
}
private ICommand _searchCommand;
public ICommand SearchCommand
{
get { return _searchCommand ?? (_searchCommand = new SimpleCommand((obj) =>
{
WindowManager.ShowMessage(String.Join(", ", Fields.Select(f => $"{f.Name}: {f.Value}")));
})); }
}
}
}
SearchFieldInfo
class:
namespace DynamicViewExample
{
public class SearchFieldInfo
{
public SearchFieldInfo(string name)
{
Name = name;
}
public string Name { get; }
public string Value { get; set; } = "";
}
}
观点:
<Window
x:Class="DynamicViewExample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:DynamicViewExample"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="MainWindow"
Width="525"
Height="350"
d:DataContext="{d:DesignInstance local:MainWindowVm}"
mc:Ignorable="d">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ComboBox
Grid.Row="0"
ItemsSource="{Binding Path=SearchableTypes}"
SelectedItem="{Binding Path=SearchType}" />
<ItemsControl Grid.Row="1" ItemsSource="{Binding Path=Fields}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Name}" />
<TextBox Width="300" Text="{Binding Path=Value}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button Grid.Row="2" Command="{Binding Path=SearchCommand}">Search</Button>
</Grid>
</Window>
型号class是:
class User
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string PhoneNumber { get; set; }
public string Id { get; set; }
}
class Widget
{
public string ModelNumber { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}