MVVM 模式中代码隐藏的实用使用

发布于 2024-10-29 15:47:14 字数 3287 浏览 2 评论 0原文

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

在大多数情况下,ViewModel 属性和可视元素属性之间的数据绑定工作正常且简单。但有时我会遇到这样的情况:我看不到明显而直接的方法,而从代码隐藏访问和操作控件的解决方案却非常简单。

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

由于 CaretIndex 不是依赖属性它不能直接绑定到 ViewModel 的属性。 这里是通过创建依赖属性来解决此限制的解决方案。 这里是执行此操作的解决方案在代码隐藏中。在这种情况下我更喜欢代码隐藏的方式。我最近遇到的另一个问题是将动态列集合绑定到 WPF 数据网格。在代码隐藏中进行编程清晰且简单。但对于 MVVM 友好的数据绑定方法,我只能在几个博客中找到解决方法,这些博客对我来说都看起来相当复杂,并且在一个或另一个方面都有各种限制。

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

出于上述的务实原因,我现在正在寻找“模式”,以便在应用程序中受控地使用代码隐藏,而不会破坏 MVVM 架构或不会对其造成太多破坏。

到目前为止,我已经找到并测试了两种解决方案。我将用插入符位置示例绘制粗略的草图:

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

  • 我将拥有一个接口,其中包含将由视图实现的方法:

    公共接口IView
    {
        无效InsertTextAtCaretPosition(字符串文本);
    }
    
    公共部分类视图:UserControl、IView
    {
        公共视图()
        {
            初始化组件();
        }
    
        // 接口实现
        公共无效InsertTextAtCaretPosition(字符串文本)
        {
            MyTextBox.Text = MyTextBox.Text.Insert(MyTextBox.CaretIndex, 文本);
        }
    }
    
  • 将此接口注入 ViewModel

    公共类 ViewModel :ViewModelBase
    {
        私有只读 IView _view;
    
        公共ViewModel(IView视图)
        {
            _view = 视图;
        }
    }
    
  • 通过接口方法从 ViewModel 的命令处理程序执行代码隐藏

    public ICommand InsertCommand { get;私人套装; }
    // 例如绑定到按钮命令
    
    // 命令处理程序
    私有无效InsertText(字符串文本)
    {
        _view.InsertTextAtCaretPosition(文本);
    }
    

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

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

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

    公共类 ViewModel :ViewModelBase
    {
        公共视图模型()
        {
        }
    
        公共事件InsertTextEventHandler InsertTextEvent;
    
        // 命令处理程序
        私有无效InsertText(字符串文本)
        {
            InsertTextEventHandler 处理程序 = InsertTextEvent;
            if(处理程序!= null)
                处理程序(这,新的InsertTextEventArgs(文本));
        }
    }
    
  • View 订阅这些事件

    公共部分类视图:UserControl
    {
        公共视图()
        {
            初始化组件();
        }
    
        private void UserControl_Loaded(对象发送者, RoutedEventArgs e)
        {
            ViewModel viewModel = DataContext 作为 ViewModel;
            if (viewModel != null)
                viewModel.InsertTextEvent += OnInsertTextEvent;
        }
    
        私有无效UserControl_Unloaded(对象发送者,RotedEventArgs e)
        {
            ViewModel viewModel = DataContext 作为 ViewModel;
            if (viewModel != null)
                viewModel.InsertTextEvent -= OnInsertTextEvent;
        }
    
        私有无效OnInsertTextEvent(对象发送者,InsertTextEventArgs e)
        {
            MyTextBox.Text = MyTextBox.Text.Insert(MyTextBox.CaretIndex, e.Text);
        }
    }
    

我不确定 UserControlLoadedUnloaded 事件是否良好订阅和取消订阅事件的地方,但我在测试过程中找不到问题。

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

  1. 您认为哪种方法更好?其中一种解决方案是否有我可能看不到的优点或缺点?

  2. 您是否看到(并且可能实践)其他解决方案?

感谢您提前提供反馈!

