Click here to Skip to main content
15,885,366 members
Articles / Desktop Programming / WPF

Localization and Complex Validation in MVVM

Rate me:
Please Sign up or sign in to vote.
2.50/5 (3 votes)
21 Jul 2010CPOL8 min read 41.7K   1.2K   37   3
Presents methods to handle some of the trickier aspects of MVVM, including error message localization, multi-control validation, validation with multiple instances of a View, and whole-View validation.

Overview

MVVM (Model-View-ViewModel) is a fantastic design pattern, with numerous benefits. You already know this, or you wouldn't be reading MVVM articles. It can also get rather complicated, and you already know this as well, or you wouldn't be reading MVVM articles.

Contents

Introduction

This article, and the simple software presented here, addresses the following trickier aspects of MVVM:

  • Error message localization
  • Multi-control validation
  • Validation with multiple instances of a View
  • Whole-View validation

I also present the following positions regarding proper MVVM design:

  • No displayed text should exist in the Model or ViewModel.
  • The belief that Views should contain no code is incorrect.
  • Display-specific code belongs in the View.
  • Views should never contain business logic.

Basic Validation

There are two mechanisms for input validation in an MVVM pattern: the IDataErrorInfo interface and the ValidationRule class. In both cases, the validation can occur in the Model, the ViewModel, or both, as it should be, instead of in the View. The problem is that while validation should not occur in the View, the text of an error message is a display issue and not a business logic issue, so the View should be responsible for the actual error message text. An important subset of this issue is localization. One way to prove that an application follows the MVVM pattern is if the Model and ViewModel could have been written before the View (or Views) as a class library. It should be possible to build an application that contains such an M-VM class library, and add localizations without the need to modify the M-VM class library.

The demo application solves the problem of localizing error messages by placing validation rules (subclasses of ValidationRule) in the M-VM class library, having the validation rules return error enumerations instead of error strings to the View, and placing a Converter in the binding for the error message which lets the View convert the error enumeration into a localizable string.

The demo application simulates software for a blood pressure study. I chose this topic because it allows the demonstration of validation across multiple controls, namely the systolic and diastolic pressures, since the diastolic needs to be smaller than the systolic.

The first window lets the user choose a language. The window itself will be displayed in the system's current culture, if it's Mexican Spanish or French French; otherwise, it will display in English.

Image 1

Once Start is pressed, the application displays in the selected language, regardless of the system settings. The main window is then displayed, which contains a single button for creating and displaying one or more View instances.

Image 2

Image 3

Image 4

Each time the button is pressed, another instance of the View is created and displayed. As the images below demonstrate, the error message shown in the tool tip is localized. Using a similar mechanism, whenever the systolic and diastolic pressures are both valid, the classification of the blood pressure's healthiness (such as normal, hypotension, stage 1 hypertension, etc.) is displayed, again in the correct language by converting a bound enumeration within the View's code-behind.

Image 5

Image 6

Image 7

Image 8

Dependent Validation and Multiple View Instances

One of the challenges faced when developing this application is that the ValidationRule class' Validate function signature only takes a value and a culture, and no other parameter to indicate the source. This is fine in most cases, when validation of a control isn't dependent on the values of other controls, and when only one instance of the View class can exist. When it does depend on other controls, a mechanism is needed to allow the validator to access the dependent value.

In this application, the current ViewModel is stored as a static variable of the ViewModel class, and set by the View when it's first loaded, and whenever it becomes active. The validator is then able to access the dependent value through the current ViewModel.

Whole-View Validation

Another challenge is knowing when a View is valid, such as to know if its Model can be saved, or to determine when a Save button or menu item can be enabled. When controls of a View are bound directly to members of the Model, and the binding system handles type conversion, when a control has a validation error, the ViewModel has no way of knowing about it, as it only has access to the last valid value transferred into the Model. A related problem is validating controls that were never edited. I used Josh Smith's small but mighty RelayCommand class for the enabling and handling of the Save button within the ViewModel. My CanSave function needed to force the View to validate all controls and then let the ViewModel know if everything in the View is valid or not. When the View constructs its ViewModel, it passes a function delegate that the ViewModel calls in CanSave, and a function called after saving completes, that can pass an exception to the View so it can display any localized error message.

Image 9

Image 10

Inside the Demo Application

The demo application is organized as follows:

Image 11

