开发者

Pragmatic use of code-behind in MVVM pattern

开发者 https://www.devze.com 2023-02-22 03:36 出处:网络
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.

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 开发者_如何学CUnloaded 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!


Specifically for this problem

The simplest solution to this specific case is adding an attached property that does 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, and not even about an IView. in MVVM its "always look up", which means a View can look @ 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 (incharge 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 priniciple is important, like you asked.

MVVM is problematic when you need GUI that is very rich or the UI doens'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 codebehind free, but to keep all logic & data to the ViewModel, where 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 :)


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.


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.


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


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.


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

0

精彩评论

暂无评论...
验证码 换一张
取 消