Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement commands to use ancestor methods in WPF?

Tags:

c#

wpf

xaml

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 FooViewModels, 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?

like image 863
Tower Avatar asked Apr 28 '12 16:04

Tower


4 Answers

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!
    }
}
like image 60
MatthiasG Avatar answered Sep 23 '22 01:09

MatthiasG


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>
like image 43
Josh Avatar answered Sep 23 '22 01:09

Josh


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;
}
like image 35
Phil Avatar answered Sep 20 '22 01:09

Phil


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();
    }
}
like image 45
Stipo Avatar answered Sep 21 '22 01:09

Stipo