UWP、MVVM 和级联 ListView

UWP, MVVM and cascade ListView

我对 UWP 和两个 ListView 越来越着迷。场景很简单:我在一个页面中有两个列表视图(listviewlistViewField)。

我有一个像这样的 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 中添加一些 TableInfoFieldInfo,但第一个列表视图没有显示任何内容。 如果我在 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

但我不清楚如何在我的案例中使用它。

提前感谢您的帮助!

您的第二个 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 中的数据绑定基于 INotifyPropertyChangedINotifyCollectionChanged 接口。 List 也没有实现,因此无法自动通知用户界面它已更改。为此,您需要将其替换为 ObservableCollection<FieldInfo>。当您将项目添加到此集合时,UI 将自动更新为新项目。

不幸的是,这仍然不足以更新字段。 ObservableCollection class 可以观察到AddRemove 项的变化,但不能观察到其中单个项的变化。 为了能够做到这一点,您必须确保 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
}