views:

3117

answers:

1

(This question is related to another one, but different enough that I think it warrants placement here.)

Here's a (heavily snipped) 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>

and here's a (heavily snipped) 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>

Through various techniques, a UserControl can be dynamically added to the Window. Perhaps via the Button in the window. Perhaps, more problematically, from a persistent backing store when the application is started.

As can be seen from the xaml, I've decided that it makes sense for me to try to use Commands as a way to handle various operations that the user can perform with Tasks. I'm doing this with the eventual goal of factoring all command logic into a more formally-defined Controller layer, but I'm trying to refactor one step at a time.

The problem that I'm encountering is related to the interaction between the command in the UserControl's ContextMenu and the command's CanExecute, defined in the Window. When the application first starts and the saved Tasks are restored into TaskStopwatches on the Window, no actual UI elements are selected. If I then immediately r-click a UserControl in the Window in an attempt to execute the ViewTaskProperties command, the CanExecute handler never runs and the menu item remains disabled. If I then click some UI element (e.g., the button) just to give something focus, the CanExecute handlers are run with the CanExecuteRoutedEventArgs's Source property set to the UI element that has the focus.

In some respect, this behavior seems to be known-- I've learned that menus will route the event through the element that last had focus to avoid always sending the event from the menu item. What I think I would like, though, is for the source of the event to be the control itself, or the Task that the control is wrapping itself around (but Task isn't an Element, so I don't think it can be a source).

I thought that maybe I was missing the CommandTarget property on the MenuItem in the UserControl, and my first thought was that I wanted the command to come from the UserControl, so naturally I first tried:

<MenuItem x:Name="mnuProperties" 
          Header="_Properties" 
          Command="{x:Static localcommands:TaskCommands.ViewTaskProperties}" 
          CommandTarget="{Binding ElementName=This}" />

This failed as an invalid binding. I'm not sure why. Then I thought, "Hmmm, I'm looking up the tree, so maybe what I need is a RelativeSource" and I tried this:

<MenuItem x:Name="mnuProperties" 
          Header="_Properties" 
          Command="{x:Static localcommands:TaskCommands.ViewTaskProperties}" 
          CommandTarget="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:TaskStopwatchControl}}}" />

That also failed, but when I looked at my xaml again, I realized that the ContextMenu is in a property of the UserControl, it's not a child element. So I guessed (and at this point it was a guess):

<MenuItem x:Name="mnuProperties" 
          Header="_Properties" 
          Command="{x:Static localcommands:TaskCommands.ViewTaskProperties}" 
          CommandTarget="{Binding RelativeSource={x:Static RelativeSource.Self}}" />

And that also failed.

One failed guess-and-check like this is enough to make me back off and realize that I'm missing some sort of fundamental concept here, though. So what do I do?

  1. Is my understanding re: the role of CommandTarget correct in that this provides a mechanism to modify the source of a command?
  2. How do I bind from a MenuItem in UserControl.ContextMenu to the owning UserControl? Or am I doing something wrong simply because I perceive a need to?
  3. Is my desire to have the context of a command set by the element that was clicked to generate the context menu, as opposed to the element that had focus before the context menu, incorrect? Perhaps I need to write my own command instead of using the RoutedUICommand:

    private static RoutedUICommand viewTaskPropertiesCommand = new RoutedUICommand("View a task's details.", "ViewTaskProperties", typeof(TaskCommands));
    public static RoutedUICommand ViewTaskProperties
    {
        get { return viewTaskPropertiesCommand; }
    }
    
  4. Is there some deeper fundamental flaw in my design? This is my first significant WPF project, and I'm doing it on my own time as a learning experience, so I'm definitely not opposed to learning a superior solution architecture.

+3  A: 

1: Yes, CommandTarget controls where the RoutedCommand starts routing from.

2: ContextMenu has a PlacementTarget property that will allow access to your UserControl:

<MenuItem x:Name="mnuProperties" Header="_Properties"
          Command="{x:Static localcommands:TaskCommands.ViewTaskProperties}"
          CommandTarget="{Binding PlacementTarget,
                                  RelativeSource={RelativeSource FindAncestor,
                                                                 AncestorType={x:Type ContextMenu}}}"/>

To avoid repeating this in every MenuItem you could use a Style.

3 & 4: I would say your desire is reasonable. Since the Execute handler is on the Window it doesn't matter right now, but if you had different regions of the application, each with their own Execute handler for the same command, it would matter where the focus was.

Robert Macnee
Learn something new every day! I never ran into PlacementTarget before. :) Thanks, Robert. I'll update further when I get an opportunity to work on it again. (Hopefully later tonight)
Greg D