65.9K
CodeProject 正在变化。 阅读更多。
Home

MVVM 中的本地化和复杂验证

starIconstarIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIcon

2.50/5 (3投票s)

2010年7月21日

CPOL

8分钟阅读

viewsIcon

43221

downloadIcon

1202

本文介绍了处理 MVVM 中一些更棘手的方面的方法,包括错误消息本地化、多控件验证、多个视图实例的验证以及整个视图的验证。

概述

MVVM(模型-视图-视图模型)是一个很棒的设计模式,具有许多优点。您已经知道了这一点,否则您就不会阅读 MVVM 文章了。它也可能变得相当复杂,您也已经知道了这一点,否则您就不会阅读 MVVM 文章了。

目录

引言

本文以及此处提供的简单软件,解决了 MVVM 中以下棘手的方面:

  • 错误消息本地化
  • 多控件验证
  • 多个视图实例的验证
  • 整个视图的验证

我还就正确的 MVVM 设计提出了以下观点:

  • 不应在 Model 或 ViewModel 中出现任何显示的文本。
  • 认为视图不应包含任何代码的观点是不正确的。
  • 特定于显示的**代码**属于视图。
  • 视图不应包含业务逻辑。

基本验证

MVVM 模式中有两种输入验证机制:IDataErrorInfo 接口和 ValidationRule 类。在这两种情况下,验证都可以在 Model、ViewModel 或两者中进行,这才是正确的做法,而不是在 View 中。问题在于,虽然验证不应在 View 中进行,但错误消息的文本是一个显示问题,而不是业务逻辑问题,因此 View 应负责实际的错误消息文本。此问题的一个重要子集是本地化。证明应用程序遵循 MVVM 模式的一种方法是,Model 和 ViewModel 可以作为类库在 View(或 Views)之前编写。应该可以构建一个包含此类 M-VM 类库的应用程序,并在不修改 M-VM 类库的情况下添加本地化。

演示应用程序通过将验证规则(ValidationRule 的子类)放在 M-VM 类库中,让验证规则返回错误枚举而不是错误字符串给 View,并在错误消息的绑定中放置一个 Converter,让 View 将错误枚举转换为可本地化的字符串来解决错误消息本地化问题。

演示应用程序模拟了血压研究的软件。我选择这个主题是因为它允许演示跨多个控件的验证,即收缩压和舒张压,因为舒张压需要小于收缩压。

第一个窗口允许用户选择语言。该窗口本身将以系统当前的区域设置显示,如果是墨西哥西班牙语或法语,则以该语言显示;否则,将以英语显示。

按下“开始”后,应用程序将以所选语言显示,而忽略系统设置。然后会显示主窗口,其中包含一个用于创建和显示一个或多个视图实例的按钮。

每次按下按钮时,都会创建一个新的视图实例并显示。如下面的图像所示,在工具提示中显示的错误消息已本地化。使用类似的机制,每当收缩压和舒张压都有效时,就会显示血压健康分类(例如正常、低血压、1 级高血压等),同样以正确的语言显示,方法是在 View 的代码隐藏中转换绑定的枚举。

依赖验证和多个视图实例

开发此应用程序时面临的挑战之一是 ValidationRule 类的 Validate 函数签名仅接受值和区域设置,而不接受其他参数来指示源。在大多数情况下,当控件的验证不依赖于其他控件的值,并且 View 类只能存在一个实例时,这都没问题。当它依赖于其他控件时,就需要一种机制来允许验证器访问依赖值。

在此应用程序中,当前 ViewModel 存储为 ViewModel 类的静态变量,并在 View 加载时或变为活动状态时由 View 设置。然后,验证器可以通过当前 ViewModel 访问依赖值。

整个视图的验证

另一个挑战是知道何时视图是有效的,例如知道是否可以保存其 Model,或者确定何时可以启用“保存”按钮或菜单项。当视图中的控件直接绑定到 Model 的成员,并且绑定系统处理类型转换时,当控件存在验证错误时,ViewModel 无法得知,因为它只能访问传输到 Model 的最后一个有效值。一个相关的问题是验证从未被编辑过的控件。我使用了 Josh Smith 小巧而强大的 RelayCommand 类来启用和处理 ViewModel 中的“保存”按钮。我的 CanSave 函数需要强制 View 验证所有控件,然后让 ViewModel 知道 View 中的一切是否有效。当 View 构建其 ViewModel 时,它会传递一个函数委托,ViewModel 在 CanSave 中调用该委托,以及一个在保存完成后调用的函数,该函数可以将异常传递给 View,以便它能显示任何本地化的错误消息。

演示应用程序内部

演示应用程序的组织结构如下:

我不会展示所有源代码,只展示相关和有趣的部分。

视图

用于舒张压的 TextBox 的相关部分如下所示:

<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>

在此,样式、错误模板和验证规则按照 MSDN 关于“Validation.HasError 附加属性”的描述进行设置。View 的 DataContext 设置为 ViewModel,ViewModel 在名为 Model 的属性中包含 Model。

窗口资源的相关部分包括:

<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>

这大部分是标准的。有趣的部分是 ToolTip 绑定上的转换器,它允许 View 在其代码隐藏中本地化错误消息。

关于 MVVM 模式的一个误解是,View 永远不应该有任何代码隐藏。View 中可以有代码隐藏,只要它只处理显示问题即可。引用 Josh Smith 的书《Advanced MVVM》中的话:“在使用 ViewModel 时,您的 View 仍然可以(在许多情况下也应该)在其代码隐藏文件中包含某些类型的代码。”以下是 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 "";
        }
    }
}

模型

这是完整的 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;
        }
    }
}

视图模型

这是完整的 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
        }
    }
}

验证器

这是舒张压的验证器:

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 );
        }
    }
}

构建演示

演示应用程序已本地化,因此有几个步骤可能有些人不熟悉。如果您打开解决方案,您会看到有两个项目:启动项目,名为 ApplicationWithView,以及类库,名为 ClassLibraryOfModelAndViewModel。

为了使应用程序可本地化,我编辑了 ApplicationWithView.csproj,在第一个 PropertyGroup 元素中添加了 <UICulture>en-US</UICulture>,并取消了应用程序项目 AssemblyInfo.cs 中 "[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]" 这一行的注释。然后,我在 Debug 模式下对项目执行了“全部重新生成”,这会在应用程序项目的 bin\Debug\en-US 目录中创建 ApplicationWithView.resources.dll。在应用程序项目文件夹中运行 Localize_CreateFileToTranslate.bat,它会运行 Microsoft 的本地化工具 LocBaml,创建了 ApplicationWithView_en-US.csv。我将该 .csv 文件复制了两次,分别重命名为 ...es-MX.csv(用于墨西哥西班牙语)和 ...fr-FR.csv(用于法国法语),并在这些文件中翻译了文本。然后,我运行了批处理文件 Localize_ProcessTranslatedFile_es-MX.batLocalize_ProcessTranslatedFile_fr-FR.bat,它们会运行 LocBaml 并在应用程序项目 bin\Debug 目录中的名为 es-MXfr-FR 的文件夹中创建本地化资源。

要使用 Visual Studio 2010 构建演示,请执行以下步骤:

  1. 您无需编辑应用程序的项目文件或 AssemblyInfo.cs 文件进行本地化,因为我已经完成了。
  2. 在 Debug 模式下对解决方案执行“全部重新生成”。这将为应用程序项目创建英语资源文件。
  3. 在 Windows Explorer 窗口中打开名为 ApplicationWithView 的应用程序项目文件夹。
  4. 您无需通过运行 Localize_CreateFileToTranslate.bat 来创建 ApplicationWithView_en-US.csv,因为它已经存在;但如果您这样做,它只会用一个相同的文件替换它。
  5. 您无需创建西班牙语和法语的 .csv 文件,因为我已经完成了。
  6. 运行批处理文件 Localize_ProcessTranslatedFile_es-MX.batLocalize_ProcessTranslatedFile_fr-FR.bat 来创建西班牙语和法语资源文件。

如果您想构建 Release 版本(无论出于何种原因),可以执行以下操作:

  1. 在 Release 模式下对解决方案执行“全部重新生成”。这将为应用程序项目创建英语资源文件。
  2. 将应用程序项目目录的 bin\Debug 中的 es-MXfr-FR 目录复制到 bin\Release。请勿复制 en-US 目录,因为 Release 版本的应用程序需要 Release 版本的英语资源文件。

请注意,解决方案和 LocBaml 使用 .NET Framework 4.0。我之所以提到这一点,是因为截至本文撰写之时,如果 Microsoft 发布了 .NET 4 版本的 LocBaml,我未能找到。但我确实找到了一个 .NET 4 版本,网址是 http://michaelsync.net/2010/03/01/locbaml-for-net-4-0,我很感谢 Michael Sync 将现有版本进行了调整以兼容 .NET 4。

总结

MVVM 使您能够将显示功能完全分配给 View,执行依赖输入验证,处理多个视图实例,并执行整个视图的验证。本文提供了一个示例,解决了这些任务。我相信这些方法将帮助开发人员改进他们的软件设计,从而开发出更易于维护的软件。

© . All rights reserved.