簡體   English   中英

帶有子 UserControl/ViewModel 數據綁定問題的 WPF MVVM 父視圖/ViewModel

[英]WPF MVVM Parent View/ViewModel with child UserControl/ViewModel Data Binding Issue

我一直在嘗試在幾個不同的視圖之間實現具有一些通用功能的 WPF UserControl ,但沒有成功。 UserControl本質上是一個帶有一些上一個和下一個按鈕和一個搜索過濾器的ListBox Previous 和 Next 邏輯很容易復制和粘貼,但每次過濾都很麻煩,因此將它們全部封裝到自己的UserControlViewModel中會非常好。

但是我一直在碰壁以使子UserControl / ViewModel以兩種方式綁定回父 VM。

如果子UserControl沒有自己的ViewModel ,則此方法有效,但是我必須在該邏輯背后的代碼中實現所有功能,這並不吸引人,但並非不可能。

我把它歸結為一個演示項目- MRE 項目 - ChildVMBindingDemo

我有一個 MainWindow、MainWindowViewModel、MyListBoxControl 和一個 MyListBoxControlViewModel。

MainWindow.xaml 承載 MyListBoxControl,並將兩個綁定轉發到 MyListBoxControl 后面代碼中的 DependencyProperty。 然后后面的代碼將這些值轉發到 MyListBoxControlViewModel。 這顯然是我的問題 - “流量”命中后面的代碼,在子 VM 中設置值,這是一條從那里開始的單向街道。 我已經嘗試了所有我能想到的 BindingMode、UpdateSourceTrigger、NotifyOnSourceUpdated 和 NotifyOnTargetUpdated 組合,但都沒有成功。

MainWindow.xaml:

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="200" />
        <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>

    <local:MyListBoxControl Grid.Column="0"
        MyItems="{Binding
            RelativeSource={RelativeSource AncestorType={x:Type Window}, Mode=FindAncestor},
            Path=DataContext.MyItems}"
        SelectedMyItem="{Binding
            RelativeSource={RelativeSource AncestorType={x:Type Window}, Mode=FindAncestor},
            Path=DataContext.SelectedMyItem}"
        />
</Grid>

MainWindow.xaml.cs:

private readonly MainWindowViewModel _viewModel;

public MainWindow()
{
    InitializeComponent();

    _viewModel = new MainWindowViewModel();
    this.DataContext = _viewModel;
}

MainWindowViewModel.cs:

public MainWindowViewModel()
{
    MyItems = new ObservableCollection<MyItem>()
    {
        new MyItem() { Name = "One" },
        new MyItem() { Name = "Two" },
        new MyItem() { Name = "Thee" },
        new MyItem() { Name = "Four" },
    };
}

private ObservableCollection<MyItem> _myItems;
public ObservableCollection<MyItem> MyItems
{
    get => _myItems;
    set => Set(ref _myItems, value);
}

private MyItem _selectedMyItem;
public MyItem SelectedMyItem
{
    get => _selectedMyItem;
    set
    {
        if (Set(ref _selectedMyItem, value))
        {
            System.Diagnostics.Debug.WriteLine($"Main View Model Selected Item Set: {SelectedMyItem?.Name}");
        }
    }
}

MyListBoxControl.xaml:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="*" />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>

    <ListBox Grid.Row="0"
             ItemsSource="{Binding MyItems}"
             SelectedItem="{Binding SelectedMyItem}"
             SelectedIndex="{Binding SelectedIndex}">
        
        <ListBox.ItemTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding Name}" />
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>

    <Grid Grid.Row="1">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>

        <Button Grid.Column="0"
                Command="{Binding PrevCommand}"
                >Prev</Button>

        <Button Grid.Column="2"
                Command="{Binding NextCommand}"
                >Next</Button>
    </Grid>
</Grid>

MyListBoxControl.xaml.cs:

private readonly MyListBoxControlViewModel _viewModel;

public MyListBoxControl()
{
    InitializeComponent();

    _viewModel = new MyListBoxControlViewModel();
    this.DataContext = _viewModel;
}

public static readonly DependencyProperty MyItemsProperty =
    DependencyProperty.Register("MyItems", typeof(ObservableCollection<MyItem>), typeof(MyListBoxControl),
        new FrameworkPropertyMetadata(null, MyItemsChangedCallback));

public ObservableCollection<MyItem> MyItems
{
    get => (ObservableCollection<MyItem>)GetValue(MyItemsProperty);
    set
    {
        SetValue(MyItemsProperty, value);
        _viewModel.MyItems = MyItems;
    }
}

private static void MyItemsChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    if (d is MyListBoxControl myListBoxControl)
    {
        myListBoxControl.MyItems = (ObservableCollection<MyItem>)e.NewValue;
    }
}

