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

使用 Self-Tracking Entity Generator 和 Visual Studio 2012 构建 WPF 应用程序 - 数据验证

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (13投票s)

2012 年 8 月 27 日

CPOL

13分钟阅读

viewsIcon

35214

本文介绍如何使用自跟踪实体生成器和 Visual Studio 2012 进行数据验证。

  • 这里 下载源代码
  • 请访问此 项目站点 以获取最新版本和源代码。

目录

引言

在本文中,我们将重点介绍如何使用自跟踪实体生成器为 WPF/Silverlight 进行数据验证。使用数据验证的目的是确保在将任何数据存储到数据库之前对其进行验证。它为用户在数据输入任务期间提供了必要的指导,并且是任何 WPF LOB 应用程序的重要组成部分。

自跟踪实体生成器的先前版本严重依赖另一个 Visual Studio 2010 扩展“可移植可扩展元数据”来启用数据验证功能,该功能在 此处 进行了介绍。但是,“可移植可扩展元数据”有其自身的局限性

  • PEM 不支持 CustomValidationAttribute
  • PEM 仅支持在属性级别添加数据验证,不支持在实体级别添加。
  • PEM 仅支持使用非本地化错误消息添加数据验证。
  • PEM 允许在实体属性上多次添加相同类型的验证,这对于 RequiredRegularExpressionDataFieldLength 等验证类型几乎毫无意义。

幸运的是,我们已经在当前版本中解决了以上所有问题。接下来,我们将介绍自动生成的验证辅助方法。之后,我们将讨论在我们的演示应用程序的客户端和服务器端添加验证逻辑的不同方法。

验证辅助方法

客户端上的自动生成验证辅助方法包含以下成员

  • TryValidate() 方法会遍历实体对象的所有数据注释属性和所有自定义验证操作。如果任何验证失败,此函数将返回 false,否则返回 true。
  • TryValidate(string propertyName) 方法会遍历实体对象的指定属性名的所有数据注释属性和所有自定义验证操作。如果任何验证失败,此函数将返回 false,否则返回 true。
  • TryValidateObjectGraph() 方法会遍历整个对象图,并对每个实体对象调用 TryValidate()。如果任何验证失败,此函数将返回 false,否则返回 true。
  • 公共字段 SuspendValidation 可以设置为 true,以在数据绑定更新发生时暂时关闭验证。但是,设置此字段不会影响 TryValidate()TryValidate(string propertyName)TryValidateObjectGraph() 方法。

并且,服务器端验证辅助方法如下

  • Validate() 方法会遍历实体对象的所有数据注释属性和所有自定义验证操作。如果任何验证失败,它将抛出异常。
  • ValidateObjectGraph() 方法会遍历整个对象图,并对每个实体对象调用 Validate()。如果任何验证失败,它将抛出异常。

使用 IDataErrorInfo 接口启用验证

在将控件绑定到视图中的属性时,我们可以选择通过 IDataErrorInfo 接口进行数据验证。首先,如果我们将数据绑定的 ValidatesOnDataErrors 属性设置为 true,绑定引擎将报告绑定数据实体上 IDataErrorInfo 实现的验证错误。其次,如果我们想接收 BindingValidationFailed 事件,可以设置 NotifyOnValidationError 属性为 true;最后,当我们要捕获其他类型的错误(例如数据转换问题)时,可以设置 ValidatesOnExceptions 属性为 true。以下是来自讲师页面的数据绑定示例

使用 INotifyDataErrorInfo 接口启用验证

随着 WPF 4.5 的出现,我们有了更好的数据验证替代方案:INotifyDataErrorInfo 接口。要将验证错误报告从 IDataErrorInfo 实现切换到 INotifyDataErrorInfo 实现,我们需要将 ValidatesOnNotifyDataErrors 属性设置为 true,而不是使用 ValidatesOnDataErrors 属性。

以下是我们演示应用程序的截图,其中显示了一些验证错误。标签的标题文本来自数据绑定属性的 DisplayAttribute.Name 属性(在上述情况下,数据绑定属性是 CurrentInstructor.Name),其颜色从黑色变为红色以指示存在错误。在每个文本框的右侧,有一个 DescriptionViewer 控件,当鼠标指针悬停在图标上时,该控件会显示一个信息图标并显示工具提示中的文本描述。此文本描述来自数据绑定属性的 DisplayAttribute.Description 属性。最后,我们从验证摘要控件获取所有错误消息的摘要。

