如何在 UserControl 和父控件之间绑定 WPF 命令 Window
How do I Bind WPF Commands between a UserControl and a parent Window
我先让图片说话。
所以你看,我想创建一个支持绑定到父 window 的 DataContext 的 WPF 用户控件。用户控件只是一个按钮和一个带有自定义 ItemTemplate 的列表框,用于显示带有标签和删除按钮的内容。
添加按钮应在主视图模型上调用 ICommand 以与用户交互以选择新事物(IThing 实例)。用户控件中 ListBoxItem 中的 Remove 按钮应该类似地调用主视图模型上的 ICommand 来请求相关事物的移除。为此,Remove 按钮必须向视图模型发送一些关于请求删除的事物的标识信息。所以有两种类型的命令应该可以绑定到这个控件。像 AddThingCommand() 和 RemoveThingCommand(IThing thing) 这样的东西。
我使用 Click 事件获得了该功能,但这感觉很老套,在 XAML 后面生成了一堆代码,并且与原始 MVVM 实现的其余部分冲突。好想正常使用Commands和MVVM
有足够的代码来运行基本演示,我暂缓发布整个内容以减少混淆。让我觉得自己如此接近的工作原理是 ListBox 的 DataTemplate 正确绑定了 Label,当父级 window 将项目添加到集合中时,它们就会显示出来。
<Label Content="{Binding Path=DisplayName}" />
虽然它正确显示了 IThing,但当我单击它时,它旁边的“删除”按钮没有任何反应。
<Button Command="{Binding Path=RemoveItemCommand, RelativeSource={RelativeSource AncestorType={x:Type userControlCommands:ItemManager }}}">
这并不是特别出乎意料,因为没有提供特定的项目,但添加按钮不必指定任何内容,它也无法调用命令。
<Button Command="{Binding Path=AddItemCommand, RelativeSource={RelativeSource AncestorType={x:Type userControlCommands:ItemManager }}}">
所以我需要的是添加按钮的 "basic" 修复,以便它调用父 window 的命令来添加一个东西,以及更复杂的删除按钮修复, 因此它也调用父命令但也传递其绑定的东西。
非常感谢您的任何见解,
参考以下代码。
UserControl.XAML
<Grid>
<ListBox ItemsSource="{Binding Things}" x:Name="lst">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding ThingName}" Margin="3"/>
<Button Content="Remove" Margin="3" Command="{Binding ElementName=lst, Path=DataContext.RemoveCommand}" CommandParameter="{Binding}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
Window.Xaml
<Window x:Class="MultiBind_Learning.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MultiBind_Learning"
Title="Window1" Height="300" Width="300">
<StackPanel Orientation="Horizontal">
<Button Content="Add" Width="50" Height="25" Command="{Binding AddCommnd }"/>
<local:UserControl2/>
</StackPanel>
Window.xaml.cs
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
this.DataContext = new ThingViewModel();
}
}
ThingViewModel.cs
class ThingViewModel
{
private ObservableCollection<Thing> things = new ObservableCollection<Thing>();
public ObservableCollection<Thing> Things
{
get { return things; }
set { things = value; }
}
public ICommand AddCommnd { get; set; }
public ICommand RemoveCommand { get; set; }
public ThingViewModel()
{
for (int i = 0; i < 10; i++)
{
things.Add(new Thing() { ThingName="Thing" +i});
}
AddCommnd = new BaseCommand(Add);
RemoveCommand = new BaseCommand(Remove);
}
void Add(object obj)
{
things.Add(new Thing() {ThingName="Added New" });
}
void Remove(object obj)
{
things.Remove((Thing)obj);
}
}
Thing.cs
class Thing :INotifyPropertyChanged
{
private string thingName;
public string ThingName
{
get { return thingName; }
set { thingName = value; OnPropertyChanged("ThingName"); }
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
}
BaseCommand.cs
public class BaseCommand : ICommand
{
private Predicate<object> _canExecute;
private Action<object> _method;
public event EventHandler CanExecuteChanged;
public BaseCommand(Action<object> method)
{
_method = method;
}
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
_method.Invoke(parameter);
}
}
您可以尝试使用 MVVMLight 中的 RelayCommand 或 PRISM 库中的 DelegateCommand 而不是 Base 命令。
这是微不足道的,通过将您的 UserControl 当作一个控件(恰好由其他控件组成)来实现。那是什么意思?这意味着你应该将 DependencyProperties 放在你的 UC 上,你的 ViewModel 可以绑定到它,就像任何其他控件一样。按钮公开一个命令 属性,文本框公开一个文本 属性,等等。您需要在 UserControl 的表面上公开它完成其工作所需的一切。
让我们举一个简单的(不到两分钟的时间)的例子。我将省略 ICommand 实现。
首先,我们的Window
<Window x:Class="UCsAndICommands.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:t="clr-namespace:UCsAndICommands"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<t:ViewModel />
</Window.DataContext>
<t:ItemsEditor Items="{Binding Items}"
AddItem="{Binding AddItem}"
RemoveItem="{Binding RemoveItem}" />
</Window>
请注意,我们有项目编辑器,它公开了它需要的一切属性——它正在编辑的项目列表、添加新项目的命令以及删除项目的命令。
接下来,用户控件
<UserControl x:Class="UCsAndICommands.ItemsEditor"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:t="clr-namespace:UCsAndICommands"
x:Name="root">
<UserControl.Resources>
<DataTemplate DataType="{x:Type t:Item}">
<StackPanel Orientation="Horizontal">
<Button Command="{Binding RemoveItem, ElementName=root}"
CommandParameter="{Binding}">Remove</Button>
<TextBox Text="{Binding Name}" Width="100"/>
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<StackPanel>
<Button Command="{Binding AddItem, ElementName=root}">Add</Button>
<ItemsControl ItemsSource="{Binding Items, ElementName=root}" />
</StackPanel>
</UserControl>
我们将控件绑定到 UC 表面上定义的 DP。请不要像 DataContext=this;
这样胡说八道,因为这种反模式会破坏更复杂的 UC 实现。
这是UC上这些属性的定义
public partial class ItemsEditor : UserControl
{
#region Items
public static readonly DependencyProperty ItemsProperty =
DependencyProperty.Register(
"Items",
typeof(IEnumerable<Item>),
typeof(ItemsEditor),
new UIPropertyMetadata(null));
public IEnumerable<Item> Items
{
get { return (IEnumerable<Item>)GetValue(ItemsProperty); }
set { SetValue(ItemsProperty, value); }
}
#endregion
#region AddItem
public static readonly DependencyProperty AddItemProperty =
DependencyProperty.Register(
"AddItem",
typeof(ICommand),
typeof(ItemsEditor),
new UIPropertyMetadata(null));
public ICommand AddItem
{
get { return (ICommand)GetValue(AddItemProperty); }
set { SetValue(AddItemProperty, value); }
}
#endregion
#region RemoveItem
public static readonly DependencyProperty RemoveItemProperty =
DependencyProperty.Register(
"RemoveItem",
typeof(ICommand),
typeof(ItemsEditor),
new UIPropertyMetadata(null));
public ICommand RemoveItem
{
get { return (ICommand)GetValue(RemoveItemProperty); }
set { SetValue(RemoveItemProperty, value); }
}
#endregion
public ItemsEditor()
{
InitializeComponent();
}
}
只是UC表面的DP。没什么大不了的。我们的 ViewModel 同样简单
public class ViewModel
{
public ObservableCollection<Item> Items { get; private set; }
public ICommand AddItem { get; private set; }
public ICommand RemoveItem { get; private set; }
public ViewModel()
{
Items = new ObservableCollection<Item>();
AddItem = new DelegatedCommand<object>(
o => true, o => Items.Add(new Item()));
RemoveItem = new DelegatedCommand<Item>(
i => true, i => Items.Remove(i));
}
}
您正在编辑三个不同的集合,因此您可能想要公开更多 ICommand 以明确您是哪个集合 adding/removing。或者您可以花点钱使用 CommandParameter 来计算。
默认情况下,您的用户控件将继承其容器的 DataContext。因此,您的 window 使用的 ViewModel class 可以由用户控件直接绑定,使用 XAML 中的绑定符号。无需指定 DependentProperties 或 RoutedEvents,只需正常绑定到命令属性即可。
我先让图片说话。
所以你看,我想创建一个支持绑定到父 window 的 DataContext 的 WPF 用户控件。用户控件只是一个按钮和一个带有自定义 ItemTemplate 的列表框,用于显示带有标签和删除按钮的内容。
添加按钮应在主视图模型上调用 ICommand 以与用户交互以选择新事物(IThing 实例)。用户控件中 ListBoxItem 中的 Remove 按钮应该类似地调用主视图模型上的 ICommand 来请求相关事物的移除。为此,Remove 按钮必须向视图模型发送一些关于请求删除的事物的标识信息。所以有两种类型的命令应该可以绑定到这个控件。像 AddThingCommand() 和 RemoveThingCommand(IThing thing) 这样的东西。
我使用 Click 事件获得了该功能,但这感觉很老套,在 XAML 后面生成了一堆代码,并且与原始 MVVM 实现的其余部分冲突。好想正常使用Commands和MVVM
有足够的代码来运行基本演示,我暂缓发布整个内容以减少混淆。让我觉得自己如此接近的工作原理是 ListBox 的 DataTemplate 正确绑定了 Label,当父级 window 将项目添加到集合中时,它们就会显示出来。
<Label Content="{Binding Path=DisplayName}" />
虽然它正确显示了 IThing,但当我单击它时,它旁边的“删除”按钮没有任何反应。
<Button Command="{Binding Path=RemoveItemCommand, RelativeSource={RelativeSource AncestorType={x:Type userControlCommands:ItemManager }}}">
这并不是特别出乎意料,因为没有提供特定的项目,但添加按钮不必指定任何内容,它也无法调用命令。
<Button Command="{Binding Path=AddItemCommand, RelativeSource={RelativeSource AncestorType={x:Type userControlCommands:ItemManager }}}">
所以我需要的是添加按钮的 "basic" 修复,以便它调用父 window 的命令来添加一个东西,以及更复杂的删除按钮修复, 因此它也调用父命令但也传递其绑定的东西。
非常感谢您的任何见解,
参考以下代码。 UserControl.XAML
<Grid>
<ListBox ItemsSource="{Binding Things}" x:Name="lst">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding ThingName}" Margin="3"/>
<Button Content="Remove" Margin="3" Command="{Binding ElementName=lst, Path=DataContext.RemoveCommand}" CommandParameter="{Binding}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
Window.Xaml
<Window x:Class="MultiBind_Learning.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MultiBind_Learning"
Title="Window1" Height="300" Width="300">
<StackPanel Orientation="Horizontal">
<Button Content="Add" Width="50" Height="25" Command="{Binding AddCommnd }"/>
<local:UserControl2/>
</StackPanel>
Window.xaml.cs
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
this.DataContext = new ThingViewModel();
}
}
ThingViewModel.cs
class ThingViewModel
{
private ObservableCollection<Thing> things = new ObservableCollection<Thing>();
public ObservableCollection<Thing> Things
{
get { return things; }
set { things = value; }
}
public ICommand AddCommnd { get; set; }
public ICommand RemoveCommand { get; set; }
public ThingViewModel()
{
for (int i = 0; i < 10; i++)
{
things.Add(new Thing() { ThingName="Thing" +i});
}
AddCommnd = new BaseCommand(Add);
RemoveCommand = new BaseCommand(Remove);
}
void Add(object obj)
{
things.Add(new Thing() {ThingName="Added New" });
}
void Remove(object obj)
{
things.Remove((Thing)obj);
}
}
Thing.cs
class Thing :INotifyPropertyChanged
{
private string thingName;
public string ThingName
{
get { return thingName; }
set { thingName = value; OnPropertyChanged("ThingName"); }
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
}
BaseCommand.cs
public class BaseCommand : ICommand
{
private Predicate<object> _canExecute;
private Action<object> _method;
public event EventHandler CanExecuteChanged;
public BaseCommand(Action<object> method)
{
_method = method;
}
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
_method.Invoke(parameter);
}
}
您可以尝试使用 MVVMLight 中的 RelayCommand 或 PRISM 库中的 DelegateCommand 而不是 Base 命令。
这是微不足道的,通过将您的 UserControl 当作一个控件(恰好由其他控件组成)来实现。那是什么意思?这意味着你应该将 DependencyProperties 放在你的 UC 上,你的 ViewModel 可以绑定到它,就像任何其他控件一样。按钮公开一个命令 属性,文本框公开一个文本 属性,等等。您需要在 UserControl 的表面上公开它完成其工作所需的一切。
让我们举一个简单的(不到两分钟的时间)的例子。我将省略 ICommand 实现。
首先,我们的Window
<Window x:Class="UCsAndICommands.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:t="clr-namespace:UCsAndICommands"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<t:ViewModel />
</Window.DataContext>
<t:ItemsEditor Items="{Binding Items}"
AddItem="{Binding AddItem}"
RemoveItem="{Binding RemoveItem}" />
</Window>
请注意,我们有项目编辑器,它公开了它需要的一切属性——它正在编辑的项目列表、添加新项目的命令以及删除项目的命令。
接下来,用户控件
<UserControl x:Class="UCsAndICommands.ItemsEditor"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:t="clr-namespace:UCsAndICommands"
x:Name="root">
<UserControl.Resources>
<DataTemplate DataType="{x:Type t:Item}">
<StackPanel Orientation="Horizontal">
<Button Command="{Binding RemoveItem, ElementName=root}"
CommandParameter="{Binding}">Remove</Button>
<TextBox Text="{Binding Name}" Width="100"/>
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<StackPanel>
<Button Command="{Binding AddItem, ElementName=root}">Add</Button>
<ItemsControl ItemsSource="{Binding Items, ElementName=root}" />
</StackPanel>
</UserControl>
我们将控件绑定到 UC 表面上定义的 DP。请不要像 DataContext=this;
这样胡说八道,因为这种反模式会破坏更复杂的 UC 实现。
这是UC上这些属性的定义
public partial class ItemsEditor : UserControl
{
#region Items
public static readonly DependencyProperty ItemsProperty =
DependencyProperty.Register(
"Items",
typeof(IEnumerable<Item>),
typeof(ItemsEditor),
new UIPropertyMetadata(null));
public IEnumerable<Item> Items
{
get { return (IEnumerable<Item>)GetValue(ItemsProperty); }
set { SetValue(ItemsProperty, value); }
}
#endregion
#region AddItem
public static readonly DependencyProperty AddItemProperty =
DependencyProperty.Register(
"AddItem",
typeof(ICommand),
typeof(ItemsEditor),
new UIPropertyMetadata(null));
public ICommand AddItem
{
get { return (ICommand)GetValue(AddItemProperty); }
set { SetValue(AddItemProperty, value); }
}
#endregion
#region RemoveItem
public static readonly DependencyProperty RemoveItemProperty =
DependencyProperty.Register(
"RemoveItem",
typeof(ICommand),
typeof(ItemsEditor),
new UIPropertyMetadata(null));
public ICommand RemoveItem
{
get { return (ICommand)GetValue(RemoveItemProperty); }
set { SetValue(RemoveItemProperty, value); }
}
#endregion
public ItemsEditor()
{
InitializeComponent();
}
}
只是UC表面的DP。没什么大不了的。我们的 ViewModel 同样简单
public class ViewModel
{
public ObservableCollection<Item> Items { get; private set; }
public ICommand AddItem { get; private set; }
public ICommand RemoveItem { get; private set; }
public ViewModel()
{
Items = new ObservableCollection<Item>();
AddItem = new DelegatedCommand<object>(
o => true, o => Items.Add(new Item()));
RemoveItem = new DelegatedCommand<Item>(
i => true, i => Items.Remove(i));
}
}
您正在编辑三个不同的集合,因此您可能想要公开更多 ICommand 以明确您是哪个集合 adding/removing。或者您可以花点钱使用 CommandParameter 来计算。
默认情况下,您的用户控件将继承其容器的 DataContext。因此,您的 window 使用的 ViewModel class 可以由用户控件直接绑定,使用 XAML 中的绑定符号。无需指定 DependentProperties 或 RoutedEvents,只需正常绑定到命令属性即可。