如何将所有 WPF 命令和相关事件集中放在一个静态类中






4.78/5 (18投票s)
本类文章介绍如何将所有 WPF 命令及其相关的 CanExecute 和 Executed 事件集中放置在一个静态类中。
前言
由于本文并非为初学者撰写,我假定您已经了解 WPF 命令及其用途。但如果您不了解,我认为现在是学习它们的好时机。有许多优秀的文章和书籍描述了 WPF 命令的性质和用法。Richard Griffin 的一篇精彩文章可以在他的博客上找到:WPF Commands, a scenic tour: Part I。我还推荐阅读 Josh Smith 的《Smart Routed Commands in WPF》,以学习 WPF 命令的高级概念。
引言
WPF 中的命令可以在静态类中定义和维护。命令需要与使用它们的控件所在的窗口进行绑定。为了绑定命令,需要在窗口本身中定义 CanExecute
和 Executed
事件。但大多数情况下,人们希望将所有命令及其相关的 CanExecute
和 Executed
事件集中在一个静态类中,这样可以更方便地从一个中心位置维护程序逻辑。本文的目的是介绍如何实现此功能。
定义命令
让我们定义一些将在本示例中使用的命令。
public class MyAppCommands
{
private static RoutedUICommand _AddContact;
private static RoutedUICommand _EditContact;
private static RoutedUICommand _DeleteContact;
static MyAppCommands()
{
_AddContact = new RoutedUICommand("Add a new contact",
"AddContact", typeof(MyAppCommands));
_EditContact = new RoutedUICommand("Edit an existing contact",
"EditContact", typeof(MyAppCommands));
_DeleteContact = new RoutedUICommand("Delete an existing contact",
"DeleteContact", typeof(MyAppCommands));
}
// Command: AddContact
public static RoutedUICommand AddContact
{
get { return _AddContact; }
}
// Command: EditContact
public static RoutedUICommand EditContact
{
get { return _EditContact; }
}
// Command: DeleteContact
public static RoutedUICommand DeleteContact
{
get { return _DeleteContact; }
}
}
现在,我们假设一个窗口使用三个按钮分别触发这三个命令,并且该窗口正在使用这些命令。
<Window x:Class="CentralCommands.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Commands="clr-namespace:CentralCommands"
Title="MainWindow" Height="300" Width="300"
x:Name="mainWindow">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Button HorizontalAlignment="Stretch" Margin="20"
Name="btnAdd" VerticalAlignment="Stretch" Grid.Row="0"
Command="Commands:MyAppCommands.AddContact">Add Contact</Button>
<Button HorizontalAlignment="Stretch" Margin="20"
Name="btnEdit" VerticalAlignment="Stretch" Grid.Row="1"
Command="Commands:MyAppCommands.EditContact">Edit Contact</Button>
<Button HorizontalAlignment="Stretch" Margin="20"
Name="btnDelete" VerticalAlignment="Stretch" Grid.Row="2"
Command="Commands:MyAppCommands.DeleteContact">Save Contact</Button>
</Grid>
</Window>
目前,如果您执行此代码,所有按钮都将禁用。原因是尚未为命令绑定 CanExecute
和/或 Executed
事件。如果仅定义了 CanExecute
,当 CanExecute
事件将传递给方法的 CanExecuteRoutedEventArgs
参数的 CanExecute
属性设置为 true
时,按钮才会启用,但如果您单击按钮,将不会发生任何事情。如果为命令定义了 Executed
,当您单击按钮时将调用 Executed
方法。如果您仅定义 Executed
,则使用该命令的按钮将始终启用,无论如何。
让我们在我们 `MyAppCommands` 静态类中为命令定义 CanExecute
和 Executed
事件。这些命令除了显示有关已执行命令的消息框外,不做任何事情。出于测试目的,AddContact
和 DeleteContact
是启用的,而 EditContact
是禁用的。
public static void AddContact_Executed(object sender,
ExecutedRoutedEventArgs e)
{
MessageBox.Show("Add contact command executed");
}
public static void AddContact_CanExecute(object sender,
CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
public static void EditContact_Executed(object sender,
ExecutedRoutedEventArgs e)
{
MessageBox.Show("Edit contact command executed");
}
public static void EditContact_CanExecute(object sender,
CanExecuteRoutedEventArgs e)
{
e.CanExecute = false;
}
public static void DeleteContact_Executed(object sender,
ExecutedRoutedEventArgs e)
{
MessageBox.Show("Delete contact command executed");
}
public static void DeleteContact_CanExecute(object sender,
CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
问题所在
现在,我们需要在窗口的命令绑定中将这些事件与命令进行绑定。让我们尝试使用 x:Static
标记扩展来定义事件,该扩展将任何静态的按值实体绑定到源(目前我们只绑定 AddContact
命令)
<Window.CommandBindings>
<CommandBinding Command="Commands:MyAppCommands.AddContact"
CanExecute="{x:Static Commands:MyAppCommands.AddContact_CanExecute}"
Executed="{x:Static Commands:MyAppCommands.AddContact_Executed}"/>
</Window.CommandBindings>
如果您尝试编译此代码,将收到以下错误
CanExecute="{x:Static Commands:MyAppCommands.AddContact_CanExecute}"
is not valid. '{x:Static Commands:MyAppCommands.AddContact_CanExecute}'
is not a valid event handler method name. Only instance methods
on the generated or code-behind class are valid.
为什么会收到此错误?事件在 MyAppCommand
类中定义为静态,但我们仍然无法将事件绑定到命令。原因是,当前 WPF 版本的 XAML 不允许我们以这种方式绑定事件处理程序。事件处理程序必须在 `MainWindow` 类中的代码隐藏文件中定义。我不知道这是 bug、意外遗漏的功能,还是我们根本不应该使用此功能,但这阻止了我们为所有命令的 Executed
和 CanExecute
事件定义一个集中式位置。
解决方案
解决此问题的方法是使用代码而不是 XAML 来定义绑定。让我们创建一个静态函数,该函数将命令及其各自的事件绑定到 MainWindow
窗口。
public static void BindCommandsToWindow(Window window)
{
window.CommandBindings.Add(
new CommandBinding(AddContact, AddContact_Executed, AddContact_CanExecute));
window.CommandBindings.Add(
new CommandBinding(EditContact, EditContact_Executed, EditContact_CanExecute));
window.CommandBindings.Add(
new CommandBinding(DeleteContact, DeleteContact_Executed, DeleteContact_CanExecute));
}
我们需要从 MainWindow.Loaded
事件调用此方法,并传入 MainWindow
实例
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
MyAppCommands.BindCommandsToWindow(this);
}
现在,尝试运行并执行代码。
瞧!它奏效了。
一个小技巧
您可以使用按钮的 CommandParameter
依赖项属性将参数发送给命令。由于数据以对象形式发送,因此您可以发送几乎任何您想要的内容。参数将发送到 CanExecute
和 Executed
事件。假设我们想通过代码设置 Edit
的 CanExecute
状态,以便在单击 Add 时,Edit 可用;在单击 Delete 时,Edit 不可用。为了清晰起见,我创建了一个名为 CommandParameter
的单独类,该类定义了一个 bool
属性 CanEditBeExecuted
,尽管不定义单独的类也可以实现。我们还定义了一个类型为 CommandParameter
的参数属性,并在 MainWindow
的构造函数中将其 CanEditBeExecuted
设置为 false
。
public class CommandParameter
{
public bool CanEditBeExecuted { get; set; }
}
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public CommandParameter parameter { get; set; }
public MainWindow()
{
InitializeComponent();
Loaded += new RoutedEventHandler(MainWindow_Loaded);
parameter = new CommandParameter();
parameter.CanEditBeExecuted = false;
}
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
MyAppCommands.BindCommandsToWindow(this);
}
}
现在,为按钮定义 CommandParameter
<Button HorizontalAlignment="Stretch"
Margin="20" Name="btnAdd"
VerticalAlignment="Stretch" Grid.Row="0"
Command="Commands:MyAppCommands.AddContact"
CommandParameter="{Binding ElementName=mainWindow, Path=parameter}">Add Contact
</Button>
<Button HorizontalAlignment="Stretch" Margin="20"
Name="btnEdit" VerticalAlignment="Stretch" Grid.Row="1"
Command="Commands:MyAppCommands.EditContact"
CommandParameter="{Binding ElementName=mainWindow, Path=parameter}">Edit Contact
</Button>
<Button HorizontalAlignment="Stretch" Margin="20"
Name="btnDelete" VerticalAlignment="Stretch" Grid.Row="2"
Command="Commands:MyAppCommands.DeleteContact"
CommandParameter="{Binding ElementName=mainWindow, Path=parameter}">Save Contact
</Button>
我们还更改了命令的 Execute
和 CanExecute
处理程序以反映我们的策略。
public static void AddContact_Executed(object sender, ExecutedRoutedEventArgs e)
{
CommandParameter parameter = e.Parameter as CommandParameter;
if (parameter != null)
parameter.CanEditBeExecuted = true;
MessageBox.Show("Add contact command executed");
}
public static void AddContact_CanExecute(object sender,
CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
public static void EditContact_Executed(object sender,
ExecutedRoutedEventArgs e)
{
MessageBox.Show("Edit contact command executed");
}
public static void EditContact_CanExecute(object sender,
CanExecuteRoutedEventArgs e)
{
CommandParameter parameter = e.Parameter as CommandParameter;
if (parameter != null)
e.CanExecute = parameter.CanEditBeExecuted;
else
e.CanExecute = false;
}
public static void DeleteContact_Executed(object sender,
ExecutedRoutedEventArgs e)
{
CommandParameter parameter = e.Parameter as CommandParameter;
if (parameter != null)
parameter.CanEditBeExecuted = false;
MessageBox.Show("Delete contact command executed");
}
public static void DeleteContact_CanExecute(object sender,
CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
如果您现在执行此代码,您会注意到编辑按钮没有反映 CanEditBeExecuted
属性的更改。如果您搜索网络,通常会找到的答案是,这是因为该属性在更改时无法通知按钮。解决方案是让 CommandParameter
继承自 INotifyPropertyChanged
,并在更改 CanEditBeExecuted
时引发属性更改事件。答案是否定的。您只需要这样做:在 InitializeComponent
之前实例化参数对象,编辑按钮将反映 CanEditBeExecuted
的更改。因此,如果您按以下方式更改构造函数,编辑按钮将反映 CanEditBeExecuted
的更改。
public MainWindow()
{
parameter = new CommandParameter();
InitializeComponent();
Loaded += new RoutedEventHandler(MainWindow_Loaded);
parameter.CanEditBeExecuted = false;
}
我之所以提到这个技巧,是因为我认为许多人会犯同样的错误,而且网上关于此问题的解决方案大多是误导性的。尽管实现 INotifyPropertyChanged
是个好主意,但仅仅因为上述原因而实现它则不是。
结论
希望那些希望将所有命令功能集中在一个静态类中的人会发现本文有用。请随时评论或指出错误。