WPF MVVM Parent View/ViewModel 与 child UserControl/ViewModel 数据绑定问题
WPF MVVM Parent View/ViewModel with child UserControl/ViewModel Data Binding Issue
我一直在尝试实现一个 WPF UserControl
,在几个不同的视图之间具有一些通用功能,但没有成功。 UserControl
本质上是一个带有一些上一个和下一个按钮和一个搜索过滤器的 ListBox
。 Previous 和 Next 逻辑很容易复制和粘贴,但每次过滤都很痛苦,所以将其全部封装到自己的 UserControl
和 ViewModel
.
中会非常好
但我已经 运行 撞墙,以将 child UserControl
/ViewModel
双向绑定回 parent 虚拟机.
如果 child UserControl
没有自己的 ViewModel
,这会起作用,但是我必须在代码中为该逻辑实现所有功能,即没有吸引力,但并非不可能。
我已经将其归结为一个演示项目- MRE Project - ChildVMBindingDemo
我有一个 MainWindow、MainWindowViewModel、MyListBoxControl 和一个 MyListBoxControlViewModel。
MainWindow.xaml 托管 MyListBoxControl,并在 MyListBoxControl 的代码后面将两个绑定转发到 DependencyProperty。然后后面的代码将这些值转发给 MyListBoxControlViewModel。这显然是我的问题——“流量”影响了后面的代码,在 child 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++;
}
我们的项目中已有类似的示例(代码中的绑定将值传递给 child 虚拟机)- 所以其他人也在努力解决这个问题,看起来他们的解决方案很简单,child 控件从未向 parent 报告 - 它们仅输出一些交易。
我唯一能想到的就是用一个Messenger把选中的值直接传回给parent,或者给child VM一个Action
来调用并在依赖属性背后的代码中设置新值——但任何一个选项都只会发出臭意大利面的尖叫声,并且可能是无休止的 setter loop/stack 溢出异常。
这里是否有更好的方法,或者这里是否有我遗漏的东西?
它肯定不漂亮,而且闻起来也不太好 - 但如果这是你唯一的选择,下面是它的工作原理。
我在 ViewModel 中添加了一个 Action
,以便在后面的代码中设置 DP - 请注意,它只是调用 SetValue,而不是直接设置 SelectedMyItem,这会阻止 setter 循环 I很担心。
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 以说明其在操作中的重要性可能是明智的。
控件永远不应依赖于显式或内部视图模型。它必须单独依赖于它自己的成员,例如 public 属性。然后数据上下文可以稍后绑定到这个 public 属性。
这将实现独立于实际 DataContext
类型的可重用性,并消除冗余代码(和冗余复杂性),否则将值委托给私有视图模型是必要的。
MVVM并不意味着每个控件都必须有自己专用的视图模型。它旨在为 application 提供一个结构。 MVVM 的目标是应用程序级设计,而不是控制级设计。控件必须在其自己的视图代码中实现其 UI 相关逻辑。这可以在 code-behind 中或分布在多个 类 中。这样的 类 将被直接引用(而不是通过数据绑定),因为它们共享相同的 MVVM 上下文。 UI逻辑的MVVM上下文总是View.
数据绑定基本上是一种解耦 View 和 View Model 的技术(允许 View Model 发送数据到 View 而不必引用它——这对 MVVM 模式至关重要)。
数据操作通常发生在 View Model(从 View 角度看数据的所有者)。 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>
使用示例
MainWindow.xaml
<Window>
<Window.DataContext>
<MainViewModel />
</Window.DataContext>
<MyListBoxControl MyItemsSource="{Binding MyItems}"
SelectedMyItem="{Binding SelectedMyItem}" />
</Window>
如果您打算添加行为或更改现有 ListBox
的行为,扩展 ListBox
将是更好的选择。这将允许开箱即用地为其项目设置模板。
此外,如果您的主要目的是分离视图和相关逻辑,请始终扩展 Control
,即不要创建 UserControl
。在没有 code-behind 文件的情况下实现控件也会感觉更自然。它还将在定制方面提供更大的灵活性。例如,虽然 UserControl
是 ContentControl
,但它不能承载内容。
我一直在尝试实现一个 WPF UserControl
,在几个不同的视图之间具有一些通用功能,但没有成功。 UserControl
本质上是一个带有一些上一个和下一个按钮和一个搜索过滤器的 ListBox
。 Previous 和 Next 逻辑很容易复制和粘贴,但每次过滤都很痛苦,所以将其全部封装到自己的 UserControl
和 ViewModel
.
但我已经 运行 撞墙,以将 child UserControl
/ViewModel
双向绑定回 parent 虚拟机.
如果 child UserControl
没有自己的 ViewModel
,这会起作用,但是我必须在代码中为该逻辑实现所有功能,即没有吸引力,但并非不可能。
我已经将其归结为一个演示项目- MRE Project - ChildVMBindingDemo
我有一个 MainWindow、MainWindowViewModel、MyListBoxControl 和一个 MyListBoxControlViewModel。
MainWindow.xaml 托管 MyListBoxControl,并在 MyListBoxControl 的代码后面将两个绑定转发到 DependencyProperty。然后后面的代码将这些值转发给 MyListBoxControlViewModel。这显然是我的问题——“流量”影响了后面的代码,在 child 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++;
}
我们的项目中已有类似的示例(代码中的绑定将值传递给 child 虚拟机)- 所以其他人也在努力解决这个问题,看起来他们的解决方案很简单,child 控件从未向 parent 报告 - 它们仅输出一些交易。
我唯一能想到的就是用一个Messenger把选中的值直接传回给parent,或者给child VM一个Action
来调用并在依赖属性背后的代码中设置新值——但任何一个选项都只会发出臭意大利面的尖叫声,并且可能是无休止的 setter loop/stack 溢出异常。
这里是否有更好的方法,或者这里是否有我遗漏的东西?
它肯定不漂亮,而且闻起来也不太好 - 但如果这是你唯一的选择,下面是它的工作原理。
我在 ViewModel 中添加了一个 Action
,以便在后面的代码中设置 DP - 请注意,它只是调用 SetValue,而不是直接设置 SelectedMyItem,这会阻止 setter 循环 I很担心。
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 以说明其在操作中的重要性可能是明智的。
控件永远不应依赖于显式或内部视图模型。它必须单独依赖于它自己的成员,例如 public 属性。然后数据上下文可以稍后绑定到这个 public 属性。
这将实现独立于实际 DataContext
类型的可重用性,并消除冗余代码(和冗余复杂性),否则将值委托给私有视图模型是必要的。
MVVM并不意味着每个控件都必须有自己专用的视图模型。它旨在为 application 提供一个结构。 MVVM 的目标是应用程序级设计,而不是控制级设计。控件必须在其自己的视图代码中实现其 UI 相关逻辑。这可以在 code-behind 中或分布在多个 类 中。这样的 类 将被直接引用(而不是通过数据绑定),因为它们共享相同的 MVVM 上下文。 UI逻辑的MVVM上下文总是View.
数据绑定基本上是一种解耦 View 和 View Model 的技术(允许 View Model 发送数据到 View 而不必引用它——这对 MVVM 模式至关重要)。
数据操作通常发生在 View Model(从 View 角度看数据的所有者)。 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>
使用示例
MainWindow.xaml
<Window>
<Window.DataContext>
<MainViewModel />
</Window.DataContext>
<MyListBoxControl MyItemsSource="{Binding MyItems}"
SelectedMyItem="{Binding SelectedMyItem}" />
</Window>
如果您打算添加行为或更改现有 ListBox
的行为,扩展 ListBox
将是更好的选择。这将允许开箱即用地为其项目设置模板。
此外,如果您的主要目的是分离视图和相关逻辑,请始终扩展 Control
,即不要创建 UserControl
。在没有 code-behind 文件的情况下实现控件也会感觉更自然。它还将在定制方面提供更大的灵活性。例如,虽然 UserControl
是 ContentControl
,但它不能承载内容。