简体   繁体   English

带有子 UserControl/ViewModel 数据绑定问题的 WPF MVVM 父视图/ViewModel

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

I have been trying to implement a WPF UserControl with some common functionality between a few different views without success.我一直在尝试在几个不同的视图之间实现具有一些通用功能的 WPF UserControl ,但没有成功。 The UserControl is essentially a ListBox with some Previous & Next buttons and a Search filter. UserControl本质上是一个带有一些上一个和下一个按钮和一个搜索过滤器的ListBox Previous and Next logic is easily copied and pasted, but the filtering is a pain each time, so it would be really nice to encapsulate that all into its own UserControl and ViewModel . Previous 和 Next 逻辑很容易复制和粘贴,但每次过滤都很麻烦,因此将它们全部封装到自己的UserControlViewModel中会非常好。

But I've been running into a wall to get the child UserControl / ViewModel to two way bind back to the parent VM.但是我一直在碰壁以使子UserControl / ViewModel以两种方式绑定回父 VM。

This works if the child UserControl doesn't have its own ViewModel , but then I have to implement all the functionality in the code behind for that logic, which is unappealing, but not impossible.如果子UserControl没有自己的ViewModel ,则此方法有效,但是我必须在该逻辑背后的代码中实现所有功能,这并不吸引人,但并非不可能。

I've boiled this down to a demo project- MRE Project - ChildVMBindingDemo我把它归结为一个演示项目- MRE 项目 - ChildVMBindingDemo

I have a MainWindow, MainWindowViewModel, MyListBoxControl, and a MyListBoxControlViewModel.我有一个 MainWindow、MainWindowViewModel、MyListBoxControl 和一个 MyListBoxControlViewModel。

The MainWindow.xaml hosts the MyListBoxControl, and forwards two bindings to DependencyProperty in the code behind of the MyListBoxControl. MainWindow.xaml 承载 MyListBoxControl,并将两个绑定转发到 MyListBoxControl 后面代码中的 DependencyProperty。 That code behind then forwards those values to the MyListBoxControlViewModel.然后后面的代码将这些值转发到 MyListBoxControlViewModel。 This is obviously my issue- the "traffic" hits the code behind, sets the values in the child VM, and it's a one way street from there.这显然是我的问题 - “流量”命中后面的代码,在子 VM 中设置值,这是一条从那里开始的单向街道。 I've tried every combination of BindingMode, UpdateSourceTrigger, NotifyOnSourceUpdated, and NotifyOnTargetUpdated that I can think of without success.我已经尝试了所有我能想到的 BindingMode、UpdateSourceTrigger、NotifyOnSourceUpdated 和 NotifyOnTargetUpdated 组合,但都没有成功。

MainWindow.xaml: 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: MainWindow.xaml.cs:

private readonly MainWindowViewModel _viewModel;

public MainWindow()
{
    InitializeComponent();

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

MainWindowViewModel.cs: 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: 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: 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;
    }
}

And finally最后

MyListBoxControlViewModel.cs: 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++;
}

There were preexisting examples similar to this in our project (with the bindings in the code behind passing the values to the child VM)- so someone else struggled with this as well, and it looks like their solution was simply, that the child control never reported back to the parent- they were output only kinda deals.在我们的项目中有与此类似的预先存在的示例(代码中的绑定将值传递给子 VM) - 所以其他人也为此苦苦挣扎,看起来他们的解决方案很简单,子控件永远不会向父母报告——他们只输出了一些交易。

The only thing I can really think of is to use a Messenger to send the selected value back to the parent directly, or give the child VM an Action to call and set the new value in the code behind dependency properties- but either option just screams of odorous spaghetti, and a probably an endless setter loop/stack overflow exception.我真正能想到的唯一一件事是使用 Messenger 将选定的值直接发送回父级,或者给子 VM 一个Action来调用并在依赖属性后面的代码中设置新值 - 但任何一个选项都会尖叫有异味的意大利面条,可能还有一个无休止的 setter 循环/堆栈溢出异常。

Is there a better approach here, or is there something here that I am just missing?这里有更好的方法吗,或者这里有什么我只是想念的东西?