public static readonly DependencyProperty SelectedMyItemProperty =
    DependencyProperty.Register(nameof(SelectedMyItem), typeof(MyItem), typeof(MyListBoxControl),
        new FrameworkPropertyMetadata(null, SelectedMyItemChangedCallback)
        {
            BindsTwoWayByDefault = true,
            DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
        });

public MyItem SelectedMyItem
{
    get => (MyItem)GetValue(SelectedMyItemProperty);
    set
    {
        SetValue(SelectedMyItemProperty, value);
        _viewModel.SelectedMyItem = SelectedMyItem;
    }
}

private static void SelectedMyItemChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    if (d is MyListBoxControl myListBoxControl)
    {
        myListBoxControl.SelectedMyItem = (MyItem)e.NewValue;
    }
}

最后

MyListBoxControlViewModel.cs:

private ObservableCollection<MyItem> _myItems;
public ObservableCollection<MyItem> MyItems
{
    get => _myItems;
    set => Set(ref _myItems, value);
}

private MyItem _selectedMyItem;
public MyItem SelectedMyItem
{
    get => _selectedMyItem;
    set
    {
        if (Set(ref _selectedMyItem, value))
        {
            System.Diagnostics.Debug.WriteLine($"Child View Model Selected Item Set: {SelectedMyItem?.Name}");
        }
    }
}

private int _selectedIndex;
public int SelectedIndex
{
    get => _selectedIndex;
    set => Set(ref _selectedIndex, value);
}

private ICommand _prevCommand;
public ICommand PrevCommand => _prevCommand ?? (_prevCommand = new RelayCommand((param) => Prev(), (param) => CanPrev()));
public bool CanPrev() => SelectedIndex > 0;
private void Prev()
{
    SelectedIndex--;
}

private ICommand _nextCommand;
public ICommand NextCommand => _nextCommand ?? (_nextCommand = new RelayCommand((param) => Next(), (param) => CanNext()));
public bool CanNext() => MyItems != null ? SelectedIndex < (MyItems.Count - 1) : false;
private void Next()
{
    SelectedIndex++;
}

在我們的項目中有與此類似的預先存在的示例(代碼中的綁定將值傳遞給子 VM) - 所以其他人也為此苦苦掙扎,看起來他們的解決方案很簡單,子控件永遠不會向父母報告——他們只輸出了一些交易。

我真正能想到的唯一一件事是使用 Messenger 將選定的值直接發送回父級,或者給子 VM 一個Action來調用並在依賴屬性后面的代碼中設置新值 - 但任何一個選項都會尖叫有異味的意大利面條,可能還有一個無休止的 setter 循環/堆棧溢出異常。

這里有更好的方法嗎,或者這里有什么我只是想念的東西?

控件永遠不應依賴於顯式或內部視圖模型。 它必須單獨依賴於它自己的成員,比如公共屬性。 然后數據上下文可以稍后綁定到這個公共屬性。

這將使可重用性獨立於實際的DataContext類型,並消除了將值委托給私有視圖模型所必需的冗余代碼(和冗余復雜性)。

MVVM 並不意味着每個控件都必須有自己的專用視圖模型。 它旨在為應用程序提供一個結構。 MVVM 的目標是應用程序級設計,而不是控制級設計。 控件必須在其自己的視圖代碼中實現其 UI 相關邏輯。 這可以在代碼隱藏中或分布在多個類中。 這些類將被直接引用(而不是通過數據綁定),因為它們共享相同的 MVVM 上下文。 UI 邏輯的 MVVM 上下文始終是View
數據綁定基本上是一種解耦ViewView Model的技術(允許View Model將數據發送到View而無需引用它——這對 MVVM 模式至關重要)。 數據操作通常發生在View Model中(從View角度來看數據的所有者)。 View只會對數據視圖進行操作(例如過濾或排序集合)。 但從不直接使用數據。

查看以下示例如何將所有與視圖相關的邏輯移至控件。
您的固定和改進(在設計方面) MyListBoxControl可能如下所示:

MyListBoxControl.xaml.cs

public partial class MyListBoxControl : UserControl
{
  public static RoutedCommand NextCommand { get; } = new RoutedUICommand("Select next MyItem", "NextCommand", typeof(MyListBoxControl));
  public static RoutedCommand PreviousCommand { get; } = new RoutedUICommand("Select previous MyItem", "PreviousCommand", typeof(MyListBoxControl));

  public ObservableCollection<MyItem> MyItemsSource
  {
    get => (ObservableCollection<MyItem>)GetValue(MyItemsSourceProperty);
    set => SetValue(MyItemsSourceProperty, value);
  }

  public static readonly DependencyProperty MyItemsSourceProperty = DependencyProperty.Register(
    "MyItemsSource", 
    typeof(ObservableCollection<MyItem>), 
    typeof(MyListBoxControl), 
    new PropertyMetadata(default));