I'm trying to follow the MVVM pattern in a WPF application as good as I can, mainly to be able to create unit tests for my ViewModel logic.

In most cases data binding between ViewModel properties and properties of visual elements works fine and is easy. But sometimes I encounter situations where I cannot see an obvious and straightforward way while a solution to access and manipulate controls from code-behind is very easy.

Here is an example of what I mean: Inserting a text fragment into a TextBox at the current caret position

Since CaretIndex isn't a dependency property it can't be bound directly to a ViewModel's property. Here is a solution to work around this limitation by creating a dependency property. And here is the solution to do this in code-behind. I would prefer the code-behind way in this situation. Another problem I recently had was binding a dynamic collection of columns to a WPF datagrid. It was clear and simple to program in code-behind. But for a MVVM-friendly databinding approach I could only find work arounds in several blogs which all looked quite complex to me and had various limitations in one or the other aspect.

I don't want to keep the MVVM architecture clean of code-behind logic at all costs. If the amount of work arounds is too big, a MVVM-friendly solution requires a lot of code which I don't fully understand (I'm still a WPF beginner) and is too time consuming I prefer a code-behind solution and sacrifice automatic testability of a few parts of my application.

For the mentioned pragmatic reasons I am looking now for "patterns" to make controlled use of code-behind in an application without breaking the MVVM architecture or without breaking it too much.

Up to now I've found and tested two solutions. I will draw rough sketches with the Caret Position example:

Solution 1) Give the ViewModel a reference to the View through an abstract interface

  • I would have an interface with methods which would be implemented by the 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);
        }
    }
    
  • Inject this interface into the ViewModel

    public class ViewModel : ViewModelBase
    {
        private readonly IView _view;
    
        public ViewModel(IView view)
        {
            _view = view;
        }
    }
    
  • Execute code-behind from a ViewModel's command handler through the interface methods

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

To create a View-ViewModel pair I would use dependency injection to instantiate the concrete View and inject it into the ViewModel.

Solution 2) Execute code-behind methods through events

  • The ViewModel is publisher of special events and command handlers raise those events

    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));
        }
    }
    
  • The View subscribes to these events

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

I am not sure if the Loaded and Unloaded events of the UserControl are good places to subscribe and unsubscribe to the events but I couldn't find problems during test.

I have tested both approaches in two simple examples and they both seem to work. Now my questions are:

  1. Which approach do you think is preferable? Are there any benefits or downsides of one of the solutions which I possibly don't see?

  2. Do you see (and perhaps practice) other solutions?

Thank you for feedback in advance!

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

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

发布评论

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

评论(7

甜嗑 2024-11-05 15:47:14

专门针对此问题

对于此特定情况,最简单的解决方案是添加一个附加属性来执行此操作,或添加一个行为。对于 MVVM 中大多数不支持丰富 GUI 的情况,行为可能是灵丹妙药。

对于一般情况

ViewModel 在任何情况下都不应该知道视图。甚至与 IView 无关。在 MVVM 中,它是“始终向上查找”,这意味着视图可以查看 VM,VM 也可以查看模型。从来没有相反。
这创造了更好的可维护性,因为这样 ViewModel 不会做两件事(负责逻辑和 gui),而只做一件事。这就是 MVVM 优于任何先前 MV* 模式的地方。

我还会尝试避免让 View 以耦合方式依赖 ViewModel。这会创建丑陋的代码,以及两个类之间的可破坏的依赖关系,但有时正如您所说,这更务实。
更漂亮的方法是从 ViewModel 向 View 发送松散消息(例如 MVVMLight 中的 Messenger 或 Prism 中的 EventAggregator),因此两者之间不存在强依赖性。有些人认为这更好,尽管在我看来这仍然是一种依赖。

在某些情况下,在视图中编写代码是可以的,这可能就是其中一种情况。您可以使用附加行为实现完美的解决方案,但正如您所要求的,原则很重要。

当您需要非常丰富的 GUI 或 UI 没有要绑定的正确属性时,MVVM 就会出现问题。
在这些情况下,你会求助于以下三件事之一:

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

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

总结

在 MVVM 中,最重要的事情不是保持代码隐藏自由,而是保持所有逻辑和逻辑。将数据传输到 ViewModel,因为 View 必须仅包含与 View 相关的代码。架构师告诉你根本不要编写代码隐藏的原因只是因为这是一个滑坡。您开始编写一些小内容,最终会在视图中执行逻辑操作或维护应用程序状态,这是最大的禁忌。

快乐的 MVVMing :)

Specifically for this problem

The simplest solution to this specific case is adding an Attached Property to do it, or a Behavior. Behaviors can be a silver bullet for most of these rich-gui-not-supported cases in MVVM.

As for the general case

ViewModel should never ever under any circumstance know about the view. Not even about an IView. In MVVM it's "always look up", which means a View can look at the VM, and the VM can look at the Model. Never the other way around.
This creates much better maintainability, since this way the ViewModel doesn't do two things (in charge of logic AND the gui), but only one thing. This is where MVVM is superior to any prior MV* pattern.

I would also try to refrain from having the View rely on the ViewModel in a coupled way. This creates ugly code, and a breakable dependency between the two classes, but sometimes this is more pragmatic as you said.
A prettier way is to send a Loose Message (e.g. Messenger in MVVMLight, or EventAggregator in Prism) from the ViewModel to the View, and thus there is no strong dependency between the two. Some think this is better although IMO this is still a dependency.

Writing code in the View is OK in some situations, and this could be one of those situation. You could achieve a perfect solution using Attached-Behaviors, but the principle is important, like you asked.

MVVM is problematic when you need GUI that is very rich or the UI doesn't have the right Properties to bind to.
In those situations you would resort to one of three things:

  1. Attached Behaviors.
  2. Deriving from existing controls and adding the properties you'd like.
  3. Actually writing code in the View.

All of those ways are legitimate, but I've ordered them according to what you should resort to first.

To Summarize

The most important thing you have to keep in MVVM is not to keep the code-behind free, but to keep all logic & data to the ViewModel, as the View must only contain View-related code. The reason architects tell you not to write code-behind at all is only because it's a slippery slope. You start writing something small, and you end up doing logical stuff or maintaining application state in the View, which is the big no-no.

Happy MVVMing :)

时光暖心i 2024-11-05 15:47:14

开发 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
}

这是您的视图的简化代码:

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

在实际应用程序中,我通常使用外部逻辑来连接 V 和 VM,例如附加行为。

Developing WPF applications I found both ways useful. If you need just one call from ViewModel to View, the second option, with event handler, looks simpler and good enough. But if you are requiring more complex interface between these layers, then it makes sense to introduce interface.

And my personal preference is to revert your option one and have a IViewAware interface implemented by my ViewModel and inject this ViewModel into View. Looks like an option three.

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
}

And this a simplified code for your 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;
    }

In real application I usually use an external logic to connect V and VM, for example Attached Behaviors.

往事随风而去 2024-11-05 15:47:14

我会尽量避免让 ViewModel 引用视图。

在这种情况下执行此操作的方法是:

从 TextBox 派生并添加一个依赖属性,该属性通过订阅 OnSelectionChanged 事件来包装 CaretIndex,该事件让您知道插入符号已移动。

这样,ViewModel 就能够通过绑定到插入符号来了解插入符号的位置。

I'd try to keep away from having the ViewModel a reference to the View.

A way of doing that in this case:

Derive from TextBox and add a dependency property that wraps the CaretIndex by subscribing to the OnSelectionChanged event that lets you know the caret has moved.

This way the ViewModel is able to know where the caret is by binding to it.

天暗了我发光 2024-11-05 15:47:14

当控件很难与 MVVM 兼容时,您通常需要使用隐藏代码中的控件。在这种情况下,您可以使用 Blend SDK 中的 AttachedProperties、EventTriggers、Behaviors 来扩展控件的功能。但我经常使用继承来扩展控制功能并使其更兼容 MVVM。您可以创建自己的一组从具有实现的视图功能的基础继承的控件。这种方法的一大优点是您可以访问 ControlTemplate 控件,这通常是实现特定视图功能所必需的。

Often you need work with controls from code behind when the control is hardly compotible with MVVM. In this case you can use AttachedProperties, EventTriggers, Behaviors from blend SDK to extend functionality of the control. But very often I use inheritance to extend functionality of control and make it more MVVM compatible. You can create own set of controls inherited from base with implemented view functionality. A big advantage of this approach is that you can access ControlTemplate controls, it often neccessary to implement specific view functionality.

我为君王 2024-11-05 15:47:14

在我看来,第一个选择更好。它仍然保持视图和视图模型之间的分离(通过视图接口),并将事物保留在其逻辑位置。事件的使用不太直观。

我赞成在无法通过绑定实现或需要添加数百行 XAML 来实现我们通过 3 行代码即可实现的情况下务实地使用代码隐藏。

我的直觉是,如果您可以通过对背后的代码进行代码审查来或多或少地确定正确性(这与我们对 XAML 所做的相同),并保留我们可以对其进行单元测试的主要复杂性 - 即 ViewModel ,那么我们就有了一个快乐的媒介。创建技术上纯粹的 MVVM 太容易了,这是可维护性的噩梦。

恕我直言:D

To my mind, the first option is preferable. It still maintains the separation between the View and the ViewModel (via the view interface), and keeps things in their logical places. The use of events is less intuitive.

I am in favour of pragmatic use of code behind in situations where it is either impossible to achieve through bindings, or requires you to add hundreds of lines of XAML to achieve what we can achieve with 3 lines of code behind.

My gut feeling is that if you can more or less be sure of correctness by code review of the code behind (which is the same as what we do with XAML anyway) and keep the main complexity where we can unit test it - ie the ViewModel, then we have a happy medium. It is all too easy to create technically pure MVVM which is a maintainability nightmare.

All IMHO :D

波浪屿的海角声 2024-11-05 15:47:14

我会尝试将其实现为文本框的混合行为,类似于选择和展开树视图的示例,而不使用后面的代码。我会尝试一起举一个例子。
http://www.codeproject.com/KB/silverlight/ViewModelTree.aspx

编辑:埃拉德已经提到使用附加行为,在做了一些之后,确实使做这样的事情变得简单。

mvvm 方式的弹出窗口行为的另一个示例:http://www.codeproject。 com/KB/silverlight/HisowaSimplePopUpBehavior.aspx

I would try and implement this as a blend behavior of the text box similar to this example of selecting and expanding a tree view without using code behind. I'll try and trow an example together.
http://www.codeproject.com/KB/silverlight/ViewModelTree.aspx

Edit: Elad already mentioned using attached behaviors, which, after doing a couple, really make doing things like this simple.

Another example of a behavior for popup windows in an mvvm fashion: http://www.codeproject.com/KB/silverlight/HisowaSimplePopUpBehavior.aspx

洋洋洒洒 2024-11-05 15:47:14

我同意许多其他答案,但我认为使用附加的 DependencyProperty 来展示完整的 MVVM 友好实现会很有用,该实现可以避免代码隐藏或依赖注入:

public static class AttachedHelpers
{
    public static readonly DependencyProperty InsertTextProperty =
        DependencyProperty.RegisterAttached(
            "InsertText",
            typeof(string),
            typeof(AttachedHelpers),
            new FrameworkPropertyMetadata(
                (string)null,
                // Likewise the default should be two-way so we can 
                // update the bound property to null after its use
                // to ensure no accidental re-use.
                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                new PropertyChangedCallback((d, e) =>
                {
                    if (!(d is TextBox tb) || !(e.NewValue is string s))
                        // no-op
                        return;
                    tb.Text.Insert(tb.CaretIndex, s);
                    // This ensures the value is used only once and
                    // text doesn't get re-inserted if the TextBox
                    // loses and regains its DataContext for some reason.
                    tb.SetCurrentValue(InsertTextProperty, null);
                })));
    public static string GetInsertText(DependencyObject obj)
    {
        return (string)obj.GetValue(InsertTextProperty);
    }
    public static void SetInsertText(DependencyObject obj, string value) 
    { 
        obj.SetValue(InsertTextProperty, value);
    }
}

public class ViewModel : ViewModelBase
{
    #region string TextToInsert property
    private string _textToInsert;
    public string TextToInsert
    {
        get
        {
            return _textToInsert;
        }
        set
        {
            _textToInsert = value;
            OnPropertyChanged();
        }
    }
    #endregion

    // Command handler
    private void InsertText(string text)
    {
        this.TextToInsert = text;
    }
}

以及非常简单的 XAML

 <TextBox so:AttachedHelpers.InsertText="{Binding InsertTextProperty}" />

:问题中提出的其他想法:

  • 我绝对不会尝试为视图模型提供用于直接控制的IView接口。虽然不想让视图模型访问视图类(或任何 WPF 依赖类型)是正确的,但 IView 范例却因为错误的原因做了错误的事情。

    更具体地说,MVVM 和 XAML 绑定方案的原始动机之一是将视图和视图模型之间的接触点限制为声明性定义的绑定。当局限于一个域或另一个域时,命令式代码是完全没问题的,但是如果您让视图和视图模型通过命令式代码进行交互(无论命令式代码位于视图端还是视图模型端),那么该模式及其优点就会崩溃。 IView 范例与过多的代码隐藏一样,都是对声明式定义的 UI 目标的侮辱,甚至可能更严重,因为它的扩展性非常差。

  • “我不想不惜一切代价保持 MVVM 架构中代码隐藏逻辑的干净。” 100%。 MVVM 不是自杀契约。但我们必须小心代码隐藏的含义,因为并非视图层中的所有 C# 代码都同样糟糕(或良好)。毕竟,WPF 本身 90% 以上是用 C# 编码的。当我们谈论 MVVM 不鼓励的“代码隐藏”类型时,我们指的是部分 UserControl 派生类,其中在 XAML 中声明的命名元素作为私有成员公开给 C# 代码,并且声明了事件处理程序在 XAML 中并在 .xaml.cs 文件中定义。那是代码隐藏。代码隐藏不是子类化控件或编写附加属性或行为。这只是简单的代码。

    此外,即使按照上面的定义,代码隐藏也可以,当其目的仅仅是与其他 UI 元素交互和/或操作其他 UI 元素时,创建子类化 Control 就显得有些过分了。但是,通过代码隐藏的 partial class 直接访问(更不用说操作)视图模型确实是应该尽量避免的事情 - 不是不惜“一切代价”,而是在合理的情况下这样做。

    你的第二个想法 - 让视图模型公开一个事件并让视图订阅它 - 并不是,有时几乎没有其他方法可以完成某些事情,但是在这个案子我不喜欢。它确实与 IView 具有相同的问题,除了问题从视图模型转移到视图之外。它仍然是必须维护/调试等的命令,而不是声明性

那么为什么我更喜欢我的解决方案(除了我写的这个事实?:))?

  • IViewevent 解决方案不同,附加属性是可重用的,因为它不依赖于特定于应用程序的逻辑。您可以将 AttachedHelpers 类放入您在整个职业生涯中维护的帮助程序库中,并根据需要与任何其他 WPF 应用程序一起重用(和添加)。子类化 TextBox 也可以工作,并且在原理上是相同的。
  • 撇开与附加属性相关的有些笨重但必要的样板代码不谈,解决方案的其余部分非常简单,易于阅读和编写,并且易于调试。您可以避免混乱的代码隐藏,并且成本是,嗯,恕我直言,成本是负数,因为我认为这个解决方案比任何其他提出的解决方案都更简单(至少,再次,给予津贴对于样板附加属性),并带来超出特定应用程序的附加值。

I agree with many of the other answers but I thought it would be useful to show a full MVVM-friendly implementation using an attached DependencyProperty that avoids code-behind or dependency injection:

public static class AttachedHelpers
{
    public static readonly DependencyProperty InsertTextProperty =
        DependencyProperty.RegisterAttached(
            "InsertText",
            typeof(string),
            typeof(AttachedHelpers),
            new FrameworkPropertyMetadata(
                (string)null,
                // Likewise the default should be two-way so we can 
                // update the bound property to null after its use
                // to ensure no accidental re-use.
                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                new PropertyChangedCallback((d, e) =>
                {
                    if (!(d is TextBox tb) || !(e.NewValue is string s))
                        // no-op
                        return;
                    tb.Text.Insert(tb.CaretIndex, s);
                    // This ensures the value is used only once and
                    // text doesn't get re-inserted if the TextBox
                    // loses and regains its DataContext for some reason.
                    tb.SetCurrentValue(InsertTextProperty, null);
                })));
    public static string GetInsertText(DependencyObject obj)
    {
        return (string)obj.GetValue(InsertTextProperty);
    }
    public static void SetInsertText(DependencyObject obj, string value) 
    { 
        obj.SetValue(InsertTextProperty, value);
    }
}

public class ViewModel : ViewModelBase
{
    #region string TextToInsert property
    private string _textToInsert;
    public string TextToInsert
    {
        get
        {
            return _textToInsert;
        }
        set
        {
            _textToInsert = value;
            OnPropertyChanged();
        }
    }
    #endregion

    // Command handler
    private void InsertText(string text)
    {
        this.TextToInsert = text;
    }
}

And the very simple XAML:

 <TextBox so:AttachedHelpers.InsertText="{Binding InsertTextProperty}" />

Regarding the other ideas presented in the question:

  • I would absolutely not try to give the view model an IView interface for direct control. While it's right to not want to ever give the view model access to view classes (or any WPF dependent types), the IView paradigm does the wrong thing for the wrong reasons.

    One of the original motives for MVVM and the XAML binding scheme more specifically was to limit contact points between view and view model to declaratively-defined bindings. Imperative code is perfectly fine when confined to one domain or the other, but if you let the view and view model interact through imperative code (whether that imperative code lives on the view or view model side), the pattern and its benefits break down. An IView paradigm is no less of an affront to the goal of a declaratively-defined UI than excessive code-behind, and probably even more so because it scales so poorly at that.

  • "I don't want to keep the MVVM architecture clean of code-behind logic at all costs." 100%. MVVM isn't a suicide pact. But we have to be careful what we mean by code-behind because not all C# code in the view layer is equally bad (or good). After all, WPF itself is 90+% coded in C#. When we talk about the kind of "code-behind" that MVVM discourages, we mean partial UserControl-derived classes with named elements declared in XAML being exposed to the C# code as private members, and event handlers declared in XAML and defined in the .xaml.cs file. That's code-behind. Code-behind is not subclassing Controls or writing attached properties or behaviors. That's just plain code.

    Moreover, code-behind, even as defined above, is fine when its purpose is solely to interact with and/or manipulate other UI elements, where making a subclassed Control would be overkill. But directly accessing - let alone manipulating - the view model through partial class code-behind is indeed something to try to avoid - not at "all costs", but when it's reasonable to do so.

    Your second idea - to have the view model expose an event and have the view subscribe to it - is not bad, and there are times when there's virtually no other way to accomplish something, but in this case I don't love it. It really has the same problem as IView, except problem moved from view model to view. It's still an imperative, rather than declarative, meeting point to have to maintain/debug/etc.

So why do I prefer my solution (besides the fact that I wrote it? :))?

  • Unlike either the IView or event solutions, an attached property is reusable, since it depends on no application-specific logic. You can put the AttachedHelpers class in a helper library that you maintain for your whole career and reuse (and add to) as needed with any other WPF app. Subclassing TextBox would also work, and is philosophically the same.
  • Leaving aside the somewhat clunky but necessary boilerplate code associated with attached properties, the rest of the solution is super-simple, easy to read and write, and easy to debug. You avoid messy code-behind, and the cost is, well, IMHO the cost is in the negative because I think this solution is even simpler than any other that's been proposed (at least, again, giving allowance for the boilerplate attached property), and brings added value beyond the specific application.
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文