在不使用System.Windows.Input.ICommand的情况下在WPF中实现MVVM

时间:2009-02-02 14:15:26

标签: wpf mvvm

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

这里的扭曲是保持Model / ViewModel程序集不受任何WPF依赖项的影响。原因是我想从具有不同(非WPF)UI技术的可执行文件中重用它,例如Mono下的WinForms或GTK#。

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

那么,有没有办法在不使用ICommand的情况下满足WPF绑定机制?

谢谢!

8 个答案:

答案 0 :(得分:7)

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

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

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

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

答案 1 :(得分:5)

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

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

我没有在一个真实的项目中使用GTK#所以我不知道它能做什么或不能做什么,但我怀疑MVVM不是GTK#的最佳方法。

尝试将应用程序的大部分行为移动到模型中,使视图模型仅公开模型中的数据,并根据视图模型中没有逻辑的命令调用模型。

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

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

不要试图强制一种技术进入针对他人优化的使用模式,不要从头开始编写自己的命令基础架构(我以前做过,而不是我最有成效的选择),使用最好的工具对于每种技术。

答案 2 :(得分:4)

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

我有一些设计目标

1 - 保持简单

2 - 视图中完全没有代码隐藏(Window类)

3 - 演示了ViewModel类库中只有System引用的依赖关系。

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

Proof

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

快乐编码:)

编辑:为了简化我自己的代码,你可能会发现这对于将Adds变成单行很有用。

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

答案 3 :(得分:3)

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

我采取了不同的方法。

我有一个Mvvm实用程序程序集(没有WPF依赖项),我在viewmodel中使用它。在该程序集中,我声明了一个自定义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项目中引用它。在该程序集中,我声明了一个CommandWrapper类,它具有标准的System.Windows.Input.ICommand接口。 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();
        }
    }
}

现在我的viewmodels可以将命令公开为我的自定义命令类型的实例,而不必依赖于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>

现在,如果我真的很懒(我就是这样),并且每次都在我的Wpf程序集中手动应用CommandConverter时,我可以创建我自己的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>

答案 4 :(得分:2)

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

答案 5 :(得分:2)

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

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

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

答案 6 :(得分:1)

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

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

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

答案 7 :(得分:0)

“你不能在WinForms应用程序中重用WPF MVVM”

为此,请参阅url http://waf.codeplex.com/,我已经在Win Form中使用了MVVM,现在当我想将应用程序的演示文稿从Win Form升级到WPF时,它将被更改而不会改变应用程序逻辑,

但是我在Asp.net MVC中重用ViewModel有一个问题,所以我可以在Web上制作相同的桌面获胜应用程序而不需要或者更少应用程序逻辑的更改。

...谢谢