简体   繁体   中英

How to implement commands to use ancestor methods in WPF?

I have this context menu resource:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <ContextMenu x:Key="FooContextMenu">
        <ContextMenu.CommandBindings>
            <CommandBinding Command="Help" Executed="{Binding ElementName=MainTabs, Path=HelpExecuted}" />
        </ContextMenu.CommandBindings>

        <MenuItem Command="Help">
            <MenuItem.Icon>
                <Image Source="../Resources/Icons/Help.png" Stretch="None" />
            </MenuItem.Icon>
        </MenuItem>
    </ContextMenu>
</ResourceDictionary>

I want to re-use it in two places. Firstly I'm trying to put it in a DataGrid :

<DataGrid ContextMenu="{DynamicResource FooContextMenu}">...

The ContextMenu itself works fine, but with the Executed="..." I have right now breaks the application and throws:

A first chance exception of type 'System.InvalidCastException' occurred in PresentationFramework.dll

Additional information: Unable to cast object of type 'System.Reflection.RuntimeEventInfo' to type 'System.Reflection.MethodInfo'.

If I remove the entire Executed="..." definition, then the code works (and the command does nothing/grayed out). The exception is thrown as soon as I right click the grid/open the context menu.

The DataGrid is placed under a few elements, but eventually they all are below a TabControl (called MainTabs ) which has ItemsSource set to a collection of FooViewModel s, and in that FooViewModel I have a method HelpExecuted which I want to be called.

Let's visualize:

  • TabControl ( ItemsSource=ObservableCollection<FooViewModel> , x:Name=MainTabs )
    • Grid
      • More UI
        • DataGrid (with context menu set)

Why am I getting this error and how can I make the context menu command to "target" the FooViewModel 's HelpExecuted method?

Does this help?

<ContextMenu>
    <ContextMenu.ItemContainerStyle>
       <Style TargetType="MenuItem">
          <Setter Property="Command" Value="{Binding RelativeSource={RelativeSource AncestorType={x:Type TabItem}}, Path=HelpExecuted}" />
       </Style>
    </ContextMenu.ItemContainerStyle>
    <MenuItem Header="Help" />
</ContextMenu>

Unfortunately you cannot bind Executed for a ContextMenu as it is an event. An additional problem is that the ContextMenu does not exist in the VisualTree the rest of your application exists. There are solutions for both of this problems.

First of all you can use the Tag property of the parent control of the ContextMenu to pass-through the DataContext of your application. Then you can use an DelegateCommand for your CommandBinding and there you go. Here's a small sample showing View , ViewModel and the DelegateCommand implementation you would have to add to you project.

DelegateCommand.cs

public class DelegateCommand : ICommand
{
    private readonly Action<object> execute;
    private readonly Predicate<object> canExecute;

    public DelegateCommand(Action<object> execute)
        : this(execute, null)
    { }

    public DelegateCommand(Action<object> execute, Predicate<object> canExecute)
    {
        if (execute == null)
            throw new ArgumentNullException("execute");

        this.execute = execute;
        this.canExecute = canExecute;
    }

    #region ICommand Members

    [DebuggerStepThrough]
    public bool CanExecute(object parameter)
    {
        return canExecute == null ? true : canExecute(parameter);
    }

    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }

    public void Execute(object parameter)
    {
        execute(parameter);
    }

    #endregion
}

MainWindowView.xaml

<Window x:Class="Application.MainWindowView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindowView" Height="300" Width="300"
        x:Name="MainWindow">
    <Window.Resources>
        <ResourceDictionary>
            <ContextMenu x:Key="FooContextMenu">
                <MenuItem Header="Help" Command="{Binding PlacementTarget.Tag.HelpExecuted, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
            </ContextMenu>
        </ResourceDictionary>
    </Window.Resources>
    <Grid>
        <TabControl ItemsSource="{Binding FooViewModels}" x:Name="MainTabs">
            <TabControl.ContentTemplate>
                <DataTemplate>
                    <DataGrid ContextMenu="{DynamicResource FooContextMenu}" Tag="{Binding}" />
                </DataTemplate>
            </TabControl.ContentTemplate>
        </TabControl>
    </Grid>
</Window>

MainWindowView.xaml.cs

public partial class MainWindowView : Window
{
    public MainWindowView()
    {
        InitializeComponent();
        DataContext = new MainWindowViewModel();
    }
}

MainWindowViewModel.cs

public class MainWindowViewModel
{
    public ObservableCollection<FooViewModel> FooViewModels { get; set; }

    public MainWindowViewModel()
    {
        FooViewModels = new ObservableCollection<FooViewModel>();
    }
}

FooViewModel.cs

public class FooViewModel
{
    public ICommand HelpExecuted { get; set; }

    public FooViewModel()
    {
        HelpExecuted = new DelegateCommand(ShowHelp);
    }

    private void ShowHelp(object obj)
    {
        // Yay!
    }
}

I'm afraid MatthiasG beat me to it. My solution is similar:

Here the Help command is handled by the tab item's view model. It would be simple to pass a reference to the TestViewModel to each of the TestItemViewModel and have ShowHelp call back into TestViewModel if required.

public class TestViewModel
{
    public TestViewModel()
    {
        Items = new List<TestItemViewModel>{ 
                    new TestItemViewModel(), new TestItemViewModel() };
    }

    public ICommand HelpCommand { get; private set; }

    public IList<TestItemViewModel> Items { get; private set; }
}

public class TestItemViewModel
{
    public TestItemViewModel()
    {
        // Expression Blend ActionCommand
        HelpCommand = new ActionCommand(ShowHelp);
        Header = "header";
    }

    public ICommand HelpCommand { get; private set; }

