UWP、MVVM 和级联 ListView
UWP, MVVM and cascade ListView
我对 UWP 和两个 ListView 越来越着迷。场景很简单:我在一个页面中有两个列表视图(listview 和 listViewField)。
我有一个像这样的 ViewModel:
public class FormViewModel : BaseViewModel {
public ObservableCollection<TableInfo> Tables;
}
TableInfo 具有以下定义:
public class TableInfo {
/// <summary>
/// Gets or sets the name.
/// </summary>
/// <value>The name.</value>
public string Name { get; set; }
/// <summary>
/// Gets or sets the fields.
/// </summary>
/// <value>The fields.</value>
public List<FieldInfo> Fields { get; set; }
}
最后我想要这样的东西:
listview 显示表中的所有 Name(第 n.1 点)。当用户单击 listview 中的项目时,listviewField 显示该项目的字段列表(第 n.2 点)。如果我想更改或添加新字段,用户可以使用表单(第 n.3 点)。
<ListView x:Name="listView" ItemsSource="{x:Bind vm.Tables, Mode=OneWay}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="mdl:TableInfo">
<TextBlock Text="{x:Bind Name}" FontSize="25" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<ListView x:Name="listViewTable"
ItemsSource="{Binding ElementName=listView, Path=SelectedItem.Fields}"
SelectedIndex="{x:Bind vm.SelectedTableInfoIndex, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="mdl:FieldInfo">
<TextBlock Text="{x:Bind Name}" FontSize="25" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
如果我在 Tables 的开头插入一些项目,我可以在第一个列表视图中看到列表,并且(神奇地)如果我点击一个项目,我可以看到所有第二个列表视图中的字段。但是我尝试从表单中添加一个字段,但没有任何效果。
您对此有什么建议或好的例子吗?提前谢谢你。
更新
在@ibebbs 的帮助下,我更改了 listview,您可以在下面找到我的 BaseViewModel
。我尝试以不同的方式在 viewmodel 中添加一些 TableInfo
和 FieldInfo
,但第一个列表视图没有显示任何内容。
如果我在 viewmodel 初始化后立即启动应用程序时添加了一些 TableInfo
,列表视图将按我预期的方式工作。
我添加了一个函数来添加一个新的TableInfo
public void AddTableInfo(TableInfo tbl) {
Tables.Add(tbl);
RaisePropertyChanged("Tables");
}
第二次更新
正如@MZetko 所建议的,我在绑定中添加了 x:Bind vm.Tables, Mode=OneWay
,当我添加新项目时列表视图会更新。
最后一个问题是如何将所选项目与表单连接(第 n.3 点)。
public int SelectedTableInfoIndex
{
get { return _selectedTableInfoIndex; }
set {
_selectedTableInfoIndex = value;
if (_selectedTableInfoIndex >= 0) {
_selectedTableInfo = Tables[_selectedTableInfoIndex];
}
}
}
private int _selectedTableInfoIndex = -1;
public TableInfo SelectedTableInfo
{
get {
return (_selectedTableInfoIndex >= 0) ? Tables[_selectedTableInfoIndex] : null;
}
set {
_selectedTableInfo = value;
RaisePropertyChanged("SelectedTableInfo");
}
}
private TableInfo _selectedTableInfo;
然后我的表单数据应该来自我的模型列表视图。
<StackPanel x:Name="stackTable" Orientation="Vertical" Padding="10">
<TextBlock>Table name</TextBlock>
<TextBox x:Name="textboxTableName"
Text="{Binding ElementName=listView, Path=SelectedItem.Name}" />
<TextBlock>Project namespace</TextBlock>
<TextBox x:Name="textboxNameSpace"
Text="{Binding ElementName=listView, Path=SelectedItem.ProjectNameSpace}" />
</StackPanel>
这段代码显示了表单中的项目,一个用于table,另一个用于字段。现在我的问题是:如果我想添加一条新记录或删除一条记录,我该如何更改?
我环顾四周,发现了那些 link
- https://github.com/johnshew/Minimal-UWP-MVVM-CRUD/tree/master/Simple%20MVVM%20UWP%20with%20CRUD
- https://blogs.msdn.microsoft.com/johnshews_blog/2015/09/09/a-minimal-mvvm-uwp-app/
但我不清楚如何在我的案例中使用它。
提前感谢您的帮助!
您的第二个 ListView 正在绑定到 List<FieldInfo>
类型的 TableInfo 的字段 属性。列表不支持数据绑定依赖于更新 UI.
的更改通知
将 List<FieldInfo>
更改为 ObservableCollection<FieldInfo>
,您应该会看到更好的结果。
顺便说一下,我建议只在第二个 ListView 中绑定 ItemsSource,而不是绑定 DataContext 和 ItemsSource(这需要两次刷新 UI)。它应该像这样简单:
<ListView x:Name="listView" ItemsSource="{x:Bind vm.Tables}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="mdl:TableInfo">
<TextBlock Text="{x:Bind Name}" FontSize="25" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<ListView x:Name="listViewTable"
ItemsSource="{Binding ElementName=listView, Path=SelectedItem.Fields}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="mdl:FieldInfo">
<TextBlock Text="{x:Bind Name}" FontSize="25" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
希望对您有所帮助。
如果我理解正确的话,问题是当添加新字段时第二个列表视图没有更新。
问题在于,字段列表的类型是List<FieldInfo>
。 UWP 中的数据绑定基于 INotifyPropertyChanged
和 INotifyCollectionChanged
接口。 List
也没有实现,因此无法自动通知用户界面它已更改。为此,您需要将其替换为 ObservableCollection<FieldInfo>
。当您将项目添加到此集合时,UI 将自动更新为新项目。
不幸的是,这仍然不足以更新字段。 ObservableCollection
class 可以观察到Add
或Remove
项的变化,但不能观察到其中单个项的变化。
为了能够做到这一点,您必须确保 FieldInfo
class 实现 INotifyPropertyChanged
并且 PropertyChanged
事件在您想要的 属性 时被调用观察变化。
最常见的接口实现如下所示:
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
现在在所有属性中你需要调用NotifyPropertyChanged
方法:
private string _description = "";
public string Description
{
get
{
return _description;
}
set
{
_description = value;
NotifyPropertyChanged();
}
}
因为[CallerMemberAttribute]
你不必指定propertyName
参数,它会被编译器自动填充。
如果有人需要,我会发布我的BaseViewModel
public class BaseViewModel : INotifyPropertyChanged {
#region Property changed
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Notifies the property changed.
/// </summary>
/// <param name="propertyName">Name of the property.</param>
protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "") {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected void NotifyPropertyChanged(string propertyName, Action<bool> message) {
if (this.PropertyChanged != null) {
// property changed
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
message?.Invoke(this.IsValid);
}
}
/// <summary>
/// Raises the property changed.
/// </summary>
/// <param name="property">The property.</param>
protected void RaisePropertyChanged(string property) {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
// SetField(()=> somewhere.Name = value; somewhere.Name, value)
// Advanced case where you rely on another property
protected bool SetProperty<T>(T currentValue, T newValue, Action DoSet,
[CallerMemberName] String property = null) {
if (EqualityComparer<T>.Default.Equals(currentValue, newValue)) return false;
DoSet.Invoke();
RaisePropertyChanged(property);
return true;
}
#endregion
}
我对 UWP 和两个 ListView 越来越着迷。场景很简单:我在一个页面中有两个列表视图(listview 和 listViewField)。
我有一个像这样的 ViewModel:
public class FormViewModel : BaseViewModel {
public ObservableCollection<TableInfo> Tables;
}
TableInfo 具有以下定义:
public class TableInfo {
/// <summary>
/// Gets or sets the name.
/// </summary>
/// <value>The name.</value>
public string Name { get; set; }
/// <summary>
/// Gets or sets the fields.
/// </summary>
/// <value>The fields.</value>
public List<FieldInfo> Fields { get; set; }
}
最后我想要这样的东西:
listview 显示表中的所有 Name(第 n.1 点)。当用户单击 listview 中的项目时,listviewField 显示该项目的字段列表(第 n.2 点)。如果我想更改或添加新字段,用户可以使用表单(第 n.3 点)。
<ListView x:Name="listView" ItemsSource="{x:Bind vm.Tables, Mode=OneWay}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="mdl:TableInfo">
<TextBlock Text="{x:Bind Name}" FontSize="25" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<ListView x:Name="listViewTable"
ItemsSource="{Binding ElementName=listView, Path=SelectedItem.Fields}"
SelectedIndex="{x:Bind vm.SelectedTableInfoIndex, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="mdl:FieldInfo">
<TextBlock Text="{x:Bind Name}" FontSize="25" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
如果我在 Tables 的开头插入一些项目,我可以在第一个列表视图中看到列表,并且(神奇地)如果我点击一个项目,我可以看到所有第二个列表视图中的字段。但是我尝试从表单中添加一个字段,但没有任何效果。
您对此有什么建议或好的例子吗?提前谢谢你。
更新
在@ibebbs 的帮助下,我更改了 listview,您可以在下面找到我的 BaseViewModel
。我尝试以不同的方式在 viewmodel 中添加一些 TableInfo
和 FieldInfo
,但第一个列表视图没有显示任何内容。
如果我在 viewmodel 初始化后立即启动应用程序时添加了一些 TableInfo
,列表视图将按我预期的方式工作。
我添加了一个函数来添加一个新的TableInfo
public void AddTableInfo(TableInfo tbl) {
Tables.Add(tbl);
RaisePropertyChanged("Tables");
}
第二次更新
正如@MZetko 所建议的,我在绑定中添加了 x:Bind vm.Tables, Mode=OneWay
,当我添加新项目时列表视图会更新。
最后一个问题是如何将所选项目与表单连接(第 n.3 点)。
public int SelectedTableInfoIndex
{
get { return _selectedTableInfoIndex; }
set {
_selectedTableInfoIndex = value;
if (_selectedTableInfoIndex >= 0) {
_selectedTableInfo = Tables[_selectedTableInfoIndex];
}
}
}
private int _selectedTableInfoIndex = -1;
public TableInfo SelectedTableInfo
{
get {
return (_selectedTableInfoIndex >= 0) ? Tables[_selectedTableInfoIndex] : null;
}
set {
_selectedTableInfo = value;
RaisePropertyChanged("SelectedTableInfo");
}
}
private TableInfo _selectedTableInfo;
然后我的表单数据应该来自我的模型列表视图。
<StackPanel x:Name="stackTable" Orientation="Vertical" Padding="10">
<TextBlock>Table name</TextBlock>
<TextBox x:Name="textboxTableName"
Text="{Binding ElementName=listView, Path=SelectedItem.Name}" />
<TextBlock>Project namespace</TextBlock>
<TextBox x:Name="textboxNameSpace"
Text="{Binding ElementName=listView, Path=SelectedItem.ProjectNameSpace}" />
</StackPanel>
这段代码显示了表单中的项目,一个用于table,另一个用于字段。现在我的问题是:如果我想添加一条新记录或删除一条记录,我该如何更改?
我环顾四周,发现了那些 link
- https://github.com/johnshew/Minimal-UWP-MVVM-CRUD/tree/master/Simple%20MVVM%20UWP%20with%20CRUD
- https://blogs.msdn.microsoft.com/johnshews_blog/2015/09/09/a-minimal-mvvm-uwp-app/
但我不清楚如何在我的案例中使用它。
提前感谢您的帮助!
您的第二个 ListView 正在绑定到 List<FieldInfo>
类型的 TableInfo 的字段 属性。列表不支持数据绑定依赖于更新 UI.
将 List<FieldInfo>
更改为 ObservableCollection<FieldInfo>
,您应该会看到更好的结果。
顺便说一下,我建议只在第二个 ListView 中绑定 ItemsSource,而不是绑定 DataContext 和 ItemsSource(这需要两次刷新 UI)。它应该像这样简单:
<ListView x:Name="listView" ItemsSource="{x:Bind vm.Tables}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="mdl:TableInfo">
<TextBlock Text="{x:Bind Name}" FontSize="25" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<ListView x:Name="listViewTable"
ItemsSource="{Binding ElementName=listView, Path=SelectedItem.Fields}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="mdl:FieldInfo">
<TextBlock Text="{x:Bind Name}" FontSize="25" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
希望对您有所帮助。
如果我理解正确的话,问题是当添加新字段时第二个列表视图没有更新。
问题在于,字段列表的类型是List<FieldInfo>
。 UWP 中的数据绑定基于 INotifyPropertyChanged
和 INotifyCollectionChanged
接口。 List
也没有实现,因此无法自动通知用户界面它已更改。为此,您需要将其替换为 ObservableCollection<FieldInfo>
。当您将项目添加到此集合时,UI 将自动更新为新项目。
不幸的是,这仍然不足以更新字段。 ObservableCollection
class 可以观察到Add
或Remove
项的变化,但不能观察到其中单个项的变化。
为了能够做到这一点,您必须确保 FieldInfo
class 实现 INotifyPropertyChanged
并且 PropertyChanged
事件在您想要的 属性 时被调用观察变化。
最常见的接口实现如下所示:
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
现在在所有属性中你需要调用NotifyPropertyChanged
方法:
private string _description = "";
public string Description
{
get
{
return _description;
}
set
{
_description = value;
NotifyPropertyChanged();
}
}
因为[CallerMemberAttribute]
你不必指定propertyName
参数,它会被编译器自动填充。
如果有人需要,我会发布我的BaseViewModel
public class BaseViewModel : INotifyPropertyChanged {
#region Property changed
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Notifies the property changed.
/// </summary>
/// <param name="propertyName">Name of the property.</param>
protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "") {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected void NotifyPropertyChanged(string propertyName, Action<bool> message) {
if (this.PropertyChanged != null) {
// property changed
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
message?.Invoke(this.IsValid);
}
}
/// <summary>
/// Raises the property changed.
/// </summary>
/// <param name="property">The property.</param>
protected void RaisePropertyChanged(string property) {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
// SetField(()=> somewhere.Name = value; somewhere.Name, value)
// Advanced case where you rely on another property
protected bool SetProperty<T>(T currentValue, T newValue, Action DoSet,
[CallerMemberName] String property = null) {
if (EqualityComparer<T>.Default.Equals(currentValue, newValue)) return false;
DoSet.Invoke();
RaisePropertyChanged(property);
return true;
}
#endregion
}