  public int SelectedMyItemIndex
  {
    get => (int)GetValue(SelectedMyItemIndexProperty);
    set => SetValue(SelectedMyItemIndexProperty, value);
  }

  public static readonly DependencyProperty SelectedMyItemIndexProperty = DependencyProperty.Register(
    "SelectedMyItemIndex", 
    typeof(int), 
    typeof(MyListBoxControl), 
    new FrameworkPropertyMetadata(default(int), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

  public MyItem SelectedMyItem
  {
    get => (MyItem)GetValue(SelectedMyItemProperty);
    set => SetValue(SelectedMyItemProperty, value);
  }

  public static readonly DependencyProperty SelectedMyItemProperty = DependencyProperty.Register(
    "SelectedMyItem", 
    typeof(MyItem), 
    typeof(MyListBoxControl), 
    new FrameworkPropertyMetadata(default(MyItem), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

  public MyListBoxControl()
  {
    InitializeComponent();
    this.CommandBindings.Add(new CommandBinding(NextCommand, ExecuteNextCommand, CanExecuteNextCommand));
    this.CommandBindings.Add(new CommandBinding(PreviousCommand, ExecutePreviousCommand, CanExecutePreviousCommand));
  }

  private void CanExecutePreviousCommand(object sender, CanExecuteRoutedEventArgs e)
    => e.CanExecute = this.MyItems?.Any() ?? false  && this.SelectedMyItemIndex > 0;

  private void ExecutePreviousCommand(object sender, ExecutedRoutedEventArgs e)
    => this.SelectedMyItemIndex = Math.Max(this.SelectedMyItemIndex - 1, 0);

  private void CanExecuteNextCommand(object sender, CanExecuteRoutedEventArgs e) 
    => e.CanExecute = this.MyItems?.Any() ?? false  && this.SelectedMyItemIndex < this.MyItemsSource.Count - 1;

  private void ExecuteNextCommand(object sender, ExecutedRoutedEventArgs e) 
    => this.SelectedMyItemIndex = Math.Min(this.SelectedMyItemIndex + 1, this.MyItemsSource.Count - 1);
}

MyListBoxControl.xaml

<UserControl>
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="*" />
      <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>

    <ListBox Grid.Row="0"
             ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=MyItemsSource}"
             SelectedItem="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=SelectedMyItem}"
             SelectedIndex="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=SelectedMyItemIndex}">
      <ListBox.ItemTemplate>
        <DataTemplate>
          <TextBlock Text="{Binding Name}" />
        </DataTemplate>
      </ListBox.ItemTemplate>
    </ListBox>

    <Grid Grid.Row="1">
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="Auto" />
      </Grid.ColumnDefinitions>

      <Button Grid.Column="0"
              Command="{x:Static local:MyListBoxControl.PreviousCommand}"
              Content="Prev" />
      <Button Grid.Column="2"
              Command="{x:Static local:MyListBoxControl.NextCommand}"
              Content="Next" />
    </Grid>
  </Grid>
</UserControl>

使用示例

主窗口.xaml

<Window>
  <Window.DataContext>
    <MainViewModel />
  </Window.DataContext>

  <MyListBoxControl MyItemsSource="{Binding MyItems}"
                    SelectedMyItem="{Binding SelectedMyItem}" />
</Window>

如果您打算添加行為或更改現有ListBox的行為,擴展ListBox將是更好的選擇。 這將允許對其項目進行模板化。

此外,如果您的主要目的是分離視圖和相關邏輯,請始終擴展Control即不要創建UserControl 在沒有代碼隱藏文件的情況下實現控件也會感覺更自然。 它還將在定制方面提供更大的靈活性。 例如,雖然UserControl是一個ContentControl ,但它不能托管內容。

它肯定不漂亮,而且聞起來也不好——但如果這是你唯一的選擇,這就是它的工作原理。

我在 ViewModel 中添加了一個Action ,以在后面的代碼中設置 DP——注意它只是調用 SetValue,而不是直接設置 SelectedMyItem,這可以防止我擔心的 setter 循環。

MyListBoxControlViewModel.cs

public Action<MyItem> SelectedSetter { get; set; }

private MyItem _selectedMyItem;
public MyItem SelectedMyItem
{
    get => _selectedMyItem;
    set
    {
        if (Set(ref _selectedMyItem, value))
        {
            SelectedSetter?.Invoke(value);
            System.Diagnostics.Debug.WriteLine($"Child View Model Selected Item Set: {SelectedMyItem?.Name}");
        }
    }
}

MyListBoxControl.xmal.cs

public MyListBoxControl()
{
    InitializeComponent();

    _viewModel = new MyListBoxControlViewModel();
    _viewModel.SelectedSetter = (value) => SetValue(SelectedMyItemProperty, value);

    this.DataContext = _viewModel;
}

雖然不是很好,但它可能會在有限的使用中起作用。

通過構造函數傳入 Action 來說明它在操作中的重要性可能很聰明。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM