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

总视图验证

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (59投票s)

2009年1月24日

CPOL

11分钟阅读

viewsIcon

159202

downloadIcon

1347

跨业务对象验证:一种更集中的视图。

引言

WPF 是一项相对较新的技术(好吧,它已经存在一段时间了),因此它仍在不断发展,新的技术和想法也在不断涌现。作为开发人员,我们应该为用户提供可用的系统。作为可用系统的一部分,我们必须提供某种形式的输入验证。现在,WPF 确实提供了一个标准的机制来进行验证,但正如你将看到的,它并不完美,并且在某些领域存在局限性。

本文将尝试概述一种可能的解决方案来解决 WPF 内置的不足之处。它确实假设您对 WPF 和验证有一定了解,但它应该涵盖大多数想法,并且足够详细,以便 WPF 新手也能理解。

本文试图解决什么问题

如我所述,本文将尝试向您展示一种替代的验证概念,该概念弥补了标准 WPF 验证机制的不足。

但标准 WPF 验证机制有什么问题呢?嗯,通常,内置的 WPF 验证机制提供了验证单个对象的方法。所以,如果你愿意,它为该对象提供了边界检查。那么这是如何工作的呢?嗯,虽然这现在是本文的主要重点,但值得快速提及一下……所以,继续。

标准 WPF 验证工作原理如下

在 .NET 3.0 时代,您通常会这样做:

<Binding Source="{StaticResource data}" Path="Age"
    UpdateSourceTrigger="PropertyChanged">
    <Binding.ValidationRules>
        <ExceptionValidationRule/>
        </Binding.ValidationRules>
</Binding>

在您的绑定中添加 ValidationRules

这还可以,但如果您想要一种特定的验证类型,您要么必须使用标准的 ValidationRule,要么创建自己的,这可能看起来像这样:

class FutureDateRule : ValidationRule
{
    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        DateTime date;
        try
        {
            date = DateTime.Parse(value.ToString());
        }
        catch (FormatException)
        {
            return new ValidationResult(false, "Value is not a valid date.");
        }
        if (DateTime.Now.Date > date)
        {
            return new ValidationResult(false, "Please enter a date in the future.");
        }
        else
        {
            return ValidationResult.ValidResult;
        }
    }
}

哎哟,真痛苦。

幸运的是,在 .NET 3.5 中,情况有所改善。我们能够直接在业务对象上使用 IDataErrorInfo 接口,它就像这样工作:

public class Person : IDataErrorInfo
{

    private int age;

    public int Age
    {
        get { return age; }
        set { age = value; }
    }

    public string Error
    {
        get
        {
            return null;
        }
    }

    public string this[string name]
    {
        get
        {
            string result = null;
            if (name == "Age")
            {

                if (this.age < 0 || this.age > 150)
                {
                    result = 
                      "Age must not be less than 0 or greater than 150.";
                }
            }
            return result;
        }
    }
}

而 XAML(实际上是绑定代码)看起来会像这样:

<Binding Source="{StaticResource data}" Path="Age"
 UpdateSourceTrigger="PropertyChanged"
 ValidatesOnDataErrors="True"   />

这也有些痛苦,因为我们不得不在业务对象中编写大量的规则。我见过几个人尝试使用各种技术来减轻这种机制的痛苦,例如:

您可以使用以下链接阅读有关标准 WPF 验证机制的更多信息:

我个人对所有这些标准方法感到最大的问题是:

  1. 您只能验证您正在绑定的对象。在某些情况下,这还可以,但我的业务中存在非常复杂的、跨多个对象的业务数据规则。这就是使用标准 WPF 验证技术全部失效的地方。我完全不知道如何使用标准机制通过另一个对象的值来交叉验证一个字段。
  2. 我的某些公司并不真正接受业务规则存在于业务对象中的想法;这似乎使对象臃肿。我们更喜欢精简、高效、轻量级的业务对象。Rocky Lhotka CSLA 框架提倡使用持有自己规则的业务对象,他是一位非常、非常聪明的人,所以也许这并不全是坏事。这取决于个人品味,我猜。

然而,不可否认的是,上面的第 1 点无法通过让每个业务对象持有自己的验证规则来解决。我的意思是,如果一个业务对象需要知道另一个对象才能进行验证,它该如何进行验证。即使使用一些新的 WPF 风格设计模式(如 MVVM),我们也可能会有一个 ViewModel 持有多个业务对象,而这些业务对象驱动单个视图。在这些情况下,很可能会需要一些跨业务对象的验证。

这促使我思考了一下,并让我放弃了我一直认为是个好主意的东西;所以我放弃了使用 IDataErrorInfo 接口。但为什么我这样做呢?

那么你的想法是什么?

嗯,我个人认为验证逻辑实际上不属于对象本身。我决定将这些规则移出业务对象。那么我在哪里可以进行验证呢?我必须在某处进行,对吧?是的,所以我的决定是坚持使用 WPF 的一些好东西,例如:

  • 模型-视图-视图模型设计模式
  • INotifyPropertyChanged,用于愉快的绑定

然后我决定这样做:

  1. 将所有验证从我的业务对象中移到一个中央验证对象,该对象通过访问 ViewModel 来了解所有相关对象。
  2. ViewModel 会使自己对验证器可见。
  3. ViewModel 将持有驱动视图的对象。
  4. 视图将绑定到 ViewModel。
  5. ViewModel 将持有所有验证错误的列表。
  6. 创建了一组新的控件(我只做了 TextBox,但这个想法适用于您选择的任何控件),当绑定时,它们将接受一个参数,该参数指示它感兴趣的验证失败列表。

差不多就是这样。那么代码看起来怎么样?

嗯,让我们看一个示例应用程序(这是附件中的演示应用程序,但您可以根据自己的实际需求进行更改)。

好的,那么它是如何工作的

嗯,很简单,它是这样工作的。实际的业务对象(Person 类)仅具有通过 INotifyPropertyChanged 接口机制通知绑定更改的属性。每个窗口有一个 ViewModel(Window1ViewModel)对象(您可能决定有多个 ViewModel 来管理您的页面,这取决于您),它持有视图(Window1)需要绑定的多个业务对象(Person 类)。所以基本上,视图将直接绑定到 ViewModel(这是 MVVM 模式)。ViewModel 还持有一个验证器(Window1Validator),该验证器负责运行给定 ViewModel 的所有业务逻辑。因此,验证器(Window1Validator)需要了解 ViewModel(Window1ViewModel),以便它可以检查 ViewModel(Window1ViewModel)中当前驱动视图(Window1)的业务对象(Person 类)的值。

那么,接下来会发生什么呢?视图(Window1)将在某个阶段需要验证其内容。对于演示,这一刻发生在单击“验证”按钮时,它将调用关联 ViewModel(Window1ViewModel)内的 Validate() 方法。当 ViewModel 被要求验证时,它只是将调用传递给其内部验证器(Window1Validator)。由于内部 ViewModel 持有的验证器了解 ViewModel(Window1ViewModel),因此只需运行您想要的任何业务验证逻辑即可。

当您运行业务验证逻辑时,ViewModel(Window1ViewModel)持有的 ObservableCollection<ValidationFailure> 会被添加,其中 ValidationFailure 键的 Key 属性将是某个唯一的名称。这通常可以是您正在验证的属性的名称,例如“Age”。这种方法的优点在于,您可以访问驱动视图的 **所有** 对象,从而允许跨对象验证。据我所知,这是标准 WPF 验证框架无法提供的。

因此,当我们完成所有验证逻辑后,我们会在 ViewModel(Window1ViewModel)中得到一堆由 ObservableCollection<ValidationFailure> 形式表示的错误规则,这些规则可以用于绑定。由于每个 ValidationFailure 键都是某个唯一的名称,我们可以仅提取与视图上特定字段匹配的那些,这是通过使用 ValueConverter 完成的,或者选择查看整个视图的所有 ObservableCollection<ValidationFailure>

大致就是这样,所以是时候看代码了。

代码示例

所以,我想现在是时候开始检查代码了。嗯,让我们从业务对象本身开始。演示使用了以下业务对象:

/// <summary>
/// A simple Person domain object
/// </summary>
public class Person : DomainObject
{
    #region Data
    private String firstName = String.Empty;
    private String lastName = String.Empty;
    private Boolean isAbleToVote = false;
    private Int32 age = 0;
    #endregion

    #region Public Properties
    public String FirstName
    {
        get { return firstName;  }
        set
        {
            firstName = value;
            NotifyChanged("FirstName");
        }
    }

    public String LastName
    {
        get { return lastName; }
        set
        {
            lastName = value;
            NotifyChanged("LastName");
        }
    }


    public Boolean IsAbleToVote
    {
        get { return isAbleToVote; }
        set
        {
            isAbleToVote = value;
            NotifyChanged("IsAbleToVote");
        }
    }


    public Int32 Age
    {
        get { return age; }
        set
        {
            age = value;
            NotifyChanged("Age");
        }
    }
    #endregion
}

这个类继承自一个名为 DomainObject 的基类,它看起来像这样:

/// <summary>
/// The class all domain objects must inherit from. 
/// 
/// INotifyPropertyChanged : Provides change notification
/// to allow WPF bindings to work, without the 
/// need to inherit from a WPF specifio class.
/// So this will work even with WinForms/ASP .NET
/// </summary>
[Serializable()]
public abstract class DomainObject : INotifyPropertyChanged
{
    #region Data
    protected int id;
    #endregion

    #region Ctor
    /// <summary>
    /// Constructor.
    /// </summary>
    public DomainObject()
    {
    }
    #endregion

    #region Public Properties

    /// <summary>
    /// Gets or sets the Address primary key value.
    /// </summary>
    public int ID
    {
        get { return id; }
        set
        {
            id = value;
            NotifyChanged("ID");
        }
    }
    #endregion

    #region INotifyPropertyChanged Implementation

    /// <summary>
    /// Occurs when any properties are changed on this object.
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;


    /// <summary>
    /// A helper method that raises the PropertyChanged event for a property.
    /// </summary>
    /// <param name="propertyNames">The names
    ///             of the properties that changed.</param>
    protected virtual void NotifyChanged(params string[] propertyNames)
    {
        foreach (string name in propertyNames)
        {
            OnPropertyChanged(new PropertyChangedEventArgs(name));
        }
    }

    /// <summary>
    /// Raises the PropertyChanged event.
    /// </summary>
    /// <param name="e">Event arguments.</param>
    protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
    {
        if (this.PropertyChanged != null)
        {
            this.PropertyChanged(this, e);
        }
    }

    #endregion
}

如您所见,DomainObject 类仅提供 INotifyPropertyChanged 接口实现。接下来,让我们检查 ViewModel 代码,它在附件的演示代码中看起来像这样:

/// <summary>
/// View model for Window1
/// </summary>
public class Window1ViewModel : ViewModelBase
{
    #region Data
    private Window1Validator window1Validator = null;
    private Person currentPerson1 = new Person();
    private Person currentPerson2 = new Person();
    private ObservableCollection<ValidationFailure>
        validationErrors = new ObservableCollection<ValidationFailure>();

    private ICommand validateCommand;
    #endregion

    #region Ctor

    public Window1ViewModel()
    {
        //initialise validator for this view model
        window1Validator = new Window1Validator(this);

        //wire up command
        validateCommand = new SimpleCommand
        {
            CanExecuteDelegate = x => true,
            ExecuteDelegate = x => Validate()
        };
    }

    #endregion

    #region Public Properties

    /// <summary>
    /// Return an ICommand that can execute the search
    /// </summary>
    public ICommand ValidateCommand
    {
        get { return validateCommand; }
    }


    public ObservableCollection<ValidationFailure> ValidationErrors
    {
        get { return validationErrors; }
        set
        {
            validationErrors = value;
            NotifyChanged("ValidationErrors");
        }
    }

    public Person CurrentPerson1
    {
        get { return currentPerson1; }
        set
        {
            currentPerson1 = value;
            NotifyChanged("CurrentPerson1");
        }
    }

    public Person CurrentPerson2
    {
        get { return currentPerson2; }
        set
        {
            currentPerson2 = value;
            NotifyChanged("CurrentPerson2");
        }
    }

    #endregion

    #region Public Methods
    public void Validate()
    {
        window1Validator.Validate();
    }
    #endregion

}

同样,这个类继承自一个提供一些有用功能的基类,例如 INotifyPropertyChanged 接口实现。还可以看到,这个示例 ViewModel 持有多个将被视图绑定的业务对象。然后,在视图中,此 ViewModel 用于绑定,其中视图的 DataContext 被设置为该 ViewModel。让我们接下来看看视图。

/// <summary>
/// Demonstrates an alternative way fo validation across all objects within a
/// View, rather than using IDataErrorInfo which must be done on your actual
/// DomainObject and can only validate internal properties.
/// </summary>
public partial class Window1 : Window
{
    private Window1ViewModel window1ViewModel = new Window1ViewModel();

    public Window1()
    {
        InitializeComponent();
        this.DataContext = window1ViewModel;
    }
}

如您所见,视图正在使用 Window1ViewModel ViewModel 作为其 DataContext,这使得视图上的控件可以直接绑定到 ViewModel。我不会用所有视图的 XAML 来烦扰您,但我只会向您展示其中一个 ViewModel 持有的业务对象的典型绑定。

<!-- FirstName Property-->
<StackPanel Orientation="Horizontal" Margin="5">
    <Label Content="FirstName" Width="100" />
    <local:TextBoxEx x:Name="txtFirstName1" Width="150" 
       Height="25" FontSize="12"  FontWeight="Bold" 
       ValidationErrors="{Binding Path=ValidationErrors, Mode=OneWay, 
          Converter={StaticResource ValidationErrorsLookupConv}, 
          ConverterParameter='FirstName1'}"
       Text="{Binding Path=CurrentPerson1.FirstName, UpdateSourceTrigger=PropertyChanged}"
       Foreground="Black" HorizontalAlignment="Center" 
       HorizontalContentAlignment="Center"    />
</StackPanel>

因此,您可以看到我们有一个专用的 TextBoxTextBoxEx),它被绑定到视图当前 DataContext(实际上是 Window1ViewModel ViewModel)的 ValidationErrors 属性。还可以看到我们正在使用 ValueConverterValidationErrorsLookupConverter),我们向 ValueConverter 传递了一个参数值。

要理解该机制,我们首先需要了解 Window1ViewModel ViewModel 中的 ValidationErrors 属性是如何工作的,所以让我们现在来检查一下。

基本上,当构建一个新的 ViewModel(Window1ViewModel)时,它会创建一个新的验证器对象(Window1Validator),用于对 ViewModel(Window1ViewModel)执行所有验证。验证器对象(Window1Validator)只是运行一堆非常枯燥的规则代码,并将一个新的 ValidationFailure 对象添加到 ViewModel(Window1ViewModel)中持有的 ObservableCollection<ValidationFailure> 列表中。

让我们检查一下验证器对象(Window1Validator)的代码;它很乏味,但这就是验证代码的性质。

/// <summary>
/// Validation code for Window1
/// </summary>
public class Window1Validator
{
    #region Data
    private Window1ViewModel window1ViewModel { get; set; }
    #endregion

    #region Ctor
    public Window1Validator(Window1ViewModel window1ViewModel)
    {
        this.window1ViewModel = window1ViewModel;
    }
    #endregion

    #region Public Methods
    public void Validate()
    {
        ObservableCollection<ValidationFailure> localValidationErrors= 
            new ObservableCollection<ValidationFailure>();

        #region Validate CurrentPerson1

        if (window1ViewModel.CurrentPerson1.Age < 0)
            localValidationErrors.Add(
                new ValidationFailure("Age1", 
                    "Person 1 Age cant be < 0"));

        if (window1ViewModel.CurrentPerson1.Age > 65 
            && window1ViewModel.CurrentPerson1.IsAbleToVote)
            localValidationErrors.Add(
                new ValidationFailure("Age1", 
                    "Person 1 Age, You can't vote > 65"));

        if (window1ViewModel.CurrentPerson1.FirstName == String.Empty)
            localValidationErrors.Add(
                new ValidationFailure("FirstName1", 
                    "Person 1 FirstName can't be empty"));

        if (window1ViewModel.CurrentPerson1.LastName == String.Empty)
            localValidationErrors.Add(
                new ValidationFailure("LastName1", 
                    "Person 1 LastName can't be empty"));
        

        #endregion

        #region Validate CurrentPerson2

        if (window1ViewModel.CurrentPerson1.Age < 18 
            && window1ViewModel.CurrentPerson2.Age == 0)
            localValidationErrors.Add(
                new ValidationFailure(
                    "Age2", 
                    "Person 2 Age cant be < 0 if Person1 Age < 18"));

        if (window1ViewModel.CurrentPerson2.Age > 65 
            && window1ViewModel.CurrentPerson2.IsAbleToVote)
            localValidationErrors.Add(
                new ValidationFailure("Age2", 
                    "Person 2, You can't vote > 65"));

        if (window1ViewModel.CurrentPerson2.FirstName == String.Empty)
            localValidationErrors.Add(
                new ValidationFailure("FirstName2", 
                    "Person 2 FirstName can't be empty"));

        if (window1ViewModel.CurrentPerson2.LastName == String.Empty)
            localValidationErrors.Add(
                new ValidationFailure("LastName2", 
                    "Person 2 LastName can't be empty"));


        #endregion

        window1ViewModel.ValidationErrors = localValidationErrors;
    }
    #endregion
}

需要注意的重要部分是,ViewModel(Window1ViewModel)持有所有 ValidationFailure 的完整列表,并且每个 ValidationFailure 都有一个键,可以用于仅获取与特定 TextBox 相关的 ValidationFailure。这正是 ValueConverterValidationErrorsLookupConverter)内部发生的情况,我们在其中将参数值传递给 ValueConverter,该值用于仅获取与具有正确键(值转换器参数值)的 TextBox 相关的 ValidationFailure,这将用于过滤所有 ValidationFailure 的列表,使其仅匹配键(值转换器参数值)。

回想一下 XAML 中单个 TextBoxEx 的此绑定:

我不会用所有视图的 XAML 来烦扰您,但我只会向您展示其中一个 ViewModel 持有的业务对象的典型绑定。

<!-- FirstName Property-->
<StackPanel Orientation="Horizontal" Margin="5">
    <Label Content="FirstName" Width="100" />
    <local:TextBoxEx x:Name="txtFirstName1" Width="150" 
      Height="25" FontSize="12"  FontWeight="Bold" 
      ValidationErrors="{Binding Path=ValidationErrors, Mode=OneWay, 
                        Converter={StaticResource ValidationErrorsLookupConv}, 
                        ConverterParameter='FirstName1'}"
      Text="{Binding Path=CurrentPerson1.FirstName, UpdateSourceTrigger=PropertyChanged}"
      Foreground="Black" HorizontalAlignment="Center" 
      HorizontalContentAlignment="Center"    />
</StackPanel>

看到 ValidationErrors 部分和 ConverterParameter='FirstName1',这部分使我们能够仅获取当前 TextBox 所需的验证错误。

当您看到 ValueConverterValidationErrorsLookupConverter)时,这可能会更清楚。

/// <summary>
/// Obtains a sub set of all validation errors from the
/// Bound object (ViewModel) that matches a particular key
/// for the actual bound control
/// </summary>
[ValueConversion(typeof(ObservableCollection<ValidationFailure>), 
    typeof(ObservableCollection<ValidationFailure>))]
public class ValidationErrorsLookupConverter : IValueConverter
{
    #region IValueConverter implementation
    public object Convert(object value, Type targetType, 
                  object parameter, CultureInfo culture)
    {
        if (value != null)
        {
            ObservableCollection<ValidationFailure> validationLookup =
          (ObservableCollection<ValidationFailure>)value;

            List<ValidationFailure> failuresForKey = 
                (   
                    from vf in validationLookup
                    where vf.Key.Equals(parameter.ToString())
                    select vf
                ).ToList();

            return new ObservableCollection<ValidationFailure>(failuresForKey);

        }

        return null;
    }

    public object ConvertBack(object value, Type targetType, 
                  object parameter, CultureInfo culture)
    {
        throw new NotImplementedException("Can't convert back");
    }
    #endregion
}

最后一步是拥有一个专用的 TextBoxTextBoxEx),它知道如何在其弹出窗口中显示自己的验证错误列表。现在,这只是我选择这样做的方式,您可能会想出不同的方法,但这正是我选择的方式。

我应该指出,通常我反对创建继承自 System.Windows.Controls 的专用控件,因为大多数额外的行为都可以通过附加属性添加,但这种情况似乎不适合,因为我希望 XAML 中的弹出窗口由鼠标移动等触发。

Josh Smith 可能会说:“哦,你只需这样做,然后那样,你就完成了。” 我只会微笑着对他说:“这我知道了,谢谢 Josh。”

自从撰写本文以来,一位读者“SE_GEEK”提出了一种新的控件,它继承自 ContentControl 而不是 TextBox;您可以在论坛链接中使用以下链接阅读更多关于这种方法的信息:GlobalWPFValidation.aspx?msg=2895494#xx2895494xx

无论如何,这是专用 TextBoxTextBoxEx)的代码,它知道如何在弹出窗口中显示自己的 ValidationFailure 列表。我只为附件中的演示代码做了专用 TextBoxTextBoxEx),但您可以将相同的想法应用于任何标准控件。在工作中,我们实际上已经用 CheckBox/ComboBox 和其他控件做过,并且效果非常好。

/// <summary>
/// A Simple extentended TextBox that supports our 
/// custom validation collection & logic
/// </summary>
[TemplatePart(Name = "PART_PopupErrors", Type = typeof(Popup))]
[TemplatePart(Name = "PART_Close", Type = typeof(Button))]
public class TextBoxEx : TextBox
{
    #region Data
    private Popup errorPopup = null;
    private Button cmdClose = null;
    #endregion

    #region Ctor
    public TextBoxEx() : base()
    {
        this.MouseEnter += (s, e) =>
        {
            if (errorPopup != null && !IsValid)
            {
                errorPopup.IsOpen = false;
                errorPopup.IsOpen = true;
            }
        };
    }
    #endregion

    #region Private Methods

    private void ErrorPopup_MouseUp(object sender, 
    System.Windows.Input.MouseButtonEventArgs e)
    {
        if (errorPopup.IsOpen)
            errorPopup.IsOpen = false;
    }

    private void CmdClose_Click(object sender, RoutedEventArgs e)
    {
        Button button = e.OriginalSource as Button;
        Popup pop = button.Tag as Popup;
        if (pop != null)
            pop.IsOpen = false;
    }

    #endregion

    #region DPs
    #region IsValid

    /// <summary>
    /// IsValid Dependency Property
    /// </summary>
    public static readonly DependencyProperty IsValidProperty =
        DependencyProperty.Register("IsValid", typeof(bool), typeof(TextBoxEx),
            new FrameworkPropertyMetadata((bool)true));

    /// <summary>
    /// Gets or sets the IsValid property. This dependency property 
    /// indicates ....
    /// </summary>
    public bool IsValid
    {
        get { return (bool)GetValue(IsValidProperty); }
        set { SetValue(IsValidProperty, value); }
    }

    #endregion

    #region NewStyle

    /// <summary>
    /// NewStyle Dependency Property
    /// </summary>
    public static readonly DependencyProperty NewStyleProperty =
        DependencyProperty.Register("NewStyle", typeof(Style), typeof(TextBoxEx),
            new FrameworkPropertyMetadata((Style)null,
                new PropertyChangedCallback(OnNewStyleChanged)));

    /// <summary>
    /// Gets or sets the NewStyle property.
    /// </summary>
    public Style NewStyle
    {
        get { return (Style)GetValue(NewStyleProperty); }
        set { SetValue(NewStyleProperty, value); }
    }

    /// <summary>
    /// Handles changes to the NewStyle property.
    /// </summary>
    private static void OnNewStyleChanged(DependencyObject d, 
    DependencyPropertyChangedEventArgs e)
    {
        ((TextBoxEx)d).Style = e.NewValue as Style;
    }


    #endregion

    #region ValidationErrors

    /// <summary>
    /// ValidationErrors Dependency Property
    /// </summary>
    public static readonly DependencyProperty ValidationErrorsProperty =
        DependencyProperty.Register("ValidationErrors",
            typeof(ObservableCollection<ValidationFailure>), 
        typeof(TextBoxEx),
            new FrameworkPropertyMetadata(
        (ObservableCollection<ValidationFailure>)null,
                new PropertyChangedCallback(OnValidationErrorsChanged)));

    /// <summary>
    /// Gets or sets the ValidationErrors property.
    /// </summary>
    public ObservableCollection<ValidationFailure> ValidationErrors
    {
        get { return (ObservableCollection<ValidationFailure>)
        GetValue(ValidationErrorsProperty); }
        set { SetValue(ValidationErrorsProperty, value); }
    }

    /// <summary>
    /// Handles changes to the ValidationErrors property.
    /// </summary>
    private static void OnValidationErrorsChanged(DependencyObject d, 
        DependencyPropertyChangedEventArgs e)
    {
        ObservableCollection<ValidationFailure> failures =
            e.NewValue as ObservableCollection<ValidationFailure>

        TextBoxEx thisObj = (TextBoxEx)d;

        if (failures == null)
            thisObj.IsValid = true;
        else thisObj.IsValid = failures.Count == 0 ? true : false;
    }



    #endregion

    #endregion

    #region Overrides

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();

        errorPopup = this.Template.FindName("PART_PopupErrors", this) as Popup;
        if (errorPopup != null)
            errorPopup.MouseUp += new MouseButtonEventHandler(ErrorPopup_MouseUp);

        cmdClose = this.Template.FindName("PART_Close", this) as Button;
        if (cmdClose != null)
            cmdClose.Click += new RoutedEventHandler(CmdClose_Click);
    }

    #endregion
}

而且,这是为 TextBoxEx 控件提供实际 Style 的 XAML:

<Style TargetType="{x:Type local:TextBoxEx}">
    <Setter Property="SnapsToDevicePixels" Value="true"/>
    <Setter Property="OverridesDefaultStyle" Value="true"/>
    <Setter Property="FocusVisualStyle"    Value="{x:Null}"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:TextBoxEx}">
                <Grid>
                    <!-- POPUP-->
                    <Popup x:Name="PART_PopupErrors"
                       PlacementTarget="{Binding ElementName=Bd}"   
                       Placement="Relative"
                       AllowsTransparency="True"
                       PopupAnimation="Slide"
                       HorizontalOffset ="20"
                       StaysOpen="False"
                       VerticalOffset="20">
                        <Border HorizontalAlignment="Stretch" 
                            BorderBrush="WhiteSmoke"
                            Background="Red"
                            Margin="0"
                            Width="250"
                            VerticalAlignment="Stretch"
                            Height="120"
                            Opacity="0.97"
                            BorderThickness="2" 
                            CornerRadius="3" >
                            <Border HorizontalAlignment="Stretch" 
                                BorderBrush="Red"
                                Background="White"
                                Margin="0"
                                VerticalAlignment="Stretch"
                                Opacity="1"
                                BorderThickness="5" 
                                CornerRadius="0" >

                                    ......
                                    ......
                                    ......
                                    ......

                                    <ScrollViewer Grid.Row="1" Margin="0" 
                                          HorizontalScrollBarVisibility="Auto" 
                                          VerticalScrollBarVisibility="Auto">

                                        <ItemsControl Margin="0" 
                                          BorderThickness="0"  
                                          ItemsSource="{Binding 
                                            RelativeSource={RelativeSource Mode=FindAncestor, 
                                            AncestorType={x:Type local:TextBoxEx}}, 
                                            Path=ValidationErrors}"/>

                                    </ScrollViewer>
                                </Grid>
                            </Border>
                        </Border>
                    </Popup>

                    <!-- Control-->
                    <Border SnapsToDevicePixels="true" x:Name="Bd"
                        Height="{TemplateBinding Height}" 
                            Width="{TemplateBinding Width}"
                        VerticalAlignment="Center"
                        Background="{TemplateBinding Background}" 
                        CornerRadius="2"
                        BorderBrush="Black" 
                        BorderThickness="2">
                        <ScrollViewer x:Name="PART_ContentHost" 
                          SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" 
                          TextElement.FontSize="{TemplateBinding FontSize}"  
                          VerticalAlignment="Center" 
                          VerticalContentAlignment="Center" 
                          Margin="2,0,0,0"/>
                    </Border>
                </Grid>
                <ControlTemplate.Triggers>

                    <Trigger Property="IsValid" Value="False">
                        <Setter Property="BorderBrush" 
                                TargetName="Bd" Value="Red"/>
                    </Trigger>

                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

差不多就是这样。有了这个机制,我们就可以为当前视图执行几乎任何我们想要的跨业务对象验证。为了证明这一点,这里有几张截图:

这是单个 TextBoxEx 对象的单个弹出窗口:

这是我获得的当前视图的所有错误:

就是这样

我说的就这些了。希望这篇文章对您有所帮助。如果您喜欢它,能否请您好心投票或留言?谢谢。

修订

自发布本文以来,一位同事(Colin Eberhardt)告诉我,从 .NET 3.5 SP1 开始,实际上可以使用 BindingGroups 的标准 WPF 机制来实现这一点。由于 .NET 3.5 SP1 如此庞大,我并不惊讶我错过了这一点。无论如何,Colin 已经写了一篇关于此的出色文章,您可以在他的博客上阅读所有相关内容。这是链接:BindingGroups for Total View Validation

© . All rights reserved.