【问题标题】:Implementing MVVM in WPF without using System.Windows.Input.ICommand在 WPF 中实现 MVVM 而不使用 System.Windows.Input.ICommand
【发布时间】:2009-02-02 14:15:25
【问题描述】:

我正在尝试使用 MVVM (Model-View-ViewModel) 模式实现 WPF 应用程序,并且我希望将 View 部件与 Model 和 ViewModel 部件(DLL)放在单独的程序集中(EXE)中.

这里的转折是保持模型/视图模型程序集清除任何 WPF 依赖项。这样做的原因是我想从具有不同(非 WPF)UI 技术的可执行文件中重用它,例如 Mono 下的 WinForms 或 GTK#。

默认情况下,这是无法做到的,因为 ViewModel 公开了一个或多个 ICommand。但是ICommand类型是在System.Windows.Input命名空间中定义的,属于WPF!

那么,有没有不使用ICommand也能满足WPF绑定机制的方法呢?

谢谢!

【问题讨论】:

  • @aoven:当你 8 个月前问这个问题时,我和你一样,想知道你最后做了什么,它对你的效果如何。干杯

标签: wpf mvvm


【解决方案1】:

您应该能够在 wpf 层和单个命令处理程序类中定义单个 WPF 自定义路由命令。您的所有 WPF 类都可以使用适当的参数绑定到这一命令。

然后,处理程序类可以将命令转换为您自己在 ViewModel 层中定义的自定义命令接口,并且独立于 WPF。

最简单的例子是使用 Execute 方法封装 void 委托。

所有不同的 GUI 层只需要在一个位置从其本机命令类型转换为您的自定义命令类型。

