Fat Models, skinny ViewModels and dumb Views, the best MVVM approach?
Asked Answered
C

2

21

Through generous help on this question, I put together the following MVVM structure which displays the changes of a model in real time in XAML (current date/time), very nice.

A cool advantage of this set up is that when you look at your view in design mode of Visual Studio or Blend, you see the time ticking by, which means that at design time you have access to live data from your model.

In the process of getting this to work, I was surprised to see most of the bulk move from my ViewModel into my Model, including implementation of INotifyPropertyChange. Another change is that I no longer bind to properties on the ViewModel but to methods.

So currently this is my favorite flavor of MVVM:

  1. View is dumb:

    • one ObjectDataProvider for each object you need from your model
    • each ObjectDataProvider maps to a method on the ViewModel (not a property)
    • no x:Name properties in XAML elements
  2. ViewModel is skinny:

    • the only thing in your ViewModel are the methods to which your view binds
  3. Model is fat:

    • the model implements INotifyPropertyChanged on each of its properties.
    • for every method on your ViewModel (e.g. GetCurrentCustomer) there is a corresponding singleton method in your Model (e.g. GetCurrentCustomer).
    • the model takes care of any real time threading functionality as in this example

Questions:

  1. Those of you who have been implementing MVVM in real scenarios, is this the basic structure you have also settled upon, and if not, how does yours vary?
  2. How would you extend this to include routed commands and routed events?

The following code will work if you just copy the XAML and code behind into a new WPF project.

XAML:

<Window x:Class="TestBinding99382.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:TestBinding99382"
    Title="Window1" Height="300" Width="300">

    <Window.Resources>
        <ObjectDataProvider 
             x:Key="DataSourceCustomer" 
             ObjectType="{x:Type local:ShowCustomerViewModel}" 
                        MethodName="GetCurrentCustomer"/>
    </Window.Resources>

    <DockPanel DataContext="{StaticResource DataSourceCustomer}">
        <StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
            <TextBlock Text="{Binding Path=FirstName}"/>
            <TextBlock Text=" "/>
            <TextBlock Text="{Binding Path=LastName}"/>
        </StackPanel>
        <StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
            <TextBlock Text="{Binding Path=TimeOfMostRecentActivity}"/>
        </StackPanel>

    </DockPanel>
</Window>

Code Behind:

using System.Windows;
using System.ComponentModel;
using System;
using System.Threading;

namespace TestBinding99382
{
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
        }
    }

    //view model
    public class ShowCustomerViewModel
    {
        public Customer GetCurrentCustomer() {
            return Customer.GetCurrentCustomer();
        }
    }

    //model
    public class Customer : INotifyPropertyChanged
    {
        private string _firstName;
        private string _lastName;
        private DateTime _timeOfMostRecentActivity;
        private static Customer _currentCustomer;
        private Timer _timer;

        public string FirstName
        {
            get
            {
                return _firstName;
            }
            set
            {
                _firstName = value;
                this.RaisePropertyChanged("FirstName");
            }
        }

        public string LastName
        {
            get
            {
                return _lastName;
            }
            set
            {
                _lastName = value;
                this.RaisePropertyChanged("LastName");
            }
        }

        public DateTime TimeOfMostRecentActivity
        {
            get
            {
                return _timeOfMostRecentActivity;
            }
            set
            {
                _timeOfMostRecentActivity = value;
                this.RaisePropertyChanged("TimeOfMostRecentActivity");
            }
        }

        public Customer()
        {
            _timer = new Timer(UpdateDateTime, null, 0, 1000);
        }

        private void UpdateDateTime(object state)
        {
            TimeOfMostRecentActivity = DateTime.Now;
        }

        public static Customer GetCurrentCustomer()
        {
            if (_currentCustomer == null)
            {
                _currentCustomer = new Customer 
                     {  FirstName = "Jim"
                        , LastName = "Smith"
                        , TimeOfMostRecentActivity = DateTime.Now 
                     };
            }
            return _currentCustomer;
        }

        //INotifyPropertyChanged implementation
        public event PropertyChangedEventHandler PropertyChanged;
        private void RaisePropertyChanged(string property)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(property));
            }
        }
    }
}
Clueless answered 12/5, 2009 at 12:15 Comment(1)
Regarding RelayCommands and RoutedCommands, you might want to have a look at these answers: #650510Shamefaced
M
31

Here's my opinion, for what it's worth :

I don't really agree with the approach you suggest (except for the dumb view). In real life, you will often have to use an existing model : it could be legacy code that you don't have the time (or will) to change, or even a library for which you don't have the code. In my opinion, the model should be completely unaware of the way it will be displayed, and should be easily usable in a non-WPF application. So it doesn't have to implement any specific interface like INotifyPropertyChanged of INotifyCollectionChanged to make it usable in MVVM. I think that all the logic related to UI should reside in the ViewModel.

Regarding RoutedEvents and RoutedCommands, they are not really suitable for use with the MVVM pattern. I usually try to use as little RoutedEvents as possible, and no RoutedCommands at all. Instead, my ViewModels expose RelayCommand properties that I bind to the UI in XAML (see this article by Josh Smith for details on RelayCommand). When I really need to handle events for some control, I use attached behaviors to map the events to ViewModel commands (have a look at Marlon Grech's implementation)

So, in summary :

  • Dumb View
  • Big and smart ViewModel
  • Any model you want or have to use

Of course it's just my approach, and it may not be the best, but I feel quite comfortable with it ;)

Milt answered 12/5, 2009 at 20:54 Comment(2)
Very insightful, especially regarding the model, that you should be able to use the MVVM pattern to hook up to data classes from old projects which have no knowledge of WPF. Interesting the you think RoutedEvents and RoutedCommands are not really suitable for MVVM, I thought they were made to be used in decoupling patterns such as MVVM. Thanks for the feedback.Clueless
I refactored this example based on this feedback, put the logic up into the ViewModel: #858320Clueless
S
3

I agree with Thomas. My advise to anyone on WPF architecturing would be:

  • Plain POCO entities with no INotifyPropertyChange, state tracking, BL, etc.
  • Simple and small ViewModels that notify Views just-in-time
  • Simple reusable UI with a smart navigation system that avoid complex data hierarchies and complex underlying ViewModels
  • MVVM with View First approach to keep dependencies simple
  • Async operations with Tasks or Rx
  • A simple theme
  • No complex robust UI, keep it simple, just take advantage of WPFs UI composition and binding capabilities
  • Don't hesitate to use code-behind to generate content dynamically (forms, lists etc) and save significant time on declarative eye configuration (applies to most cases) - and for me a must on 2015. Use extension methods to create a Fluent API for that.
Strobe answered 8/3, 2015 at 20:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.