在学习了如何在用户界面上启用验证之后,我们接下来将讨论添加数据验证逻辑的不同方法。

客户端数据验证

客户端数据验证元数据通过实体数据模型设计器添加,自跟踪实体生成器将使用这些信息来生成实际的客户端数据验证属性。此外,我们需要在提交更改之前调用 TryValidate()TryValidate(string propertyName)TryValidateObjectGraph(),以确保所有客户端验证逻辑都能成功通过。

通过实体数据模型设计器添加属性级验证

让我们先来看看如何通过实体数据模型设计器添加属性级数据验证。假设我们要为 Person 类的 Name 属性添加一个必填字段验证。为此,我们首先需要打开资源文件 *SchoolModelResource.resx*,并添加一个名为 FieldRequiredErrorMessage 的字符串资源,如下所示。

之后,打开 *SchoolModel.edmx* 的实体数据模型设计器,并选择 Person 实体的 Name 属性。在“属性”窗口中,选择“STE Validations”。

接下来,通过选择“STE Validations”集合(上面已突出显示)打开“自跟踪实体属性验证编辑器”窗口,并添加一个 Required 验证条件。要设置此验证条件,我们需要设置 *错误消息* 或 *资源名称* 和 *资源类型* 字段。*错误消息* 字段包含非本地化的错误消息,而另外两个字段指定资源文件中的字符串资源。请注意,在生成实际验证属性时,*资源名称* 和 *资源类型* 字段具有优先权。因此,如果设置了所有三个字段,*错误消息* 字段将被忽略。

对于新添加的 Required 验证条件,我们将设置其 *资源名称* 和 *资源类型* 字段的值。

这是通过选择这两个字段之一,然后单击其省略号按钮来完成的。将弹出第二个模态窗口“错误消息资源选择”,我们可以选择资源文件为 SchoolModelResource.resx,资源名称为上面已创建的 FieldRequiredErrorMessage

保存 EDM 文件 *SchoolModel.edmx* 的更改后,T4 模板将自动重新生成所有自跟踪实体类,并添加下面的数据注释属性。

这完成了为 Person 类的 Name 属性添加必填字段验证的步骤。接下来,我们将讨论如何为实体属性添加自定义验证。

添加属性级自定义验证

要为实体属性添加自定义验证,我们首先需要定义一个具有以下签名的自定义验证方法

public static ValidationResult MethodName(object value, ValidationContext context) {...}

我们的自定义验证示例是验证 Person 类的 Name 属性是否包含任何数字。首先,我们将创建以下名为 ValidatePersonName() 的自定义验证方法。

创建此方法后,我们将再次打开 *SchoolModel.edmx* 的实体数据模型设计器,并选择 Person 实体的 Name 属性。在“属性”窗口中,选择“STE Validations”,然后打开“自跟踪实体属性验证编辑器”窗口。之后,添加一个 CustomValidation 条件,如下所示。

对于新的 CustomValidation 条件,我们需要将其 MethodValidator Type 字段的值链接到刚刚创建的自定义验证方法。这是通过模态窗口“验证器类型和方法选择”完成的,自跟踪实体生成器将自动搜索并找到 ValidatePersonName() 方法。我们只需要选择该选项并保存所有更改。

保存 EDM 文件 *SchoolModel.edmx* 的更改后,我们可以使用“STE 设置”弹出窗口中的更新按钮,重新生成所有自跟踪实体类,并添加下面的数据注释属性。

请注意,自定义验证条件是唯一可以对属性级别添加多次的条件,其他验证条件(如 RequiredRegularExpression)最多只能添加一次。

添加实体级自定义验证

接下来,我们将介绍如何在实体级别添加验证逻辑。我们的第一个示例是验证课程的开始日期是否早于或等于其结束日期。如果不是,课程的开始日期和结束日期字段都将突出显示错误消息:课程结束日期不能早于开始日期。

要启用此类型的验证,我们首先需要将 ValidateEntityOnPropertyChanged 设置为 true。由于实体级别的验证逻辑与任何实体属性无关,因此将 ValidateEntityOnPropertyChanged 设置为 true 可以确保实体级别的验证逻辑在每次更新实体属性时都会被调用。之后,我们需要定义自定义验证方法 ValidateCourseStartAndEndDate(),如下所示。

创建自定义验证方法后,我们基本上遵循相同的步骤:打开 *SchoolModel.edmx* 的实体数据模型设计器,并选择 Course 实体。在“属性”窗口中,选择“STE Validations”以打开“自跟踪实体属性验证编辑器”窗口。

然后添加一个新的 CustomValidation 条件,如下所示。

这是显示与 Course 实体类关联的自动生成的 CustomValidationAttribute 的代码片段。

而这个屏幕显示了当用户输入错误的开始日期和结束日期时的实际验证错误。

自定义验证方法 ValidateCourseStartAndEndDate() 返回一个 ValidationResult 对象,其构造函数接受两个参数:一个显示给用户的错误消息,以及与验证结果关联的成员名称集合。在我们的例子中,成员名称集合包括开始日期和结束日期,这就是为什么在发生错误时,课程的开始日期和结束日期都会被突出显示。

我们的下一个示例是验证课程当前的注册人数是否低于班级容量限制。此示例与前一个示例的区别在于,此自定义验证是真正的实体级别验证,因为它不针对其任何属性报告验证结果。

要设置此自定义验证,我们需要遵循前面概述的相同步骤。第一步是创建一个名为 ValidateEnrollmentLimit() 的自定义验证方法。

由于此自定义验证不通过其任何属性报告验证错误,并且实体级别的验证错误不会自动添加到 ValidationSummary 控件的 Errors 集合中,因此我们必须创建一个名为 ValidationSummaryBehavior 的 WPF 附加行为并使用它来专门监听来自 CurrentCourse 对象的任何实体级别验证错误。

在这里,我们可以看到当课程注册学生人数超过课程规模限制时的实际验证错误。请注意,错误消息未与任何属性关联,并且该错误是关于课程本身的。

到目前为止,我们已经讨论了添加验证逻辑的不同选项。下一个主题是如何确保在将更改提交到服务器端之前不跳过任何客户端验证。

使用 TryValidateObjectGraph() 进行验证

验证辅助方法 TryValidate()TryValidate(string propertyName)TryValidateObjectGraph() 用于确保在将更改提交到服务器端之前,所有客户端验证逻辑都能成功通过。第一个辅助方法不带参数,它会遍历实体对象的所有数据注释属性和所有自定义验证操作。如果任何验证失败,此函数将返回 false;否则返回 true。第二个方法带有一个字符串参数,即需要验证的属性名称,此辅助方法仅针对指定的属性进行验证。最后一个方法 TryValidateObjectGraph() 与第一个方法类似,但它遍历整个对象图而不是单独的实体对象。

下面的代码示例来自 InstructorPageViewModel 类,我们通过对 AllInstructors 列表的每个元素调用 TryValidateObjectGraph() 方法来验证它,以确保我们只在每个讲师都通过验证时才保存更改。

private void OnSubmitAllChangeCommand()
{
    try
    {
        if (!_schoolModel.IsBusy)
        {
            if (AllInstructors != null)
            {
                // we only save changes when all instructors passed validation
                var passedValidation = AllInstructors.All(o => o.TryValidateObjectGraph());
                if (!passedValidation) return;

                _schoolModel.SaveInstructorChangesAsync();
            }
        }
    }
    catch (Exception ex)
    {
        // notify user if there is any error
        AppMessages.RaiseErrorMessage.Send(ex);
    }
}

使用 SuspendValidation 关闭验证

客户端上的另一个功能是公共字段 SuspendValidation。通过将其设置为 true,我们可以在初始化实体对象时跳过调用数据验证逻辑,在这种情况下,我们通常不希望向用户显示任何验证错误。以下示例来自 InstructorPageViewModel 类。当用户想要添加一条新的讲师记录时,会通过首先将 SuspendValidation 字段设置为 true 来创建一个新的 Instructor 对象。这确保了设置其余属性不会触发任何数据验证错误。在对象完全初始化后,我们可以将 SuspendValidation 设置回 false,以便用户后续的任何更改都会触发验证逻辑。

最后一点要记住的是,设置 SuspendValidation 不会影响 TryValidate()TryValidate(string propertyName)TryValidateObjectGraph() 方法。即使 SuspendValidation 设置为 true,这三个方法仍然会触发数据验证。