【讨论】:

    【解决方案2】:

    WinForms 没有使用 MVVM 样式视图模型所需的丰富数据绑定和命令基础架构。

    就像您不能在客户端应用程序中重用 Web 应用程序 MVC 控制器一样(至少不能在没有创建大量包装器和适配器的情况下,最终只会使编写和调试代码变得更加困难,而不会为客户提供任何价值) 您不能在 WinForms 应用程序中重用 WPF MVVM。

    我没有在实际项目中使用 GTK#,所以我不知道它能做什么或不能做什么,但我怀疑 MVVM 无论如何都不是 GTK# 的最佳方法。

    尝试将应用程序的尽可能多的行为转移到模型中,拥有一个仅公开来自模型的数据并基于在视图模型中没有逻辑的命令调用模型的视图模型。

    那么对于 WinForms 只需移除视图模型并直接从 UI 调用模型,或者创建另一个基于 WinForms 的中间层更有限的数据绑定支持。

    重复 GTK# 或编写 MVC 控制器和视图,为模型提供 Web 前端。

    不要试图将一种技术强加为针对另一种技术优化的使用模式,不要从头开始编写自己的命令基础架构(我以前做过,不是我最高效的选择),使用最好的工具每种技术。

    【讨论】:

      【解决方案3】:

      我需要一个这样的例子,所以我用各种技术写了一个。

      我有几个设计目标

      1 - 保持简单

      2 - 视图中绝对没有代码隐藏(Window 类)

      3 - 演示 ViewModel 类库中仅系统引用的依赖关系。

      4 - 将业务逻辑保留在 ViewModel 中并直接路由到适当的方法,而无需编写一堆“存根”方法。

      这是代码...

      App.xaml(没有 StartupUri 是唯一值得注意的)

      <Application 
          x:Class="WpfApplicationCleanSeparation.App" 
          xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
      </Application>
      

      App.xaml.cs(加载主视图)

      using System.Windows;
      using WpfApplicationCleanSeparation.ViewModels;
      
      namespace WpfApplicationCleanSeparation
      {
          public partial class App
          {
              protected override void OnStartup(StartupEventArgs e)
              {
                  var view = new MainView();
                  var viewModel = new MainViewModel();
      
                  view.InitializeComponent();
                  view.DataContext = viewModel;
                  CommandRouter.WireMainView(view, viewModel);
                  view.Show();
              }
          }
      }
      

      CommandRouter.cs(魔法)

      using System.Windows.Input;
      using WpfApplicationCleanSeparation.ViewModels;
      
      namespace WpfApplicationCleanSeparation
      {
          public static class CommandRouter
          {
              static CommandRouter()
              {
                  IncrementCounter = new RoutedCommand();
                  DecrementCounter = new RoutedCommand();
              }
      
              public static RoutedCommand IncrementCounter { get; private set; }
              public static RoutedCommand DecrementCounter { get; private set; }
      
              public static void WireMainView(MainView view, MainViewModel viewModel)
              {
                  if (view == null || viewModel == null) return;
      
                  view.CommandBindings.Add(
                      new CommandBinding(
                          IncrementCounter,
                          (λ1, λ2) => viewModel.IncrementCounter(),
                          (λ1, λ2) =>
                              {
                                  λ2.CanExecute = true;
                                  λ2.Handled = true;
                              }));
                  view.CommandBindings.Add(
                      new CommandBinding(
                          DecrementCounter,
                          (λ1, λ2) => viewModel.DecrementCounter(),
                          (λ1, λ2) =>
                              {
                                  λ2.CanExecute = true;
                                  λ2.Handled = true;
                              }));
              }
          }
      }
      

      MainView.xaml(没有代码隐藏,直接删除!)

      <Window 
          x:Class="WpfApplicationCleanSeparation.MainView" 
          xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
          xmlns:WpfApplicationCleanSeparation="clr-namespace:WpfApplicationCleanSeparation" 
          Title="MainWindow" 
          Height="100" 
          Width="100">
          <StackPanel>
              <TextBlock Text="{Binding Counter}"></TextBlock>
              <Button Content="Decrement" Command="WpfApplicationCleanSeparation:CommandRouter.DecrementCounter"></Button>
              <Button Content="Increment" Command="WpfApplicationCleanSeparation:CommandRouter.IncrementCounter"></Button>
          </StackPanel>
      </Window>
      

      MainViewModel.cs(也包括实际的模型,因为这个例子非常简单,请原谅 MVVM 模式的出轨。

      using System.ComponentModel;
      
      namespace WpfApplicationCleanSeparation.ViewModels
      {
          public class CounterModel
          {
              public int Data { get; private set; }
      
              public void IncrementCounter()
              {
                  Data++;
              }
      
              public void DecrementCounter()
              {
                  Data--;
              }
          }
      
          public class MainViewModel : INotifyPropertyChanged
          {
              private CounterModel Model { get; set; }
              public event PropertyChangedEventHandler PropertyChanged = delegate { };
      
              public MainViewModel()
              {
                  Model = new CounterModel();
              }
      
              public int Counter
              {
                  get { return Model.Data; }
              }
      
              public void IncrementCounter()
              {
                  Model.IncrementCounter();
      
                  PropertyChanged(this, new PropertyChangedEventArgs("Counter"));
              }
      
              public void DecrementCounter()
              {
                  Model.DecrementCounter();
      
                  PropertyChanged(this, new PropertyChangedEventArgs("Counter"));
              }
          }
      }
      

      只是一个快速和肮脏的,我希望它对某人有用。我通过各种 Google 看到了几种不同的方法,但没有什么比我想要的尽可能少的代码更简单易实现的了。如果有进一步简化的方法,请告诉我,谢谢。

      快乐编码:)

      编辑:为了简化我自己的代码,您可能会发现这对于将添加到单行中很有用。

          private static void Wire(this UIElement element, RoutedCommand command, Action action)
          {
              element.CommandBindings.Add(new CommandBinding(command, (sender, e) => action(), (sender, e) => { e.CanExecute = true; }));
          }
      

      【讨论】:

      • 我在 SO 的某个地方遇到了 lambda 字符语法,我很想表扬:它一直困扰着我。
      【解决方案4】:

      抱歉,戴夫,我不太喜欢您的解决方案。首先,您必须在代码中手动编写每个命令的管道,然后您必须配置 CommandRouter 以了解应用程序中的每个视图/视图模型关联。

      我采取了不同的方法。

      我有一个 Mvvm 实用程序程序集(它没有 WPF 依赖项)并且我在我的视图模型中使用它。在该程序集中,我声明了一个自定义 ICommand 接口和一个实现该接口的 DelegateCommand 类。

      namespace CommonUtil.Mvvm
      {
          using System;
      
      
          public interface ICommand
          {
              void Execute(object parameter);
              bool CanExecute(object parameter);
      
              event EventHandler CanExecuteChanged;
          }
      
          public class DelegateCommand : ICommand
          {
              public DelegateCommand(Action<object> execute) : this(execute, null)
              {
      
              }
      
              public DelegateCommand(Action<object> execute, Func<object, bool> canExecute)
              {
                  _execute = execute;
                  _canExecute = canExecute;
              }
      
              public void Execute(object parameter)
              {
                  _execute(parameter);
              }
      
              public bool CanExecute(object parameter)
              {
                  return _canExecute == null || _canExecute(parameter);
              }
      
      
              public event EventHandler CanExecuteChanged;
      
              private readonly Action<object> _execute;
              private readonly Func<object, bool> _canExecute;
          }
      }
      

      我还有一个 Wpf 库程序集(它确实引用了系统 WPF 库),我从我的 WPF UI 项目中引用了它。在该程序集中,我声明了一个具有标准 System.Windows.Input.ICommand 接口的 CommandWrapper 类。 CommandWrapper 是使用我的自定义 ICommand 的实例构造的,并且只是将 Execute、CanExecute 和 CanExecuteChanged 直接委托给我的自定义 ICommand 类型。

      namespace WpfUtil
      {
          using System;
          using System.Windows.Input;
      
      
          public class CommandWrapper : ICommand
          {
              // Public.
      
              public CommandWrapper(CommonUtil.Mvvm.ICommand source)
              {
                  _source = source;
                  _source.CanExecuteChanged += OnSource_CanExecuteChanged;
                  CommandManager.RequerySuggested += OnCommandManager_RequerySuggested;
              }
      
              public void Execute(object parameter)
              {
                  _source.Execute(parameter);
              }
      
              public bool CanExecute(object parameter)
              {
                  return _source.CanExecute(parameter);
              }
      
              public event System.EventHandler CanExecuteChanged = delegate { };
      
      
              // Implementation.
      
              private void OnSource_CanExecuteChanged(object sender, EventArgs args)
              {
                  CanExecuteChanged(sender, args);
              }
      
              private void OnCommandManager_RequerySuggested(object sender, EventArgs args)
              {
                  CanExecuteChanged(sender, args);
              }
      
              private readonly CommonUtil.Mvvm.ICommand _source;
          }
      }
      

      在我的 Wpf 程序集中,我还创建了一个 ValueConverter,当传递我的自定义 ICommand 的一个实例时,它会吐出一个与 Windows.Input.ICommand 兼容的 CommandWrapper 的实例。

      namespace WpfUtil
      {
          using System;
          using System.Globalization;
          using System.Windows.Data;
      
      
          public class CommandConverter : IValueConverter
          {
      
              public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
              {
                  return new CommandWrapper((CommonUtil.Mvvm.ICommand)value);
              }
      
              public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
              {
                  throw new System.NotImplementedException();
              }
          }
      }
      

      现在,我的视图模型可以将命令公开为我的自定义命令类型的实例,而不必依赖 WPF,并且我的 UI 可以使用我的 ValueConverter 将 Windows.Input.ICommand 命令绑定到这些视图模型,就像这样。 (已省略 XAML 命名空间垃圾邮件)。

      <Window x:Class="Project1.MainWindow">
      
          <Window.Resources>
              <wpf:CommandConverter x:Key="_commandConv"/>
          </Window.Resources>
      
          <Grid>
              <Button Content="Button1" Command="{Binding CustomCommandOnViewModel,
                                              Converter={StaticResource _commandConv}}"/>
          </Grid>
      
      </Window>
      

      现在,如果我真的很懒(我是),并且懒得每次都手动应用 CommandConverter,那么在我的 Wpf 程序集中我可以像这样创建自己的 Binding 子类:

      namespace WpfUtil
      {
          using System.Windows.Data;
      
      
          public class CommandBindingExtension : Binding
          {
              public CommandBindingExtension(string path) : base(path)
              {
                  Converter = new CommandConverter();
              }
          }
      }
      

      所以现在我可以更简单地绑定到我的自定义命令类型:

      <Window x:Class="Project1.MainWindow"
                      xmlns:wpf="clr-namespace:WpfUtil;assembly=WpfUtil">
      
          <Window.Resources>
              <wpf:CommandConverter x:Key="_commandConv"/>
          </Window.Resources>
      
          <Grid>
              <Button Content="Button1" Command="{wpf:CommandBinding CustomCommandOnViewModel}"/>
          </Grid>
      
      </Window>
      

      【讨论】:

      • +1,别抱歉,我希望在 2 年内出现更聪明的人,这就是 SO 的用途;)。刚刚浏览了您的答案,但我喜欢增加的抽象以及它带来的东西。 “我可以像这样创建自己的 Binding 子类”= 非常性感,我喜欢它;)
      【解决方案5】:

      不是 VM 公开命令,而是公开方法。然后使用附加行为将事件绑定到方法,或者如果您需要命令,请使用可以委托给这些方法并通过附加行为创建命令的 ICommand。

      【讨论】:

        【解决方案6】:

        当然这是可能的。您可以创建另一个抽象级别。 添加您自己的与 ICommand 相似或相同的 IMyCommand 接口并使用它。

        看看我当前的 MVVM 解决方案,它解决了您提到的大多数问题,但它完全从特定于平台的事物中抽象出来并且可以重用。此外,我没有使用任何代码隐藏,仅与实现 ICommand 的 DelegateCommands 绑定。 Dialog 基本上是一个 View - 一个单独的控件,它有自己的 ViewModel,它从主屏幕的 ViewModel 显示,但通过 DelagateCommand 绑定从 UI 触发。

        在此处查看完整的 Silverlight 4 解决方案 Modal dialogs with MVVM and Silverlight 4

        【讨论】:

        • 回答演示此方法here
        【解决方案7】:

        我认为你在错误的地方分离了你的项目。我认为你应该只分享你的模型和业务逻辑类。

        VM 是模型的改编,以适应 WPF 视图。我会保持 VM 简单并做到这一点。

        我无法想象在 Winforms 上强制执行 MVVM。 OTOH 只有模型和业务逻辑,如果需要,您可以将它们直接注入表单。

        【讨论】:

          【解决方案8】:

          "您不能在 WinForms 应用程序中重用 WPF MVVM"

          这个请看 url http://waf.codeplex.com/ ,我在 Win Form 中使用过 MVVM,现在每当我想将应用程序的演示从 Win Form 升级到 WPF 时,它将在应用程序逻辑没有变化的情况下进行更改,

          但是我在 Asp.net MVC 中重用 ViewModel 有一个问题,所以我可以在 Web 中制作相同的 Desktop win 应用程序,而无需或更少更改应用程序逻辑..

          谢谢...

          【讨论】:

            猜你喜欢
            • 2013-01-16
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2017-04-26
            • 2016-12-08
            • 1970-01-01
            • 1970-01-01
            • 2012-09-17
            相关资源
            最近更新 更多