Rather than showing all of the source, I'll just show the relevant and interesting portions.

The View

The relevant part of the TextBox for the diastolic pressure looks like:

XML
<TextBox
    Name="Diastolic_TextBox"
    Validation.ErrorTemplate="{StaticResource validationTemplate}"
    Style="{StaticResource diastolicTextBoxInError}">
    <TextBox.Text>
        <Binding
            Path="Model.Diastolic"
            UpdateSourceTrigger="Explicit">
            <Binding.ValidationRules>
                <vm:Diastolic_ValidationRule />
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>

Here, the style, error template, and validation rule are set as described on MSDN for the "Validation.HasError Attached Property". The View's DataContext is set to the ViewModel, which contains the model in a property named Model.

The related portions of the window's resources include:

XML
<clr:DiastolicErrorConverter
    x:Key="diastolicErrorConverter" />

<ControlTemplate
    x:Key="validationTemplate">
    <DockPanel>
        <TextBlock
            Foreground="Red"
            FontWeight="Bold"
            VerticalAlignment="Center">
            !
        </TextBlock>
        <AdornedElementPlaceholder />
    </DockPanel>
</ControlTemplate>

<Style
    x:Key="diastolicTextBoxInError"
    TargetType="{x:Type TextBox}">
    <Style.Triggers>
        <Trigger
            Property="Validation.HasError"
            Value="true">
            <Setter
                Property="ToolTip"
                Value="{Binding
                    RelativeSource={x:Static RelativeSource.Self},
                    Path=(Validation.Errors)[0].ErrorContent,
                    Converter={StaticResource diastolicErrorConverter}}" />
        </Trigger>
    </Style.Triggers>
</Style>

Most of this is standard. The interesting part is the converter on the ToolTip's binding, which allows the View to localize the error messages in its code-behind.

One misunderstanding about the MVVM pattern is that a View should never have any code-behind. It's okay for there to be a code-behind, as long as it only addresses display issues. To quote the book Advanced MVVM by Josh Smith, "When using ViewModels, your Views can and, in many cases, should still have certain kinds of code in their code-behind files." Below is the full code-behind file of the View:

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.Globalization; // for CultureInfo

namespace ApplicationWithView
{
    /// <summary>
    ///        Interaction logic for BloodPressure_Window.xaml,
    ///     the View for editing a blood pressure test.
    /// </summary>
    /// 
    public partial class BloodPressure_Window : Window
    {
        /// <summary>
        ///        The ViewModel of this View.
        /// </summary>
        ///
        private ClassLibraryOfModelAndViewModel.BloodPressure_ViewModel
            _viewModel;


        /// <summary>
        ///        Constructor.
        /// </summary>
        /// 
        public BloodPressure_Window()
        {
            InitializeComponent();

            //    Create and save this View's ViewModel.
            _viewModel = 
              new ClassLibraryOfModelAndViewModel.BloodPressure_ViewModel( 
                  ValidateView, ViewModelCompletedSaving );

            //    Set this View's data context to its ViewModel.
            DataContext = _viewModel;
        }


        /// <summary>
        ///        Callback for full-View validation,
        ///     called to determine if this blood pressure test can be saved.
        /// </summary>
        /// <returns>Returns if this View is valid.</returns>
        /// 
        private bool ValidateView()
        {
            bool
                result;
            BindingExpression
                bindingExpression;

            result = true;
            bindingExpression = 
              Systolic_TextBox.GetBindingExpression( TextBox.TextProperty );
            if ( bindingExpression != null )
            {
                bindingExpression.UpdateSource();
                if ( Validation.GetErrors( Systolic_TextBox ).Count > 0 )
                {
                    //    The systolic pressure is invalid.
                    result = false;
                }
            }
            bindingExpression = 
              Diastolic_TextBox.GetBindingExpression( TextBox.TextProperty );
            if ( bindingExpression != null )
            {
                bindingExpression.UpdateSource();
                if ( Validation.GetErrors( Diastolic_TextBox ).Count > 0 )
                {
                    //    The diastolic pressure is invalid.
                    result = false;
                }
            }
            return result;
        }


        /// <summary>
        ///     Callback called when the ViewModel is done saving
        ///  this blood pressure test, either successfully or with an error.
        /// </summary>
        /// <param name="anyException">Any exception that occurred while trying to save, 
        /// of which the user needs to be informed.</param>
        /// 
        private void ViewModelCompletedSaving(
            Exception
                anyException )
        {
            if ( anyException == null )
            {
                Close();
                return;
            }
            //    Here is where it would display any error saving.
            //    ...
        }


        /// <summary>
        ///    Handler for this View becoming active, which sets a static
        /// to this View's ViewModel, for multi-field validation.
        /// </summary>
        /// <param name="sender">Sender.</param>
        /// <param name="eventArgs">Event arguments.</param>
        /// 
        private void BloodPressure_Window_Activated(
            object
                sender,
            EventArgs
                eventArgs )
        {
            ClassLibraryOfModelAndViewModel.
               BloodPressure_ViewModel.ActiveViewModel = _viewModel;
        }


        /// <summary>
        ///    Handler for when this View is loaded, which sets up the handler for Activated,
        ///    and sets a static to this View's ViewModel, for multi-field validation.
        /// </summary>
        /// <param name="sender">Sender.</param>
        /// <param name="routedEventArgs">Routed event arguments.</param>
        /// 
        private void BloodPressure_Window_Loaded(
            object
                sender,
            RoutedEventArgs
                routedEventArgs )
        {
            ClassLibraryOfModelAndViewModel.
                    BloodPressure_ViewModel.ActiveViewModel = _viewModel;
            this.Activated += new EventHandler( BloodPressure_Window_Activated );
        }
    }



    /// <summary>
    ///    Converter for turning a systolic error from
    /// the ViewModel into a localized error message.
    /// </summary>
    /// 
    [ValueConversion( typeof( 
      ClassLibraryOfModelAndViewModel.BloodPressureTestResult.SystolicErrorType ), 
      typeof( String ) )]
    public class SystolicErrorConverter : IValueConverter
    {
        public object Convert( object value, Type targetType, 
               object parameter, CultureInfo culture )
        {
            ClassLibraryOfModelAndViewModel.BloodPressureTestResult.SystolicErrorType
                systolicErrorType;

            if ( value.GetType() != typeof( 
                    ClassLibraryOfModelAndViewModel.
                          BloodPressureTestResult.SystolicErrorType ) )
                return "";
            systolicErrorType = 
              (ClassLibraryOfModelAndViewModel.
                     BloodPressureTestResult.SystolicErrorType) value;
            switch ( systolicErrorType )
            {
                case ClassLibraryOfModelAndViewModel.
                         BloodPressureTestResult.SystolicErrorType.None:
                    return "";
                case ClassLibraryOfModelAndViewModel.BloodPressureTestResult.
                            SystolicErrorType.InvalidIntegerString:
                    return Main_Window.__InvalidIntegerValueText;
                case ClassLibraryOfModelAndViewModel.
                         BloodPressureTestResult.SystolicErrorType.TooLow:
                    return Main_Window.__SystolicPressureTooLowText
                      + ClassLibraryOfModelAndViewModel.BloodPressureTestResult.MinSystolic
                      + Main_Window.__EndOfSentenceText;
                case ClassLibraryOfModelAndViewModel.
                             BloodPressureTestResult.SystolicErrorType.TooHigh:
                    return Main_Window.__SystolicPressureTooHighText
                      + ClassLibraryOfModelAndViewModel.BloodPressureTestResult.MaxSystolic
                      + Main_Window.__EndOfSentenceText;
            }
            return ""; // should never happen
        }

        public object ConvertBack( object value, Type targetType, 
               object parameter, CultureInfo culture )
        {
            return "";
        }
    }


    /// <summary>
    ///    Converter for turning a diastolic error
    /// from the ViewModel into a localized error message.
    /// </summary>
    /// 
    [ValueConversion( typeof( ClassLibraryOfModelAndViewModel.
           BloodPressureTestResult.DiastolicErrorType ), typeof( String ) )]
    public class DiastolicErrorConverter : IValueConverter
    {
        public object Convert( object value, Type targetType, 
               object parameter, CultureInfo culture )
        {
            ClassLibraryOfModelAndViewModel.BloodPressureTestResult.DiastolicErrorType
                diastolicErrorType;

            if ( value.GetType() != typeof( ClassLibraryOfModelAndViewModel.
                                 BloodPressureTestResult.DiastolicErrorType ) )
                return "";
            diastolicErrorType = (ClassLibraryOfModelAndViewModel.
                                    BloodPressureTestResult.DiastolicErrorType) value;
            switch ( diastolicErrorType )
            {
                case ClassLibraryOfModelAndViewModel.
                         BloodPressureTestResult.DiastolicErrorType.None:
                    return "";
                case ClassLibraryOfModelAndViewModel.BloodPressureTestResult.
                                    DiastolicErrorType.InvalidIntegerString:
                    return Main_Window.__InvalidIntegerValueText;
                case ClassLibraryOfModelAndViewModel.
                           BloodPressureTestResult.DiastolicErrorType.TooLow:
                    return Main_Window.__DiastolicPressureTooLowText
                      + ClassLibraryOfModelAndViewModel.BloodPressureTestResult.MinDiastolic
                      + Main_Window.__EndOfSentenceText;
                case ClassLibraryOfModelAndViewModel.
                         BloodPressureTestResult.DiastolicErrorType.TooHigh:
                    return Main_Window.__DiastolicPressureTooHighText;
            }
            return ""; // should never happen
        }

        public object ConvertBack( object value, Type targetType, 
               object parameter, CultureInfo culture )
        {
            return "";
        }
    }


    /// <summary>
    ///    Converter for turning a blood pressure healthiness
    /// from the ViewModel into a localized string.
    /// </summary>
    /// 
    [ValueConversion( typeof( ClassLibraryOfModelAndViewModel.
       BloodPressureTestResult.BloodPressureHealthinessType ), typeof( String ) )]
    public class BloodPressureHealthinessTypeConverter : IValueConverter
    {
        public object Convert( object value, Type targetType, 
               object parameter, CultureInfo culture )
        {
            switch ( (ClassLibraryOfModelAndViewModel.BloodPressureTestResult.
                      BloodPressureHealthinessType) value )
            {
                case ClassLibraryOfModelAndViewModel.BloodPressureTestResult.
                            BloodPressureHealthinessType.Hypotension:
                    return Main_Window.__HypotensionText;
                case ClassLibraryOfModelAndViewModel.BloodPressureTestResult.
                            BloodPressureHealthinessType.Normal:
                    return Main_Window.__NormalText;
                case ClassLibraryOfModelAndViewModel.BloodPressureTestResult.
                            BloodPressureHealthinessType.Prehypertension:
                    return Main_Window.__PrehypertensionText;
                case ClassLibraryOfModelAndViewModel.BloodPressureTestResult.
                            BloodPressureHealthinessType.Stage1Hypertension:
                    return Main_Window.__Stage1HypertensionText;
                case ClassLibraryOfModelAndViewModel.BloodPressureTestResult.
                            BloodPressureHealthinessType.Stage2Hypertension:
                    return Main_Window.__Stage2HypertensionText;
                case ClassLibraryOfModelAndViewModel.BloodPressureTestResult.
                            BloodPressureHealthinessType.Indeterminate:
                    return "";
            }
            return ""; // should never happen
        }

        public object ConvertBack( object value, Type targetType, 
               object parameter, CultureInfo culture )
        {
            return "";
        }
    }
}

The Model

Here is the whole Model:

C#
using System;
using System.Collections.Generic;
using System.ComponentModel; // for INotifyPropertyChanged
using System.Linq;
using System.Text;

namespace ClassLibraryOfModelAndViewModel
{
    /// <summary>
    ///        The Model for holding the results of a blood pressure test.
    /// </summary>
    /// 
    public class BloodPressureTestResult : INotifyPropertyChanged
    {
        #region Support for INotifyPropertyChanged

        public event PropertyChangedEventHandler PropertyChanged;

        protected void Notify(
            string
                propertyName )
        {
            if ( PropertyChanged != null )
                PropertyChanged( this, new PropertyChangedEventArgs( propertyName ) );
        }

        #endregion // Support for INotifyPropertyChanged


        #region Public Properties

        /// <summary>
        ///        Classification types for the healthiness of one's blood pressure.
        /// </summary>
        /// 
        public enum BloodPressureHealthinessType
        {
            Indeterminate,
            Hypotension,
            Normal,
            Prehypertension,
            Stage1Hypertension,
            Stage2Hypertension
        }

        /// <summary>
        ///        Systolic error types.
        /// </summary>
        /// 
        public enum SystolicErrorType
        {
            None,
            InvalidIntegerString,
            TooLow,
            TooHigh
        }

        /// <summary>
        ///        Diastolic error types.
        /// </summary>
        /// 
        public enum DiastolicErrorType
        {
            None,
            InvalidIntegerString,
            TooLow,
            TooHigh
        }

        /// <summary>
        ///        The sequential test number.
        /// </summary>
        /// 
        public int TestNumber { get; set; }

        /// <summary>
        ///        Systolic blood pressure.
        /// </summary>
        /// 
        public int Systolic
        {
            get
            {
                return _systolic;
            }
            set
            {
                _systolic = value;
                SetBloodPressureHealthiness();
            }
        }
        private int
            _systolic;

        /// <summary>
        ///        Diastolic blood pressure.
        /// </summary>
        /// 
        public int Diastolic
        {
            get
            {
                return _diastolic;
            }
            set
            {
                _diastolic = value;
                SetBloodPressureHealthiness();
            }
        }
        private int
            _diastolic;

        /// <summary>
        ///        Classification for the healthiness of one's blood pressure.
        /// </summary>
        public BloodPressureHealthinessType BloodPressureHealthiness
        {
            get
            {
                return _bloodPressureHealthiness;
            }
            private set
            {
                _bloodPressureHealthiness = value;
                Notify( "BloodPressureHealthiness" );
            }
        }
        private BloodPressureHealthinessType
            _bloodPressureHealthiness;

        #endregion // Public Properties


        #region Public Static Properties

        /// <summary>
        ///        Minimum allowed systolic value.
        /// </summary>
        /// 
        public static int MinSystolic
        {
            get
            {
                return 10;
            }
        }

        /// <summary>
        ///        Maximum allowed systolic value.
        /// </summary>
        /// 
        public static int MaxSystolic
        {
            get
            {
                return 300;
            }
        }

        /// <summary>
        ///        Minimum allowed diastolic value.
        /// </summary>
        /// 
        public static int MinDiastolic
        {
            get
            {
                return 10;
            }
        }

        #endregion // Public Static Properties


        #region Private Static Variables

        /// <summary>
        ///        Number of tests.
        /// </summary>
        /// 
        private static int
            __numTests = 0;

        #endregion // Private Static Variables


        /// <summary>
        ///        Constructor.
        /// </summary>
        /// 
        public BloodPressureTestResult()
        {
            //    Increment the number of tests and assign this test's number.
            __numTests++;
            TestNumber = __numTests;
        }


        /// <summary>
        /// Validate a string representing
        /// a systolic blood pressure and return any error.
        /// </summary>
        /// <param name="stringOfSystolic">A string representing 
        ///     a systolic blood pressure.</param>
        /// <returns>Returns any validation error.</returns>
        /// 
        public SystolicErrorType GetSystolicError(
            string
                stringOfSystolic )
        {
            int
                systolic;

            if ( !int.TryParse( stringOfSystolic, out systolic ) )
                return SystolicErrorType.InvalidIntegerString;
            if ( systolic < BloodPressureTestResult.MinSystolic )
                return SystolicErrorType.TooLow;
            if ( systolic > BloodPressureTestResult.MaxSystolic )
                return SystolicErrorType.TooHigh;
            return SystolicErrorType.None;
        }


        /// <summary>
        /// Validate a string representing
        /// a diastolic blood pressure and return any error.
        /// </summary>
        /// <param name="stringOfDiastolic">A string representing 
        ///         a diastolic blood pressure.</param>
        /// <returns>Returns any validation error.</returns>
        /// 
        public DiastolicErrorType GetDiastolicError(
            string
                stringOfDiastolic )
        {
            int
                diastolic;

            if ( !int.TryParse( stringOfDiastolic, out diastolic ) )
                return DiastolicErrorType.InvalidIntegerString;
            if ( diastolic < BloodPressureTestResult.MinDiastolic )
                return DiastolicErrorType.TooLow;
            if ( diastolic >= Systolic )
                return DiastolicErrorType.TooHigh;
            return DiastolicErrorType.None;
        }


        /// <summary>
        ///        Set the healthiness based on the systolic and diastolic pressures.
        /// </summary>
        /// 
        private void SetBloodPressureHealthiness()
        {
            if ( Systolic < MinSystolic || Systolic > MaxSystolic || 
                           Diastolic < MinDiastolic || Diastolic >= Systolic )
                BloodPressureHealthiness = BloodPressureHealthinessType.Indeterminate;
            else if ( Systolic < 90 || Diastolic < 60 )
                BloodPressureHealthiness = BloodPressureHealthinessType.Hypotension;
            else if ( Systolic >= 90 && Systolic <= 120 && 
                              Diastolic >= 60 && Diastolic <= 80 )
                BloodPressureHealthiness = BloodPressureHealthinessType.Normal;
            else if ( ( Systolic >= 121 && Systolic <= 139 ) || 
                            ( Diastolic >= 81 && Diastolic <= 89 ) )
                BloodPressureHealthiness = BloodPressureHealthinessType.Prehypertension;
            else if ( ( Systolic >= 140 && Systolic <= 159 ) || 
                        ( Diastolic >= 90 && Diastolic <= 99 ) )
                BloodPressureHealthiness = BloodPressureHealthinessType.Stage1Hypertension;
            else if ( Systolic >= 160 || Diastolic >= 100 )
                BloodPressureHealthiness = BloodPressureHealthinessType.Stage2Hypertension;
            else
                BloodPressureHealthiness = BloodPressureHealthinessType.Indeterminate;
        }
    }
}

The ViewModel

Here is the whole ViewModel:

C#
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Windows.Input; // for ICommand

namespace ClassLibraryOfModelAndViewModel
{
    public class BloodPressure_ViewModel
    {
        #region Delegates

        /// <summary>
        ///        Delegate called to validate the whole View.
        /// </summary>
        /// <returns>Returns if the view is valid (i.e.ready to save).</returns>
        /// 
        public delegate bool ValidateView_Delegate();

        /// <summary>
        ///    Delegate called when the Model has been saved,
        /// or an error occurred while saving.
        /// </summary>
        /// <param name="anyException">Any exception that ocurred</param>
        /// 
        public delegate void DoneSaving_Delegate(
            Exception
                anyException );

        #endregion


        #region Public Properties

        /// <summary>
        ///        The Model, a blood pressure test result.
        /// </summary>
        /// 
        public BloodPressureTestResult
            Model { get; private set; }

        /// <summary>
        ///        The save command, bound to by the View's Save button or menu item.
        /// </summary>
        /// 
        public ICommand SaveCommand
        {
            get
            {
                if ( _saveCommand == null )
                {
                    _saveCommand = new RelayCommand(
                        param => this.Save(),
                        param => this.CanSave );
                }
                return _saveCommand;
            }
        }

        /// <summary>
        ///        Static into which the View must place the active ViewModel,
        ///        so that when multiple Views are displayed, the validator
        ///        can validate dependent controls.
        /// </summary>
        /// 
        public static BloodPressure_ViewModel ActiveViewModel { get; set; }

        #endregion // Public Properties


        #region Private Members

        private RelayCommand
            _saveCommand;
        private ValidateView_Delegate
            _validateView_Callback;
        private DoneSaving_Delegate
            _doneSaving_Callback;

        #endregion // Private Members


        /// <summary>
        ///        Constructor.
        /// </summary>
        /// <param name="validateView_Callback"></param>
        /// <param name="doneSaving_Callback"></param>
        /// 
        public BloodPressure_ViewModel(
            ValidateView_Delegate
                validateView_Callback,
            DoneSaving_Delegate
                doneSaving_Callback )
        {
            this._validateView_Callback = validateView_Callback;
            this._doneSaving_Callback = doneSaving_Callback;
            Model = new BloodPressureTestResult();
        }


        /// <summary>
        ///        Determine if the Model can be saved, meaning that it's valid.
        /// </summary>
        /// 
        bool CanSave
        {
            get
            {
                if ( this != ActiveViewModel )
                    return false;
                if ( _validateView_Callback == null )
                    return false;
                return _validateView_Callback(); // let the view validate itself
            }
        }


        /// <summary>
        ///        Save the Model.
        /// </summary>
        /// <remarks>
        ///        If saving to a file (as opposed to writing to a database record),
        ///        the View should get the file name, and pass it to this function.
        /// </remarks>
        /// 
        private void Save()
        {
            //    Here is where saving would occur.
            //    ...
            _doneSaving_Callback( null ); // let the view know that saving has completed
        }
    }
}

The Validator

Here is the validator for the diastolic pressure:

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls; // for ValidationResult
using System.Globalization; // for CultureInfo

namespace ClassLibraryOfModelAndViewModel
{
    /// <summary>
    ///        Validator for a diastolic pressure.
    /// </summary>
    /// 
    public class Diastolic_ValidationRule : ValidationRule
    {
        public override ValidationResult Validate(
            object
                value,
            CultureInfo
                cultureInfo )
        {
            BloodPressureTestResult.DiastolicErrorType
                diastolicErrorType;

            //    Let the Model validate the value.
            diastolicErrorType = 
              BloodPressure_ViewModel.ActiveViewModel.
                  Model.GetDiastolicError( (string) value );

            if ( diastolicErrorType == BloodPressureTestResult.DiastolicErrorType.None )
                return new ValidationResult( true, null );
            else
                return new ValidationResult( false, diastolicErrorType );
        }
    }
}

Building the Demo

The demo application is localized, so there are a few steps with which some might not be familiar. If you open the solution, you will see that there are two projects: the startup project, named ApplicationWithView, and the class library, named ClassLibraryOfModelAndViewModel.

To make the application localizable, I edited ApplicationWithView.csproj by adding <UICulture>en-US</UICulture> to the first PropertyGroup element, and I uncommented the line "[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]" in the application project's AssemblyInfo.cs. I then did a Rebuild All of the project in Debug mode, which created ApplicationWithView.resources.dll in the application project's bin\Debug\en-US directory. Running Localize_CreateFileToTranslate.bat in the application project's folder, which runs Microsoft's localization tool, LocBaml, created ApplicationWithView_en-US.csv. I copied that .csv file twice, renaming them to ...es-MX.csv for Spanish (Español) for Mexico, and ...fr-FR.csv for French for France, and translated the text in those files. I then ran the batch files Localize_ProcessTranslatedFile_es-MX.bat and Localize_ProcessTranslatedFile_fr-FR.bat, which run LocBaml and create localized resources in folders named es-MX and fr-FR in the application project's bin\Debug directory.

To build the demo using Visual Studio 2010, please perform the following steps:

  1. You needn't edit the application's project file or the AssemblyInfo.cs file for localization, since I already did.
  2. Rebuild All the solution in Debug mode. This will create the English resource file for the application project.
  3. Open the folder for the application project, named ApplicationWithView, in a Windows Explorer window.
  4. You needn't create ApplicationWithView_en-US.csv by running Localize_CreateFileToTranslate.bat, since it already exists; but if you do, it will just replace it with an identical file.
  5. You needn't create the Spanish and French .csv files, since I already did.
  6. Run the batch files Localize_ProcessTranslatedFile_es-MX.bat and Localize_ProcessTranslatedFile_fr-FR.bat to create the Spanish and French resource files.

If you wish to build the Release version, for some reason, you may perform the following:

  1. Rebuild All the solution in Release mode. This will create the English resource file for the application project.
  2. Copy the es-MX and fr-FR directories from the application project directory's bin\Debug to bin\Release. Don't copy the en-US directory, because the release version of the application needs the release version of the English resource file.

Please note that the solution and LocBaml use .NET Framework 4.0. I mention this because as of this writing, if Microsoft has released a .NET 4 version of LocBaml, I couldn't find it. I did find a .NET 4 version, however, at http://michaelsync.net/2010/03/01/locbaml-for-net-4-0, and I am thankful to Michael Sync for having tweaked the existing version to work with .NET 4.

Summary

MVVM gives you the ability to fully assign the display functionality to the View, perform dependent input validation, handle multiple view instances, and perform whole-view validation. This article presented an example that addressed each of these tasks. I believe these approaches will help developers improve their software designs, and therefore develop more maintainable software.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
President Perceptions Unlimited, Inc.
United States United States
Martin Joel is the President and primary developer of Perceptions Unlimited, Inc., an OEM software engineering company that licenses software to and develops custom software and software components for other companies to include in their products.

Martin Joel has been a software engineer since C was just a letter and assembly language was the only solution for decent performance.

Comments and Discussions

 
GeneralI use IDataErrorInfo but I have also used this approach quite successfully to Pin
Sacha Barber20-Jul-10 21:50
Sacha Barber20-Jul-10 21:50 
GeneralGood article, but something fishy with the formatting of last half Pin
leppie20-Jul-10 20:15
leppie20-Jul-10 20:15 
GeneralRe: Good article, but something fishy with the formatting of last half Pin
uncager21-Jul-10 3:25
uncager21-Jul-10 3:25 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.