(这个问题与另一个问题有关,但差异很大,我认为它值得放在这里。)
这是一个(严重剪断)Window
:
<Window x:Class="Gmd.TimeTracker2.TimeTrackerMainForm"
xmlns:local="clr-namespace:Gmd.TimeTracker2"
xmlns:localcommands="clr-namespace:Gmd.TimeTracker2.Commands"
x:Name="This"
DataContext="{Binding ElementName=This}">
<Window.CommandBindings>
<CommandBinding Command="localcommands:TaskCommands.ViewTaskProperties"
Executed="HandleViewTaskProperties"
CanExecute="CanViewTaskPropertiesExecute" />
</Window.CommandBindings>
<DockPanel>
<!-- snip stuff -->
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- snip more stuff -->
<Button Content="_Create a new task" Grid.Row="1" x:Name="btnAddTask" Click="HandleNewTaskClick" />
</Grid>
</DockPanel>
</Window>
这是一个(严重剪断)UserControl
:
<UserControl x:Class="Gmd.TimeTracker2.TaskStopwatchControl"
xmlns:local="clr-namespace:Gmd.TimeTracker2"
xmlns:localcommands="clr-namespace:Gmd.TimeTracker2.Commands"
x:Name="This"
DataContext="{Binding ElementName=This}">
<UserControl.ContextMenu>
<ContextMenu>
<MenuItem x:Name="mnuProperties" Header="_Properties" Command="{x:Static localcommands:TaskCommands.ViewTaskProperties}"
CommandTarget="What goes here?" />
</ContextMenu>
</UserControl.ContextMenu>
<StackPanel>
<TextBlock MaxWidth="100" Text="{Binding Task.TaskName, Mode=TwoWay}" TextWrapping="WrapWithOverflow" TextAlignment="Center" />
<TextBlock Text="{Binding Path=ElapsedTime}" TextAlignment="Center" />
<Button Content="{Binding Path=IsRunning, Converter={StaticResource boolToString}, ConverterParameter='Stop Start'}" Click="HandleStartStopClicked" />
</StackPanel>
</UserControl>
通过各种技术,UserControl
可以将 a 动态添加到Window
. 也许通过窗口中的按钮。也许,更有问题的是,当应用程序启动时,来自持久性后备存储。
从 xaml 可以看出,我认为尝试使用命令来处理用户可以使用Task
s 执行的各种操作对我来说是有意义的。我这样做的最终目标是将所有命令逻辑分解为更正式定义的控制器层,但我试图一次重构一个步骤。
我遇到的问题与UserControl
's中的命令和Window 中定义ContextMenu
的 command's之间的交互有关。CanExecute
当应用程序第一次启动并将保存的任务恢复到窗口上的 TaskStopwatches 时,没有选择实际的 UI 元素。如果我随后立即 r-单击 aUserControl
以Window
尝试执行ViewTaskProperties
命令,则CanExecute
处理程序永远不会运行并且菜单项保持禁用状态。如果我然后单击某个 UI 元素(例如,按钮)只是为了获得焦点,则CanExecute
处理程序将在运行时将CanExecuteRoutedEventArgs
的 Source 属性设置为具有焦点的 UI 元素。
在某些方面,这种行为似乎是已知的——我了解到菜单将通过最后一个具有焦点的元素路由事件,以避免总是从菜单项发送事件。不过,我想我想要的是事件的源是控件本身,或者是控件环绕的任务(但Task
不是元素,所以我认为它不能是来源)。
我想也许我错过了 中的属性CommandTarget
,我的第一个想法是我希望命令来自 UserControl,所以我自然首先尝试了:MenuItem
UserControl
<MenuItem x:Name="mnuProperties"
Header="_Properties"
Command="{x:Static localcommands:TaskCommands.ViewTaskProperties}"
CommandTarget="{Binding ElementName=This}" />
这作为无效绑定失败。我不确定为什么。然后我想,“嗯,我正在查找树,所以也许我需要的是一个 RelativeSource”,然后我尝试了这个:
<MenuItem x:Name="mnuProperties"
Header="_Properties"
Command="{x:Static localcommands:TaskCommands.ViewTaskProperties}"
CommandTarget="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:TaskStopwatchControl}}}" />
那也失败了,但是当我再次查看我的 xaml 时,我意识到它ContextMenu
位于 UserControl 的属性中,它不是子元素。所以我猜到了(在这一点上这是一个猜测):
<MenuItem x:Name="mnuProperties"
Header="_Properties"
Command="{x:Static localcommands:TaskCommands.ViewTaskProperties}"
CommandTarget="{Binding RelativeSource={x:Static RelativeSource.Self}}" />
这也失败了。
不过,像这样一次失败的猜测和检查就足以让我退缩并意识到我在这里遗漏了某种基本概念。那我该怎么办?
- 我的理解是:
CommandTarget
正确的作用在于它提供了一种修改命令源的机制吗? - 如何从
MenuItem
in绑定UserControl.ContextMenu
到 owningUserControl
?还是我做错了什么仅仅是因为我觉得有必要? 我希望通过单击以生成上下文菜单的元素设置命令的上下文,而不是在上下文菜单之前具有焦点的元素,这不正确吗?也许我需要编写自己的命令而不是使用
RoutedUICommand
:private static RoutedUICommand viewTaskPropertiesCommand = new RoutedUICommand("View a task's details.", "ViewTaskProperties", typeof(TaskCommands)); public static RoutedUICommand ViewTaskProperties { get { return viewTaskPropertiesCommand; } }
我的设计中是否存在一些更深层次的基本缺陷?这是我的第一个重要的 WPF 项目,我在自己的时间做这件事作为学习经验,所以我绝对不反对学习卓越的解决方案架构。