Big smart ViewModels, dumb Views, and any model, the best MVVM approach?
Asked Answered
S

2

8

The following code is a refactoring of my previous MVVM approach (Fat Models, skinny ViewModels and dumb Views, the best MVVM approach?) in which I moved the logic and INotifyPropertyChanged implementation from the model back up into the ViewModel. This makes more sense, since as was pointed out, you often you have to use models that you either can't change or don't want to change and so your MVVM approach should be able to work with any model class as it happens to exist.

This example still allows you to view the live data from your model in design mode in Visual Studio and Expression Blend which I think is significant since you could have a mock data store that the designer connects to which has e.g. the smallest and largest strings that the UI can possibly encounter so that he can adjust the design based on those extremes.

Questions:

  • I'm a bit surprised that I even have to "put a timer" in my ViewModel since it seems like that is a function of INotifyPropertyChanged, it seems redundant, but it was the only way I could get the XAML UI to constantly (once per second) reflect the state of my model. So it would be interesting to hear anyone who may have taken this approach if you encountered any disadvantages down the road, e.g. with threading or performance.

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

XAML:

<Window x:Class="TestMvvm73892.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:TestMvvm73892"
    Title="Window1" Height="300" Width="300">
    <Window.Resources>
        <ObjectDataProvider 
              x:Key="DataSourceCustomer" 
              ObjectType="{x:Type local:CustomerViewModel}" 
             MethodName="GetCustomerViewModel"/>
    </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;
using System.Windows;
using System.ComponentModel;
using System.Threading;

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

    //view model
    public class CustomerViewModel : INotifyPropertyChanged
    {
        private string _firstName;
        private string _lastName;
        private DateTime _timeOfMostRecentActivity;
        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 CustomerViewModel()
        {
            _timer = new Timer(CheckForChangesInModel, null, 0, 1000);
        }

        private void CheckForChangesInModel(object state)
        {
            Customer currentCustomer = CustomerViewModel.GetCurrentCustomer();
            MapFieldsFromModeltoViewModel(currentCustomer, this);
        }

        public static CustomerViewModel GetCustomerViewModel()
        {
            CustomerViewModel customerViewModel = new CustomerViewModel();
            Customer currentCustomer = CustomerViewModel.GetCurrentCustomer();

            MapFieldsFromModeltoViewModel(currentCustomer, customerViewModel);

            return customerViewModel;
        }

        public static void MapFieldsFromModeltoViewModel
             (Customer model, CustomerViewModel viewModel) 
        {
            viewModel.FirstName = model.FirstName;
            viewModel.LastName = model.LastName;
            viewModel.TimeOfMostRecentActivity = model.TimeOfMostRecentActivity;
        }

        public static Customer GetCurrentCustomer()
        {
            return Customer.GetCurrentCustomer();
        }


        //INotifyPropertyChanged implementation
        public event PropertyChangedEventHandler PropertyChanged;
        private void RaisePropertyChanged(string property)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(property));
            }
        }

    }

    //model
    public class Customer
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public DateTime TimeOfMostRecentActivity { get; set; }

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

    }

}
Sentimentalize answered 13/5, 2009 at 13:3 Comment(1)
I've posted the question having used there this code: How to (correctly) update the M in MVVM of WPF applicationAccusative
M
15

I like your sample above, I think it implements the spirit of MVVM. Just for clarification, though, the ViewModel code and the Model code should not be in the same source file as the actual Code Behind. In fact, I would argue that they should not be in the same project.

Here is MVVM as I understand it:

M - the Model is the data returned from the Business Layer (BL). This should be lightweight, containing read-only data. The Model classes are dumb and do not contain Update, Write, or Delete logic, and are generated by the BL as the result of requests, commands, actions, etc. The Model classes have no knowledge of the presentation needs of the consuming application, so they can be used by any manner of application. To really take advantage of this reusability, we want the Model classes to be independent of the UI project.

VM - the ViewModel contains the communications layer: it issues the requests to the BL and processes the results in a manner fit for presentation. Like the example above, it also receives the Model and reformats it for the particular presentation needs. Think of this as a "Binding Class". In the example above, the data is simply being moved from one object to the next, but the ViewModel would be responsible for such things as exposing a "FullName" type property or adding leading zeroes to a ZipCode. It is correct that the Binding Class is the one to implement INotifyPropertyChanged. And again, for reusability I would probably extract this layer into its own project as well. This would allow you to experiment with various UI options with no plumbing changes.

V - the View is bound to the Binding class object created in the VM. The View is super dumb: it knows nothing of the BL or the VM. Data can be bound in both directions, but the VM handles errors, validation, etc. Any data synchronization operations are handled by passing requests back the the BL, and again processing the results.

It would depend on the type of application, but it seems heavy handed to constantly check the Model to see if it has changed. Pretend you were connecting to a BL that built the Business Object(BO) from a DAL, which is connecting to a DB. In this scenario, you would be constantly recreating the BO, which I'm sure would be a performance killer. You could implement a checkout system on the BL that listened for notifications, or have a method for comparing last known time of change to actual, or you could cache the BO on the BL. Just some ideas.

Also, I said above that the Model should be Lightweight. There are heavyweight options, like CSLA, but I'm not sure how well they fit into the MVVM idea.

I don't mean to pass myself off as an expert, I have only been studying these ideas so far while designing our new software's architecture. I'd love to read some discussion about this topic.

Mauk answered 13/5, 2009 at 19:19 Comment(6)
lots to think about thanks, yes, my MV and M are all in the code behind just for easy copying purposes, one question: if your models do not contain update, write, and delete methods, are you putting those methods in the ViewModel which are "handled by request, commands, actions"? But what about another UI layer that needs to use your models, surely it needs to update, write and delete in the same waySentimentalize
The CRUD methods belong in the Business Layer. The VM simply passes the request on to the BL. So the flow would be like this: V responds to user input and fires an ICommand, which executes a method on VM, which sends the request to the BL, which returns a M object, which VM processes and converts to a Binding Object, which is passed back to the V.Mauk
Oops, forgot to answer a question: Another UI layer could use the same VM in the same way (another reason for it to be in its own project)Mauk
How do you track V in the long round-trip? @JoelCochranIliac
And what if V related to the Binding Object hasn't been created? For example, an ICommand that creates a new Model.Iliac
I think the key is understanding the complete lifecycle of M, VM, V and Binding Object in the round-trip you described. I'd appreciate it if you can update the answer about their lifecycles.Iliac
P
3

My personal opinion is that while Model should be used to load and store data, ViewModel's responsibility is to know when this data is needed, thus using timer in a ViewModel makes sense. This way you can use your Model with different ViewModel (for which it may be sufficient to retrieve data only once, not every second).

Few things to consider:

  • Implement your model to support Asynchronous data retrieval (very important if you want to target Silverlight)
  • Be careful about updating collection from background thread (not a problem in your example, but if you ever need to use ObservableCollection than remember that it cannot be updated from non UI thread, read more here )
Paratyphoid answered 13/5, 2009 at 13:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.