    public string Header { get; private set; }

    private void ShowHelp()
    {
        Debug.WriteLine("Help item");
    }
}

The xaml

<Window.Resources>
    <ContextMenu x:Key="FooMenu">
        <MenuItem Header="Help" Command="{Binding HelpCommand}"/>
    </ContextMenu>
    <DataTemplate x:Key="ItemTemplate">
        <!-- context menu on header -->
        <TextBlock Text="{Binding Header}" ContextMenu="{StaticResource FooMenu}"/>
    </DataTemplate>
    <DataTemplate x:Key="ContentTemplate">
        <Grid Background="#FFE5E5E5">
            <!-- context menu on data grid -->
            <DataGrid ContextMenu="{StaticResource FooMenu}"/>
        </Grid>
    </DataTemplate>
</Window.Resources>

<Window.DataContext>
    <WpfApplication2:TestViewModel/>
</Window.DataContext>

<Grid>
    <TabControl 
        ItemsSource="{Binding Items}" 
        ItemTemplate="{StaticResource ItemTemplate}" 
        ContentTemplate="{StaticResource ContentTemplate}" />
</Grid>

Alternative view models so that the help command is directed to the root view model

public class TestViewModel
{
    public TestViewModel()
    {
        var command = new ActionCommand(ShowHelp);

        Items = new List<TestItemViewModel>
                    {
                        new TestItemViewModel(command), 
                        new TestItemViewModel(command)
                    };
    }

    public IList<TestItemViewModel> Items { get; private set; }

    private void ShowHelp()
    {
        Debug.WriteLine("Help root");
    }
}

public class TestItemViewModel
{
    public TestItemViewModel(ICommand helpCommand)
    {
        HelpCommand = helpCommand;
        Header = "header";
    }

    public ICommand HelpCommand { get; private set; }

    public string Header { get; private set; }
}

A very simple implementation of ActionCommand

public class ActionCommand : ICommand
{
    private readonly Action _action;

    public ActionCommand(Action action)
    {
        if (action == null)
        {
            throw new ArgumentNullException("action");
        }

        _action = action;
    }

    public bool CanExecute(object parameter)
    {
        return true;
    }

    public void Execute(object parameter)
    {
        _action();
    }

    // not used
    public event EventHandler CanExecuteChanged;
}

You are getting this error because CommandBinding.Executed is not dependency property so you cannot bind to it.

Instead, use ResourceDictionary code behind to specify event handler for CommandBinding.Executed event, and in the event handler code call FooViewModel.HelpExecuted() method like this:

MainWindowResourceDictionary.xaml

<ResourceDictionary x:Class="WpfApplication.MainWindowResourceDictionary" 
                    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:WpfApplication">

    <DataTemplate DataType="{x:Type local:FooViewModel}">
        <Grid>
            <DataGrid ContextMenu="{DynamicResource FooContextMenu}"/>
        </Grid>
    </DataTemplate>

    <ContextMenu x:Key="FooContextMenu">
        <ContextMenu.CommandBindings>
            <CommandBinding Command="Help" Executed="HelpExecuted"/>
        </ContextMenu.CommandBindings>
        <MenuItem Command="Help"/>
    </ContextMenu>

</ResourceDictionary>

MainWindowResourceDictionary.xaml.cs

public partial class MainWindowResourceDictionary : ResourceDictionary
{
    public MainWindowResourceDictionary()
    {
        InitializeComponent();
    }

    private void HelpExecuted(object sender, ExecutedRoutedEventArgs e)
    {
        var fooViewModel = (FooViewModel)((FrameworkElement)e.Source).DataContext;
        fooViewModel.HelpExecuted();
    }
}

It is possible to create an adapter class which can be configured as a resource in XAML, can be attached to a Control in order to create CommandBindings there, and on the other end can bind to a method in the ViewModel which should be called when the command is triggered by a Button or MenuItem. The command in this case would be a RoutedCommand, and it wouldn't matter whether you choose one of the predefined WPF commands or create a custom RoutedCommand in your application.

The trick for binding to a method is

  • Making the adapter a Freezable, so one can use the current DataContext as a binding source,
  • Giving it a DependencyProperty of type Delegate or one of its subtypes, and
  • Using a converter which accepts the method name as a ConverterParameter and inspects the binding sources type in order to create a Delegate for the method that should be invoked by the command.

While this sounds complex, the good thing is that once you have the parts of the framework together, you can simply reuse them in XAML only, and you won't have any glue code at all in either ViewModel or code behind.

As you can imagine, this takes some infrastructure, and the code is more than I would like to post here. However, I've just published an article in my blog on the subject, http://wpfglue.wordpress.com/2012/05/07/commanding-binding-controls-to-methods/ , and through the blog you can download complete source code for the framework and an example in VB.Net .

Applied to your problem, the XAML would then look like this:

In the definition of the ContextMenu:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ContextMenu x:Key="FooContextMenu">
    <!-- No CommandBindings needed here -->
    <MenuItem Command="Help">
        <MenuItem.Icon>
            <Image Source="../Resources/Icons/Help.png" Stretch="None" />
        </MenuItem.Icon>
    </MenuItem>
</ContextMenu>
</ResourceDictionary>

And in the definition of the DataGrid

<DataGrid c:Commanding.CommandSet="{DynamicResource helpCommand}">
    <DataGrid.Resources>
        <f:ActionConverter x:Key="actionConverter"/>
        <c:ActionCommand x:Key="helpCommand" Command="Help" ExecutedAction="{Binding Converter={StaticResource actionConverter}, ConverterParameter=HelpExecuted}"/>
<!-- DataGrid definition continued... -->

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM