WPF 架构混乱 RE:路由命令、事件和手势

发布于 2024-09-30 09:15:53 字数 701 浏览 0 评论 0原文

在学习 WPF 的过程中,我阅读了大量的书籍和网站。我似乎一直回避的一件事是我们应该如何正确连接 RoutedCommands。在一篇文章中,作者指出 XAML 文件的代码隐藏应该只包含对 InitializeComponent 的调用。我可以支持这一点。它使 XAML 文件只不过是一个演示文稿文档,并满足了我对关注点分离的邪恶渴望。

另一方面,我见过的每个解决双击事件的示例似乎都希望您编写代码。据我了解,我们希望摆脱代码隐藏文件中的重复代码(并且,我再次支持这一点),所以在我看来,这不是正确的方法。菜单命令、工具栏按钮单击等也是如此。

例如,想象一下我有一个打开文档的命令。该命令必须显示“打开”对话框,然后打开文档并将其缓存在应用程序状态中。 (此应用程序仅允许您一次处理一个文档。)用户可以通过以下任一方式调用此命令:

  • 从菜单中选择“文件”->“打开”。
  • 键入 Ctrl+O。
  • 单击工具栏上的“打开”按钮。

如果我信任 Web 上的大多数源,则必须编写至少两个 Click 事件处理程序,然后调用该命令,从而污染代码隐藏文件。在我看来,这似乎违背了拥有命令的目的。我认为在某处读到,有一种方法可以在 XAML 中以声明方式将命令绑定到这些东西,它会为您执行此操作,甚至在命令无法执行时禁用该命令。但现在我似乎找不到它,也找不到如何做到这一点的好例子。

有人可以向我解释一下吗?此时一切都开始看起来像巫术和弹片。

In learning WPF, I've been reading scads of books and web sites. One thing that seems to keep evading me is how we are supposed to properly wire up RoutedCommands. In one article, the author pointed out that the codebehind for your XAML files is supposed to contain nothing more than the call to InitializeComponent. I can get behind that. It makes the XAML file nothing more than a presentation document, and satisifies my unholy craving for separation of concerns.

On the other hand, every sample I've seen that addresses double-click events seems to want you to write code. It was my understanding that we wanted to get away from having repetitive code in the codebehind files (and, again, I'm all for that), so it seems to me that that's not the right way to do it. The same is true of menu commands, toolbar button clicks, and so forth.

Imagine, for instance, that I have a command to open a document. That command has to present the Open dialog, then open the document and cache it in the application state. (This application only allows you to work on one document at a time.) The user can invoke this command by either:

  • Choosing File->Open from the menu.
  • Typing Ctrl+O.
  • Clicking the Open button on the toolbar.

If I trust most of the sources on the Web, I have to write at least two Click event handlers that then invoke the command, polluting the codebehind file. That seems, to me, to defeat the purpose of having the Commands. I thought that I read somewhere that there was a way to bind the command to these things declaratively in XAML and it would do it for you, even disabling the command if it couldn't execute. But now I can't seem to find it, nor a decent example of how to do it.

Could someone please explain this to me? It's all starting to look like voodoo and shrapnel at this point.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

同展鸳鸯锦 2024-10-07 09:15:53

避免命令代码隐藏的常用方法是避免使用 RoutedCommands。在 MVVM(模型-视图-视图模型)主题的各种变体中,人们倾向于使用 ICommand 的自定义实现。他们编写了一个放置在 UI 的 DataContext 中的 ViewModel 类。该ViewModel公开了ICommand类型的属性,并且这些命令属性通过数据绑定连接到菜单项、按钮等。 (它通常只是反复使用的 ICommand 的一个实现 - 在网络上搜索 RelayCommand 或 DelegateCommand 或 DelegatingCommand,你会看到这种模式 - 它基本上是 ICommand 作为委托的包装器,并且可选支持启用/disabled。)

在这个习惯用法中,您几乎从不使用内置命令,例如 ApplicationCommands.Open。这些东西的唯一真正用途是如果您希望焦点敏感命令由控件本质上处理。例如,文本框内置了用于编辑、复制、粘贴等的命令处理。这避免了代码隐藏问题,因为它是一个完整的自定义控件,而自定义控件实际上并不具有代码隐藏本身 - 它们都是代码。 (Xaml 实际上位于一个完全独立的对象(模板)中,并且实际上并不是控件的一部分。)并且在任何情况下,它都不是您的代码 - 您拥有一个已经知道如何支持命令的控件,因此您可以完全保留在此处的 Xaml 内。

命令路由在该特定场景中很有趣,因为它允许您放置一组与各种编辑控件关联的菜单项,并且路由系统会根据焦点所在位置确定哪个文本框(或其他内容)将处理命令。如果这不是您想要的,那么命令路由可能对您没有多大用处。

然而,这里有一个更大的问题:当您发现确实必须将代码放在代码隐藏中时该怎么办。如果您使用自定义 ICommand 实现,命令通常不是该场景的示例(尽管有奇怪的例外),但稍微更有趣的用户输入事件是。您提到了双击,而且,如果您正在执行任何类型的不寻常的交互操作,您往往需要诸如鼠标向上/向下之类的操作。

在这种情况下,通常的方法是硬着头皮将代码放在代码隐藏中,但您尝试将每个事件处理程序保留为一行。基本上,您的代码隐藏只是调用视图模型上的相关方法,这才是真正处理事件的方法。

这样做的可爱之处在于它使编写自动化测试变得非常容易。想要模拟鼠标进入 UI 的特定部分吗?无需搞乱单元测试 UI 自动化框架 - 只需直接调用相关方法即可!