private void OnAddInstructorCommand()
{
    // create a temporary PersonId
    int newPersonId = AllInstructors.Count > 0
        ? ((from instructor in AllInstructors select Math.Abs(instructor.PersonId)).Max() + 1) * (-1)
        : -1;
    // create a new instructor
    CurrentInstructor = new Instructor
    {
        SuspendValidation = true,
        PersonId = newPersonId,
        Name = string.Empty,
        HireDate = DateTime.Now,
        Salary = null
    };
    CurrentInstructor.SuspendValidation = false;
    // and begin edit
    OnEditCommitInstructorCommand();
}

以上是我们关于客户端数据验证逻辑各个方面的讨论。接下来,我们将转到服务器端数据验证的主题。

服务器端数据验证

两个自动生成的服务器端验证辅助方法是 Validate()ValidateObjectGraph()。除此之外,我们还可以根据需要添加跨实体验证。让我们先看看接下来如何使用这两个验证辅助方法。

使用 ValidateObjectGraph() 进行验证

Validate()ValidateObjectGraph() 方法在服务器端用于确保所有传入的更新调用都通过在客户端定义的同一组验证逻辑。我们添加此额外步骤是因为服务器端作为 WCF 服务公开,并且我们假设调用可能来自任何地方。因此,WCF 服务也需要先验证其数据。

以下是 SchoolService 类中的 UpdateInstructor() 方法,我们在添加和更新操作中都对 Instructor 对象调用 ValidateObjectGraph()。此验证辅助方法遍历整个对象图,并对每个实体对象调用 Validate()。这基本上重复了在客户端定义的数据验证逻辑集。如果任何验证失败,将抛出异常并传回客户端。否则,我们通过调用 context.People.ApplyChanges(item) 然后调用 context.SaveChanges() 来保存更改。

public List<object> UpdateInstructor(Instructor item)
{
    var returnList = new List<object>();
    if (item == null)
    {
        returnList.Add("Instructor cannot be null.");
        returnList.Add(0);
        return returnList;
    }
    try
    {
        using (var context = new SchoolEntities())
        {
            switch (item.ChangeTracker.State)
            {
                case ObjectState.Added:
                    // server side validation
                    item.ValidateObjectGraph();
                    // save changes
                    context.People.ApplyChanges(item);
                    context.SaveChanges();
                    break;
                case ObjectState.Deleted:
                    // verify whether there is any course assigned to this instructor
                    var courseExists = 
                        context.Courses.Any(n => n.InstructorId == item.PersonId);
                    if (courseExists)
                    {
                        returnList.Add("Cannot delete, there still " + 
                           "exists course assigned to this instructor.");
                        returnList.Add(item.PersonId);
                        return returnList;
                    }
                    // save changes
                    context.People.ApplyChanges(item);
                    context.SaveChanges();
                    break;
                default:
                    // server side validation
                    item.ValidateObjectGraph();
                    // save changes
                    context.People.ApplyChanges(item);
                    context.SaveChanges();
                    break;
            }
        }
        returnList.Add(string.Empty);
        returnList.Add(item.PersonId);
    }
    catch (OptimisticConcurrencyException)
    {
        var errorMessage = "Instructor " + item.PersonId + 
            " was modified by another user. " +
            "Refresh that item before reapply your changes.";
        returnList.Add(errorMessage);
        returnList.Add(item.PersonId);
    }
    catch (Exception ex)
    {
        Exception exception = ex;
        while (exception.InnerException != null)
        {
            exception = exception.InnerException;
        }
        var errorMessage = "Instructor " + item.PersonId + 
                           " has error: " + exception.Message;
        returnList.Add(errorMessage);
        returnList.Add(item.PersonId);
    }
    return returnList;
}

跨实体验证

对于上面的同一个 UpdateInstructor() 方法,删除操作的数据验证稍微复杂一些。无需调用 ValidateObjectGraph() 方法。相反,我们执行跨实体验证,并验证是否仍有课程分配给此讲师。如果为真,我们将一条警告消息发送回客户端,说明我们无法删除此讲师(实际消息如下所示)。否则,删除操作将继续执行,将该讲师行从数据库中删除。

总结

我们已经完成了关于如何为 WPF/Silverlight 的自跟踪实体生成器进行数据验证的讨论。首先,我们简要介绍了所有可用的自动生成验证辅助方法。然后,我们讨论了使用 IDataErrorInfoINotifyDataErrorInfo 接口启用验证的各种属性。之后,我们讨论了在客户端和服务器端添加数据验证逻辑的不同方法。

希望您觉得本文有用,请在下方评分和/或留下反馈。谢谢!

历史

  • 2012 年 8 月 - 初始发布。
© . All rights reserved.