【问题标题】:How to implement commands to use ancestor methods in WPF?如何在 WPF 中实现使用祖先方法的命令?
【发布时间】:2012-05-09 02:01:16
【问题描述】:

我有这个上下文菜单资源:

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

我想在两个地方重复使用它。首先我试图把它放在DataGrid:

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

ContextMenu 本身可以正常工作,但是我现在使用 Executed="..." 会破坏应用程序并抛出:

“System.InvalidCastException”类型的第一次机会异常 发生在 PresentationFramework.dll 中

附加信息:无法转换类型的对象 'System.Reflection.RuntimeEventInfo' 键入 'System.Reflection.MethodInfo'。

如果我删除整个 Executed="..." 定义,那么代码就可以工作(并且命令什么都不做/变灰)。只要我右键单击网格/打开上下文菜单,就会引发异常。

DataGrid 被放置在几个元素之下,但最终它们都位于TabControl(称为MainTabs)之下,其中ItemsSource 设置为FooViewModels 的集合,其中@987654332 @ 我有一个方法HelpExecuted 我想被调用。

让我们想象一下:

  • TabControl (ItemsSource=ObservableCollection&lt;FooViewModel&gt;, x:Name=MainTabs)
    • 网格
      • 更多用户界面
        • DataGrid(带有上下文菜单集)

为什么会出现此错误,如何使上下文菜单命令“定位”FooViewModel 的 HelpExecuted 方法?

【问题讨论】:

  • 我认为 MainTabs 控件不包含 HelpExecuted 属性。它只包含一个 FooViewModel 列表。

标签: c# wpf xaml


【解决方案1】:

很遗憾,您不能为 ContextMenu 绑定 Executed,因为它是一个事件。另一个问题是ContextMenu 中不存在VisualTree,您的应用程序的其余部分存在。这两个问题都有解决方案。

首先,您可以使用ContextMenu 的父控件的Tag 属性来传递应用程序的DataContext。然后,您可以使用DelegateCommand 作为您的CommandBinding,然后就可以了。这是一个小示例,显示了您必须添加到项目中的 View、ViewModel 和 DelegateCommand 实现。

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!
    }
}

【讨论】:

  • 标签和委托真的是要走的路吗? Stipo 有一个可行且简短的方法来实现这一点,您的这种方法是否有 Stipo 没有的东西?我试图弄清楚哪种方法最有意义,因为这里给出的答案完全不同。 :)
  • 确实,Stipo 的答案应该可以解决您的问题。但是,如果我了解了有关 WPF 和 MVVM 的一件事:如果您没有充分的理由这样做,就不要使用代码隐藏,因为它会取消整个模式。这有时意味着要走一条非常艰难的路:)
  • 我不同意 MatthiasG 的说法,即代码隐藏取消了 MVVM 模式。 MVVM 中最重要的是所有应用程序逻辑和状态都应该在视图模型中,并且视图模型中没有直接引用视图。如果可能的话(如果 WPF/XAML API 支持),应该通过 XAML 来绑定视图和视图模型,否则必须通过代码来完成,就像我的回答一样。如果通过代码进行的绑定是重复的,则可以使用附加的行为将它们制作成 XAMLish。
  • 我更新了您的答案并修改了它以根据问题正常工作。感谢您提供初始代码;)
【解决方案2】:

这有帮助吗?

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

【讨论】:

    【解决方案3】:

    恐怕MatthiasG 打败了我。我的解决方案类似:

    这里的帮助命令由选项卡项的视图模型处理。将 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");
        }
    }
    

    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>
    

    替代视图模型,以便将帮助命令定向到根视图模型

    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; }
    }
    

    一个非常简单的 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;
    }
    

    【讨论】:

    • ActionCommand 在安装 Expression Blend 后可用。您需要添加对 Microsoft.Expression.Interactions 的引用。或者使用来自 PRISM 的 DelegateCommand 或来自 MVVM light 的 RelayCommand,或者实现您自己的。
    【解决方案4】:

    您收到此错误是因为 CommandBinding.Executed 不是依赖属性,因此您无法绑定到它。

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

    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();
        }
    }
    

    【讨论】:

    • 这很好用!您认为这种方法有什么缺点吗?到目前为止我得到的所有答案都完全不同,似乎没有明确的方法可以做到这一点......?
    • 我的方法实际上是使用 ApplicationsCommands.Help 命令 (msdn.microsoft.com/en-us/library/…),默认情况下您可以免费获得 Key Gesture 和 UI Text。使用 ApplicationCommands 的另一个好处是它们是众所周知的命令(通过 WPF API 公开),因此控件作者可以在他们的控件中使用它们,而无需知道谁处理它们(谁提供了命令逻辑的实际实现)。其他方法不使用 ApplicationCommands,并且通过 Tag 传输 DataContext 使代码不必要地复杂化。
    • 这是一个问题还是一个陈述?
    • 另外,为了保持更好的解耦,我建议使用定义HelpExecuted或HelpCommand的接口,而不是直接依赖(FooViewModel)
    • 如果您指的是驻留在视图模型中的自定义 ICommand,那么您是对的,CommandBinding 不起作用,它仅适用于 RoutedCommands。如果您不想使用 ApplicationCommands.Help,那么您应该使用 MatthiasG 的答案,但不会因为 ContextMenu 和 MenuItems 收到与 DataGrid 相同的 DataContext,所以不会通过 Tag 属性传输 DataContext 的复杂性,因此您可以这样写:
    【解决方案5】:

    可以创建一个适配器类,它可以配置为 XAML 中的资源,可以附加到控件以便在那里创建 CommandBindings,另一方面可以绑定到 ViewModel 中的方法,该方法应该是当命令由 Button 或 MenuItem 触发时调用。在这种情况下,命令将是 RoutedCommand,无论您是选择预定义的 WPF 命令之一还是在应用程序中创建自定义 RoutedCommand,都没有关系。

    绑定到方法的技巧是

    • 使适配器成为 Freezable,因此可以将当前 DataContext 用作绑定源,
    • 给它一个 Delegate 类型或其子类型之一的 DependencyProperty,并且
    • 使用一个转换器,该转换器接受方法名称作为 ConverterParameter 并检查绑定源类型,以便为应该由命令调用的方法创建一个委托。

    虽然这听起来很复杂,但好处是,一旦将框架的各个部分组合在一起,您就可以仅在 XAML 中简单地重用它们,并且在 ViewModel 或后面的代码中根本没有任何粘合代码。

    您可以想象,这需要一些基础设施,并且代码比我想在这里发布的要多。不过,我刚刚在我的博客中发表了一篇关于该主题的文章,http://wpfglue.wordpress.com/2012/05/07/commanding-binding-controls-to-methods/,通过博客您可以下载框架的完整源代码和 VB.Net 中的示例。

    应用于您的问题,XAML 将如下所示:

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-04-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-03-21
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多