The usual way to avoid codebehind with commands is to avoid RoutedCommands. In the various variations on the MVVM (Model-View-ViewModel) theme, people tend to use custom implementations of ICommand. They write a ViewModel class that is placed in the DataContext of the UI. This ViewModel exposes properties of type ICommand, and those command properties are connected to menu items, buttons and so on through data binding. (And it's usually just the one implementation of ICommand used over and over again - search the web for either RelayCommand or DelegateCommand or DelegatingCommand, and you'll see the pattern - it's basically ICommand as a wrapper around a delegate, with optional support for enabled/disabled.)

In this idiom, you almost never use the built-in commands like ApplicationCommands.Open. The only real use for those things is if you want focus-sensitive commands to be handled intrinsically by controls. E.g., the TextBox has built in command handling for Edit, Copy, Paste, and so on. This avoids the codebehind issue because it's a full custom control, and custom controls don't really have codebehind as such - they're all code. (The Xaml is actually in a completely separate object, the Template, and isn't really part of the control.) And in any case, it's not your code - you have a control that already knows how to support the command, so you can keep entirely within the Xaml here.

Command routing is interesting in that particular scenario because it lets you put one set of menu items associated with the various editing controls, and the routing system figures out which textbox (or whatever) will handle the command based on where the focus is. If that's not what you want, command routing probably isn't much use to you.

However, there's a bigger issue here of what to do when you find that you really do have to put code in the codebehind. Commands usually aren't an example of that scenario if you use custom ICommand implementations (although there's the odd exception), but the slightly more interesting user input events are. You mention double click, but also, if you're doing any kind of unusual interactivity you tend to want things like mouse up/down and so on.

In this case, the usual approach is to bite the bullet and put code in the codebehind, but you try to keep it to one line per event handler. Basically, your codebehind just calls into the relevant method on the viewmodel, and that's what really handles the event.

The lovely thing about that is it makes it really easy to write automated tests. Want to simulate the mouse entering a particular part of your UI? No need to mess around with unit test UI automation frameworks - just call the relevant method directly!

杯别 2024-10-07 09:15:53

在WPF中命令是相当麻烦的,但是它确实为你解决了更新IsEnabled的问题。这是典型的例子。第 1 步是可选的,因为有很多内置常用命令 减少样板文件的数量。

第 1 步(可选)在静态类中创建命令

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Input;

namespace WpfApplication1
{
   public static class Commands
   {
      public static RoutedCommand WibbleCommand = new RoutedUICommand
      (
         "Wibble",
         "Wibble",
         typeof(Commands),
         new InputGestureCollection()
            {
               new KeyGesture(Key.O, ModifierKeys.Control)
            }
      );
   }
}

第 2 步:在 xaml 中声明命令绑定

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApplication1"
        Title="MainWindow" Height="350" Width="525">
   <Window.CommandBindings>
      <CommandBinding
         Command="{x:Static local:Commands.WibbleCommand}"
         Executed="WibbleCommandExecuted"
         CanExecute="WibbleCommandCanExecute"
      />
   </Window.CommandBindings>

第 3 步:连接控件(菜单项、按钮等)。

此处的长绑定是为了纠正 Button 默认情况下不会出现的情况。不要使用命令文本。

  <Button Command="{x:Static local:Commands.WibbleCommand}" Width="200" Height="80">
     <TextBlock Text="{Binding Path=Command.Text, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Button}}}">
     </TextBlock>
  </Button>

第 4 步:在代码隐藏中实现 Execute 和 CanExecute 的处理程序

小心 CanExecute!这会经常被调用,所以尽量不要在这里做任何昂贵的事情。

  private void WibbleCommandExecuted(object sender, ExecutedRoutedEventArgs e)
  {
     MessageBox.Show("Wibbled!");
  }

  private void WibbleCommandCanExecute(object sender, CanExecuteRoutedEventArgs e)
  {
     e.CanExecute = DateTime.Now.Minute % 2 == 0;
  }

Commanding in WPF is quite cumbersome, but it does solve the problem of updating IsEnabled for you. Here's the canonical example. Step 1 is optional because there are a lot of built-in common commands to reduce the amount of boiler-plate.

Step 1. (Optional) Create your command in a static class

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Input;

namespace WpfApplication1
{
   public static class Commands
   {
      public static RoutedCommand WibbleCommand = new RoutedUICommand
      (
         "Wibble",
         "Wibble",
         typeof(Commands),
         new InputGestureCollection()
            {
               new KeyGesture(Key.O, ModifierKeys.Control)
            }
      );
   }
}

Step 2: Declare command bindings in the xaml

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApplication1"
        Title="MainWindow" Height="350" Width="525">
   <Window.CommandBindings>
      <CommandBinding
         Command="{x:Static local:Commands.WibbleCommand}"
         Executed="WibbleCommandExecuted"
         CanExecute="WibbleCommandCanExecute"
      />
   </Window.CommandBindings>

Step 3: Wire up your controls (menuitems, buttons etc)

The long binding here is to rectify the fact that Button by default won't use the command text.

  <Button Command="{x:Static local:Commands.WibbleCommand}" Width="200" Height="80">
     <TextBlock Text="{Binding Path=Command.Text, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Button}}}">
     </TextBlock>
  </Button>

Step 4: Implement handlers for Execute and CanExecute in codebehind

Careful with CanExecute! This will be called quite often, so try not to do anything expensive here.

  private void WibbleCommandExecuted(object sender, ExecutedRoutedEventArgs e)
  {
     MessageBox.Show("Wibbled!");
  }

  private void WibbleCommandCanExecute(object sender, CanExecuteRoutedEventArgs e)
  {
     e.CanExecute = DateTime.Now.Minute % 2 == 0;
  }
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文