Как установить CommandTarget для MenuItem внутри ContextMenu? - PullRequest
11 голосов
/ 05 марта 2009

(Этот вопрос относится к еще одному , но достаточно отличается, так что я думаю, что он заслуживает размещения здесь.)

Вот (в большой степени) 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 может быть динамически добавлено к Window. Возможно через кнопку в окне. Возможно, более проблематично из постоянного резервного хранилища при запуске приложения.

Как видно из xaml, я решил, что для меня имеет смысл попытаться использовать команды как способ обработки различных операций, которые пользователь может выполнять с Task s. Я делаю это с возможной целью преобразования всей командной логики в более формально определенный уровень контроллера, но я пытаюсь проводить рефакторинг по одному шагу за раз.

Проблема, с которой я сталкиваюсь, связана с взаимодействием между командой в UserControl ContextMenu и командой CanExecute, определенной в окне. Когда приложение запускается впервые и сохраненные задачи восстанавливаются в TaskStopwatches в окне, фактические элементы пользовательского интерфейса не выбираются. Если я сразу же щелкаю правой кнопкой мыши UserControl в Window в попытке выполнить команду ViewTaskProperties, обработчик CanExecute никогда не запускается, и пункт меню остается отключенным. Если я затем щелкаю некоторый элемент пользовательского интерфейса (например, кнопку) просто для того, чтобы сосредоточить внимание, обработчики CanExecute запускаются со свойством Source CanExecuteRoutedEventArgs, установленным в элемент пользовательского интерфейса, который имеет фокус.

В некотором отношении это поведение кажется известным - я узнал, что меню будут направлять событие через элемент, который в последний раз был в фокусе, чтобы всегда не отправлять событие из пункта меню. Однако я думаю, что мне хотелось бы, чтобы источником события был сам элемент управления или Задача, в которую оборачивается элемент управления (но Task не является Элементом, поэтому я не думаю, что это так). может быть источником).

Я подумал, что, возможно, мне не хватает свойства CommandTarget на MenuItem в UserControl, и я сначала подумал, что хочу, чтобы команда исходила из 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}}" />

И это тоже не удалось.

Одной неудачной попытки угадать и проверить, как это, достаточно, чтобы заставить меня отступить и понять, что я здесь упускаю какую-то фундаментальную концепцию. Так что мне делать?

  1. Правильно ли я понимаю роль CommandTarget в том смысле, что это обеспечивает механизм изменения источника команды?
  2. Как связать с MenuItem в UserControl.ContextMenu с владельцем UserControl? Или я что-то не так делаю просто потому, что чувствую необходимость?
  3. Является ли мое желание иметь контекст команды, заданный элементом, по которому щелкнули для создания контекстного меню, в отличие от элемента, который был в фокусе перед контекстным меню, неверен? Возможно, мне нужно написать собственную команду вместо использования RoutedUICommand:

    private static RoutedUICommand viewTaskPropertiesCommand = new RoutedUICommand("View a task's details.", "ViewTaskProperties", typeof(TaskCommands));
    public static RoutedUICommand ViewTaskProperties
    {
        get { return viewTaskPropertiesCommand; }
    }
    
  4. Есть ли в моем дизайне более глубокий фундаментальный недостаток? Это мой первый значительный проект WPF, и я делаю его в свое свободное время в качестве обучающего опыта, поэтому я определенно не против изучения превосходной архитектуры решения.

Ответы [ 2 ]

7 голосов
/ 05 марта 2009

1: Да, CommandTarget контролирует, откуда RoutedCommand начинает маршрутизацию.

2: ContextMenu имеет свойство PlacementTarget , которое позволит получить доступ к вашему UserControl:

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

Чтобы не повторять это в каждом пункте меню, вы можете использовать стиль.

3 & 4: Я бы сказал, что ваше желание разумно. Поскольку обработчик «Выполнить» находится в окне, это сейчас не имеет значения, но если бы у вас были разные области приложения, в каждом из которых был свой обработчик «Выполнить» для одной и той же команды, было бы важно, где был фокус.

2 голосов
/ 22 декабря 2010

Аналогичное решение, которое я нашел, использовало свойство Tag родительского объекта для получения текста данных:

<Grid Tag="{Binding Path=DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}}">
    <Grid.ContextMenu>
        <ContextMenu DataContext="{Binding Path=PlacementTarget.Tag, RelativeSource={RelativeSource Self}}">
            <MenuItem 
                Header="{Binding Path=ToolbarDelete, Mode=Default, Source={StaticResource Resx}}" 
                Command="{Binding RemoveCommand}" 
                CommandParameter="{Binding DataContext.Id, RelativeSource={RelativeSource TemplatedParent}}"/>
        </ContextMenu>
    </Grid.ContextMenu>

    <TextBlock Text="{Binding Name}" Padding="2" />

</Grid>
...