A control should never depend on an explicit or internal view model.控件永远不应依赖于显式或内部视图模型。 It must depend on its own members, like public properties, alone.它必须单独依赖于它自己的成员,比如公共属性。 Then the data context can later bind to this public properties.然后数据上下文可以稍后绑定到这个公共属性。

This will enable reusability independent from the actual DataContext type and eliminates redundant code (and redundant complexity) that otherwise would be necessary to delegate values to the private view model.这将使可重用性独立于实际的DataContext类型,并消除了将值委托给私有视图模型所必需的冗余代码(和冗余复杂性)。

MVVM does not mean that each control must have its own dedicated view model. MVVM 并不意味着每个控件都必须有自己的专用视图模型。 It is meant to give the application a structure.它旨在为应用程序提供一个结构。 MVVM targets application level design and not control level design. MVVM 的目标是应用程序级设计,而不是控制级设计。 A control must implement its UI related logic in its own view code.控件必须在其自己的视图代码中实现其 UI 相关逻辑。 This can be in code-behind or spread across multiple classes.这可以在代码隐藏中或分布在多个类中。 Such classes would be referenced directly (and not via data binding) as they share the same MVVM context.这些类将被直接引用(而不是通过数据绑定),因为它们共享相同的 MVVM 上下文。 The MVVM context of UI logic is always View . UI 逻辑的 MVVM 上下文始终是View
Data binding is basically a technology to decouple View and View Model (to allow the View Model to send data to the View without having to reference it - which is crucial to the MVVM pattern).数据绑定基本上是一种解耦ViewView Model的技术(允许View Model将数据发送到View而无需引用它——这对 MVVM 模式至关重要)。 Data operations usually take place in the View Model (the owner of the data from the View perspective).数据操作通常发生在View Model中(从View角度来看数据的所有者)。 View would only operate on data views (eg to filter or sort collections). View只会对数据视图进行操作(例如过滤或排序集合)。 But never on data directly.但从不直接使用数据。

See how the following example moved all View related logic to the control.查看以下示例如何将所有与视图相关的逻辑移至控件。
Your fixed and improved (in terms of design) MyListBoxControl , could look as follows:您的固定和改进(在设计方面) MyListBoxControl可能如下所示:

MyListBoxControl.xaml.cs 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 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>

Usage example使用示例

MainWindow.xaml主窗口.xaml

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

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

In case you meant to add behavior or change the behavior of the existing ListBox , extending ListBox would be the far better option.如果您打算添加行为或更改现有ListBox的行为,扩展ListBox将是更好的选择。 This would allow to template its items out of the box.这将允许对其项目进行模板化。

Additionally, if your primary intention was to separate view and related logic, always extend Control ie don't create a UserControl .此外,如果您的主要目的是分离视图和相关逻辑,请始终扩展Control即不要创建UserControl It will also feel more natural to implement the control without a code-behind file.在没有代码隐藏文件的情况下实现控件也会感觉更自然。 It will also enable more flexibility in terms of customization.它还将在定制方面提供更大的灵活性。 For example, although UserControl is a ContentControl , it can't host content.例如,虽然UserControl是一个ContentControl ,但它不能托管内容。

It's sure not pretty, and it doesn't smell great- but if this is your only option, here is how this could work.它肯定不漂亮,而且闻起来也不好——但如果这是你唯一的选择,这就是它的工作原理。

I added an Action to the ViewModel, to set the DP in the code behind- note that it's only calling SetValue, and not directly setting the SelectedMyItem, which prevents the setter loop I was worried about.我在 ViewModel 中添加了一个Action ,以在后面的代码中设置 DP——注意它只是调用 SetValue,而不是直接设置 SelectedMyItem,这可以防止我担心的 setter 循环。

MyListBoxControlViewModel.cs 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}");
        }
    }
}

and

MyListBoxControl.xmal.cs MyListBoxControl.xmal.cs

public MyListBoxControl()
{
    InitializeComponent();

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

    this.DataContext = _viewModel;
}

While not great, it would probably work in limited use.虽然不是很好,但它可能会在有限的使用中起作用。

Probably smart to pass the Action in via the constructor to state its importance in the operation.通过构造函数传入 Action 来说明它在操作中的重要性可能很聪明。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM