Localization and Complex Validation in MVVM
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
- Basic Validation
- Whole-View Validation
- Dependent Validation and Multiple View Instances
- Inside the Demo Application
- Building the Demo
- Summary
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.
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.
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.
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.
Inside the Demo Application
The demo application is organized as follows:
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:
<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:
<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:
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:
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:
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:
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:
- You needn't edit the application's project file or the AssemblyInfo.cs file for localization, since I already did.
- Rebuild All the solution in Debug mode. This will create the English resource file for the application project.
- Open the folder for the application project, named ApplicationWithView, in a Windows Explorer window.
- 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.
- You needn't create the Spanish and French .csv files, since I already did.
- 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:
- Rebuild All the solution in Release mode. This will create the English resource file for the application project.
- 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.