在MVVM模式中务实使用代码隐藏

Sla*_*uma 19 .net wpf design-patterns code-behind mvvm

我试图尽可能地遵循WPF应用程序中的MVVM模式,主要是为了能够为我的ViewModel逻辑创建单元测试.

在大多数情况下,ViewModel属性和可视元素属性之间的数据绑定工作正常并且很容易.但有时我遇到的情况是,我无法看到明显和直接的方式,而从代码隐藏访问和操作控件的解决方案非常容易.

这是我的意思的一个例子:将文本片段插入TextBox当前插入符号位置

由于CaretIndex它不是依赖项属性,因此无法直接绑定到ViewModel的属性.以下是通过创建依赖项属性来解决此限制的解决方案.而这里是代码隐藏做到这一点的解决方案.在这种情况下,我更喜欢代码隐藏方式.我最近遇到的另一个问题是将动态的列集合绑定到WPF数据网格.在代码隐藏中编程非常简单明了.但对于MVVM友好的数据绑定方法,我只能在几个博客中找到解决方法,这些博客对我来说都很复杂,并且在一个或另一个方面有各种限制.

我不想不惜一切代价保持MVVM架构清除代码隐藏逻辑.如果工作量太大,MVVM友好的解决方案需要大量我不完全理解的代码(我仍然是WPF初学者)并且太耗时我更喜欢代码隐藏解决方案并牺牲我的应用程序的一些部分的自动可测试性.

出于上述实用的原因,我现在正在寻找"模式"来在应用程序中控制使用代码隐藏而不破坏MVVM架构或不破坏它.

到目前为止,我已经找到并测试了两种解决方案.我将使用Caret Position示例绘制粗略的草图:

解决方案1)通过抽象接口为ViewModel提供对View的引用

  • 我将有一个接口与视图实现的方法:

    public interface IView
    {
        void InsertTextAtCaretPosition(string text);
    }
    
    public partial class View : UserControl, IView
    {
        public View()
        {
            InitializeComponent();
        }
    
        // Interface implementation
        public void InsertTextAtCaretPosition(string text)
        {
            MyTextBox.Text = MyTextBox.Text.Insert(MyTextBox.CaretIndex, text);
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)
  • 将此接口注入ViewModel

    public class ViewModel : ViewModelBase
    {
        private readonly IView _view;
    
        public ViewModel(IView view)
        {
            _view = view;
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)
  • 通过接口方法从ViewModel的命令处理程序执行代码隐藏

    public ICommand InsertCommand { get; private set; }
    // Bound for instance to a button command
    
    // Command handler
    private void InsertText(string text)
    {
        _view.InsertTextAtCaretPosition(text);
    }
    
    Run Code Online (Sandbox Code Playgroud)

要创建View-ViewModel对,我将使用依赖注入来实例化具体的View并将其注入ViewModel.

解决方案2)通过事件执行代码隐藏方法

  • ViewModel是特殊事件的发布者,命令处理程序会引发这些事件

    public class ViewModel : ViewModelBase
    {
        public ViewModel()
        {
        }
    
        public event InsertTextEventHandler InsertTextEvent;
    
        // Command handler
        private void InsertText(string text)
        {
            InsertTextEventHandler handler = InsertTextEvent;
            if (handler != null)
                handler(this, new InsertTextEventArgs(text));
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)
  • View订阅了这些事件

    public partial class View : UserControl
    {
        public View()
        {
            InitializeComponent();
        }
    
        private void UserControl_Loaded(object sender, RoutedEventArgs e)
        {
            ViewModel viewModel = DataContext as ViewModel;
            if (viewModel != null)
                viewModel.InsertTextEvent += OnInsertTextEvent;
        }
    
        private void UserControl_Unloaded(object sender, RoutedEventArgs e)
        {
            ViewModel viewModel = DataContext as ViewModel;
            if (viewModel != null)
                viewModel.InsertTextEvent -= OnInsertTextEvent;
        }
    
        private void OnInsertTextEvent(object sender, InsertTextEventArgs e)
        {
            MyTextBox.Text = MyTextBox.Text.Insert(MyTextBox.CaretIndex, e.Text);
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

我不确定这些LoadedUnloaded事件UserControl是否是订阅和取消订阅事件的好地方,但我在测试期间找不到问题.

我在两个简单的例子中测试了这两种方法,它们似乎都有效.现在我的问题是:

  1. 您认为哪种方法更可取?我可能没有看到其中一种解决方案的任何好处或缺点吗?

  2. 您是否看到(也许还在练习)其他解决方案?

感谢您提前的反馈!

Ela*_*atz 23

专门针对这个问题

这个特定情况的最简单的解决方案是添加一个附加属性来执行它或一个行为.对于mvvm中大多数这些富有gui不支持的案例,行为可能是一个银弹.

至于一般情况

ViewModel在任何情况下都不应该知道视图,甚至不应该知道IView.在MVVM中它"始终查找",这意味着View可以看到@ VM,而VM可以查看模型.从来没有反过来.这样可以创建更好的可维护性,因为这样ViewModel不会做两件事(逻辑和gui的充实),但只有一件事.这是MVVM优于任何先前MV*模式的地方.

我也试图避免View以耦合的方式依赖ViewModel.这会产生丑陋的代码,并且两个类之间存在可分解的依赖关系,但有时这会更加务实,如您所说.更漂亮的方法是从ViewModel向View发送一条松散消息(例如MVVMLight中的Messenger,或Prism中的EventAggregator),因此两者之间没有强烈的依赖关系.有人认为这更好,尽管IMO仍然是依赖.

在某些情况下,在View中编写代码是可以的,这可能是其中一种情况.你可以使用附加行为来实现完美的解决方案,但是原则很重要,就像你问的那样.

当您需要非常丰富的GUI或UI没有正确的属性来绑定时,MVVM会出现问题.在这些情况下,您将采用以下三种方法之一:

  1. 附加行为.
  2. 从现有控件派生并添加您想要的属性.
  3. 实际上在View中编写代码.

所有这些方式都是合法的,但我已根据您应该首先采用的方式对它们进行排序.

总结一下

在MVVM中必须保留的最重要的事情不是保持代码隐藏,而是将所有逻辑和数据保留在ViewModel中,而View必须只包含与View相关的代码.建筑师告诉你不要写代码的原因只是因为它是一个滑坡.你开始写一些小东西,最后你会在View中做逻辑工作或维护应用程序状态,这是最大的禁忌.

快乐MVVMing :)


And*_*ich 7

开发WPF应用程序我发现两种方法都很有用.如果只需要一次从ViewModel到View的调用,那么带有事件处理程序的第二个选项看起来更简单,更好.但是如果你在这些层之间需要更复杂的接口,那么引入接口是有意义的.

我个人的偏好是恢复您的选项,并通过我的ViewModel实现IViewAware接口,并将此ViewModel注入View.看起来像一个选项三.

public interface IViewAware
{
    void ViewActivated();
    void ViewDeactivated();

    event Action CloseView;
}

public class TaskViewModel : ViewModelBase, IViewAware
{

    private void FireCloseRequest()
    {
        var handler = CloseView;
        if (handler != null)
            handler();
    }

    #region Implementation of IViewAware        
    public void ViewActivated()
    {
        // Do something 
    }

    public void ViewDeactivated()
    {
        // Do something 
    }

    public event Action CloseView;    
    #endregion
}
Run Code Online (Sandbox Code Playgroud)

这是一个简化的View代码:

    public View(IViewAware viewModel) : this()
    {
        _viewModel = viewModel;

        DataContext = viewModel;
        Loaded += ViewLoaded;

    }

    void ViewLoaded(object sender, RoutedEventArgs e)
    {
        Activated += (o, v) => _viewModel.ViewActivated();
        Deactivated += (o, v) => _viewModel.ViewDeactivated();

        _viewModel.CloseView += Close;
    }
Run Code Online (Sandbox Code Playgroud)

在实际应用中,我通常使用外部逻辑来连接V和VM,例如Attached Behaviors.