简体   繁体   English

如何在WPF中实现命令以使用祖先方法?

[英]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

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

The ContextMenu itself works fine, but with the Executed="..." I have right now breaks the application and throws: ContextMenu本身工作正常,但使用Executed="..."我现在打破了应用程序并抛出:

A first chance exception of type 'System.InvalidCastException' occurred in PresentationFramework.dll PresentationFramework.dll中出现“System.InvalidCastException”类型的第一次机会异常

Additional information: Unable to cast object of type 'System.Reflection.RuntimeEventInfo' to type 'System.Reflection.MethodInfo'. 附加信息:无法将类型为“System.Reflection.RuntimeEventInfo”的对象强制转换为“System.Reflection.MethodInfo”。

If I remove the entire Executed="..." definition, then the code works (and the command does nothing/grayed out). 如果我删除整个Executed="..."定义,那么代码就可以工作(并且命令不会执行任何操作/灰显)。 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. DataGrid放在几个元素下面,但最终它们都在TabControl (称为MainTabsMainTabs ,它将ItemsSource设置为FooViewModel的集合,并且在FooViewModel我有一个我想要调用的方法HelpExecuted

Let's visualize: 让我们想象一下:

  • TabControl ( ItemsSource=ObservableCollection<FooViewModel> , x:Name=MainTabs ) TabControl( ItemsSource=ObservableCollection<FooViewModel>x:Name=MainTabs
    • Grid
      • More UI 更多UI
        • DataGrid (with context menu set) DataGrid(带上下文菜单集)

Why am I getting this error and how can I make the context menu command to "target" the FooViewModel 's HelpExecuted method? 为什么我会收到此错误,如何使上下文菜单命令“定位” FooViewModelHelpExecuted方法?

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. 遗憾的是,您无法为ContextMenu绑定Executed ,因为它是一个事件。 An additional problem is that the ContextMenu does not exist in the VisualTree the rest of your application exists. 另一个问题是,在应用程序的其余部分存在的VisualTree中, ContextMenu不存在。 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. 首先,您可以使用ContextMenu的父控件的Tag属性来传递应用程序的DataContext Then you can use an DelegateCommand for your CommandBinding and there you go. 然后你可以使用DelegateCommand为你的CommandBinding ,你去。 Here's a small sample showing View , ViewModel and the DelegateCommand implementation you would have to add to you project. 这是一个小样本,显示了您必须添加到项目中的ViewViewModelDelegateCommand实现。

DelegateCommand.cs 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 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 MainWindowView.xaml.cs

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

MainWindowViewModel.cs MainWindowViewModel.cs

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

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

FooViewModel.cs 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. 我担心MatthiasG会打败我。 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. 如果需要,将TestViewModel的引用传递给每个TestItemViewModel并让ShowHelp回调到TestViewModel会很简单。

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 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 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. 您收到此错误,因为CommandBinding.Executed不是依赖项属性,因此您无法绑定它。

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: 相反,使用后面的ResourceDictionary代码为CommandBinding.Executed事件指定事件处理程序,并在事件处理程序代码中调用FooViewModel.HelpExecuted()方法,如下所示:

MainWindowResourceDictionary.xaml 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 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. 可以创建一个适配器类,可以将其配置为XAML中的资源,可以附加到Control以在那里创建CommandBindings,另一端可以绑定到ViewModel中的一个方法,该方法应该在命令由Button或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. 在这种情况下,命令将是RoutedCommand,无论您是选择其中一个预定义的WPF命令还是在应用程序中创建自定义RoutedCommand都无关紧要。

The trick for binding to a method is 绑定到方法的技巧是

  • Making the adapter a Freezable, so one can use the current DataContext as a binding source, 使适配器成为Freezable,因此可以使用当前的DataContext作为绑定源,
  • Giving it a DependencyProperty of type Delegate or one of its subtypes, and 给它一个Delegate类型或它的一个子类型的DependencyProperty,和
  • 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. 使用转换器接受方法名称作为ConverterParameter并检查绑定源类型,以便为应该由命令调用的方法创建委托。

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. 虽然这听起来很复杂,但好处是,一旦你将框架的各个部分放在一起,你就可以只在XAML中重复使用它们,并且在ViewModel或后面的代码中根本就没有任何粘合代码。

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 . 但是,我刚刚在我的博客上发表了一篇关于这个主题的文章, http://wpfglue.wordpress.com/2012/05/07/commanding-binding-controls-to-methods/ ,你可以通过博客下载完整的框架源代码和VB.Net中的示例。

Applied to your problem, the XAML would then look like this: 应用于您的问题,XAML将如下所示:

In the definition of the ContextMenu: 在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的定义中

<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... -->

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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