[英]WPF MVVM Parent View/ViewModel with child UserControl/ViewModel Data Binding Issue
我一直在嘗試在幾個不同的視圖之間實現具有一些通用功能的 WPF UserControl
,但沒有成功。 UserControl
本質上是一個帶有一些上一個和下一個按鈕和一個搜索過濾器的ListBox
。 Previous 和 Next 邏輯很容易復制和粘貼,但每次過濾都很麻煩,因此將它們全部封裝到自己的UserControl
和ViewModel
中會非常好。
但是我一直在碰壁以使子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 。
數據綁定基本上是一種解耦View和View 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.