域特定语言中的验证

作为域特定语言 (DSL) 的作者,你可以定义验证约束,以验证由用户创建的模型是否有意义。 例如,如果你的 DSL 允许用户绘制人员及其祖先的家族树,你可以编写一个约束,用于确保孩子的出生日期在其父母之后。

你可以在保存模型时、打开模型时以及用户显式运行“验证”菜单命令时执行验证约束。 还可以在程序控制下执行验证。 例如,你可以执行验证以响应属性值或关系中的某个更改。

如果你要编写可处理用户的模型的文本模板或其他工具,则验证尤其重要。 验证可确保模型满足由这些工具假定的前提条件。

警告

还可以允许验证约束以及扩展菜单命令和笔势处理程序一起定义在对 DSL 的单独扩展中。 除了 DSL,用户还可以选择安装这些扩展。 有关详细信息,请参阅使用 MEF 扩展 DSL

运行验证

当用户编辑模型时(即域特定语言的实例),以下操作可运行验证:

  • 右键单击关系图并选择“全部验证”。

  • 右键单击 DSL 的资源管理器中的顶层节点并选择“全部验证”

  • 保存模型。

  • 打开模型。

  • 此外,还可编写运行验证的程序代码,例如,作为菜单命令的一部分或响应更改。

    任何验证错误都将显示在“错误列表”窗口中。 用户可以双击错误消息,以选中引起错误的模型元素。

定义验证约束

通过将验证方法添加到 DSL 的域类或关系中定义验证约束。 当验证运行时,可由用户或在程序控制下执行一些或所有验证方法。 每个方法都将应用到其类的每个实例,并且在每个类中可以有多个验证方法。

每个验证方法都将报告它找到的所有错误。

注意

验证方法报告错误,但不更改模型。 如果希望调整或阻止某些更改,请参阅验证的替代方法

定义验证约束

  1. 在“编辑器\验证”节点中启用验证:

    1. 打开 Dsl\DslDefinition.dsl。

    2. 在 DSL 资源管理器中,展开“编辑器”节点,然后选择“验证” 。

    3. 在“属性”窗口中,将“Uses”属性设置为 true。 设置所有这些属性非常方便。

    4. 在“解决方案资源管理器”工具栏中,单击“转换所有模板” 。

  2. 为一个或多个域类或域关系编写分部类定义。 在 Dsl 项目的新代码文件中编写这些定义。

  3. 为每个类添加带有此特性的前缀:

    [ValidationState(ValidationState.Enabled)]
    
    • 默认情况下,此特性还将针对派生类启用验证。 如果想要针对特定派生类禁用验证,则可以使用 ValidationState.Disabled
  4. 将验证方法添加到类。 每个验证方法都可以具有任何名称,但要具有类型 ValidationContext 的一个参数。

    必须为它添加带有一个或多个 ValidationMethod 特性的前缀:

    [ValidationMethod (ValidationCategories.Open | ValidationCategories.Save | ValidationCategories.Menu ) ]
    

    ValidationCategories 指定何时执行该方法。

    例如:

using Microsoft.VisualStudio.Modeling;
using Microsoft.VisualStudio.Modeling.Validation;

// Allow validation methods in this class:
[ValidationState(ValidationState.Enabled)]
// In this DSL, ParentsHaveChildren is a domain relationship
// from Person to Person:
public partial class ParentsHaveChildren
{
  // Identify the method as a validation method:
  [ValidationMethod
  ( // Specify which events cause the method to be invoked:
    ValidationCategories.Open // On file load.
  | ValidationCategories.Save // On save to file.
  | ValidationCategories.Menu // On user menu command.
  )]
  // This method is applied to each instance of the
  // type (and its subtypes) in a model:
  private void ValidateParentBirth(ValidationContext context)
  {
    // In this DSL, the role names of this relationship
    // are "Child" and "Parent":
     if (this.Child.BirthYear < this.Parent.BirthYear
        // Allow user to leave the year unset:
        && this.Child.BirthYear != 0)
      {
        context.LogError(
             // Description:
                       "Child must be born after Parent",
             // Unique code for this error:
                       "FAB001ParentBirthError",
              // Objects to select when user double-clicks error:
                       this.Child,
                       this.Parent);
    }
  }

请注意有关此代码的以下几点:

  • 可以将验证方法添加到域类或域关系。 这些类型的代码位于 Dsl\Generated Code\Domain*.cs 中。

  • 每个验证方法都将应用到它的类和子类的每个实例。 对于域关系,每个实例都是两个模型元素之间的链接。

  • 验证方法不按任何指定的顺序进行应用,并且每个方法都不按任何可预知的顺序应用到它的类的实例。

  • 通常情况下,验证方法最好不要更新存储内容,因为这会导致不一致的结果。 相反,该方法应通过调用 context.LogErrorLogWarningLogInfo 报告任何错误。

  • 在 LogError 调用中,可以提供将在用户双击错误消息时选中的模型元素或关系链接的列表。

  • 有关如何在程序代码中读取模型的详细信息,请参阅在程序代码中导航和更新模型

    该示例将应用到以下域模型。 ParentsHaveChildren 关系具有称为 Child 和 Parent 的角色。

    DSL Definition diagram - family tree model

验证类别

ValidationMethodAttribute 特性中,指定应何时执行验证方法。

Category 执行
ValidationCategories 当用户调用验证菜单命令时。
ValidationCategories 当打开模型文件时。
ValidationCategories 当保存该文件时。 如果存在验证错误,则将向用户提供取消保存操作的选项。
ValidationCategories 当保存该文件时。 如果存在来自此类别中的方法的错误,则警告用户可能无法重新打开该文件。

将此类别用于测试重复名称或 ID 的验证方法,或者可能引起加载错误的其他条件。
ValidationCategories 当调用 ValidateCustom 方法时。 只能从程序代码中调用此类别中的验证。

有关详细信息,请参阅自定义验证类别

放置验证方法的位置

通过将验证方法放置在不同的类型上,通常可达到同样的效果。 例如,可以将方法添加到“Person”类,而不是“ParentsHaveChildren”关系,并使它循环访问链接:

[ValidationState(ValidationState.Enabled)]
public partial class Person
{[ValidationMethod
 ( ValidationCategories.Open
 | ValidationCategories.Save
 | ValidationCategories.Menu
 )
]
  private void ValidateParentBirth(ValidationContext context)
  {
    // Iterate through ParentHasChildren links:
    foreach (Person parent in this.Parents)
    {
        if (this.BirthYear <= parent.BirthYear)
        { ...

聚合验证约束。 若要按可预知的顺序应用验证,请在所有者类上定义单个验证方法,例如模型的根元素。 此技术还允许你将多个错误报告聚合到单个消息中。

缺点是组合的方法不易于管理,并且约束必须都具有相同的 ValidationCategories。 因此,建议你将每个约束保留在单独的方法中(如果可能)。

在上下文缓存中传递值。 上下文参数具有可以在其中放置任意值的字典。 该字典将在验证运行的生存期内持续存在。 例如,特定验证方法可以将错误计数保留在上下文中,并使用它来避免错误窗口被重复的消息所淹没。 例如:

List<ParentsHaveChildren> erroneousLinks;
if (!context.TryGetCacheValue("erroneousLinks", out erroneousLinks))
erroneousLinks = new List<ParentsHaveChildren>();
erroneousLinks.Add(this);
context.SetCacheValue("erroneousLinks", erroneousLinks);
if (erroneousLinks.Count < 5) { context.LogError( ... ); }

重数的验证

将为你的 DSL 自动生成用于检查最小重数的验证方法。 代码将写入 Dsl\Generated Code\MultiplicityValidation.cs。 当你在 DSL 资源管理器的“编辑器\验证”节点中启用验证时,这些方法将生效。

如果你将域关系的角色的重数设置为 1..* 或 1..1,但用户未创建此关系的链接,则将显示验证错误消息。

例如,如果 DSL 具有类“Person”和“Town”,并且关系“PersonLivesInTown”与关系“1..\*”一起位于“Town”角色上,则对于每个没有“Town”的“Person”,都将显示一条错误消息。

从程序代码运行验证

通过访问或创建 ValidationController,可运行验证。 如果想要在错误窗口中向用户显示错误,则使用附加到关系图的 DocData 的 ValidationController。 例如,如果你要编写菜单命令,则命令集类中提供了 CurrentDocData.ValidationController

using Microsoft.VisualStudio.Modeling;
using Microsoft.VisualStudio.Modeling.Validation;
using Microsoft.VisualStudio.Modeling.Shell;
...
partial class MyLanguageCommandSet
{
  private void OnMenuMyContextMenuCommand(object sender, EventArgs e)
  {
   ValidationController controller = this.CurrentDocData.ValidationController;
...

有关详细信息,请参阅如何:向快捷菜单添加命令

还可以创建单独的验证控制器,并自行管理错误。 例如:

using Microsoft.VisualStudio.Modeling;
using Microsoft.VisualStudio.Modeling.Validation;
using Microsoft.VisualStudio.Modeling.Shell;
...
Store store = ...;
VsValidationController validator = new VsValidationController(s);
// Validate all elements in the Store:
if (!validator.Validate(store, ValidationCategories.Save))
{
  // Deal with errors:
  foreach (ValidationMessage message in validator.ValidationMessages) { ... }
}

当发生更改时运行验证

如果你想要确保用户在该模型变为无效时立即收到警告,可以定义运行验证的存储事件。 有关存储事件的详细信息,请参阅事件处理程序在模型外部传播更改

除了验证代码外,还请使用与以下示例类似的内容将自定义代码文件添加到 DslPackage 项目。 此代码使用附加到文档的 ValidationController。 此控制器将在 Visual Studio 错误列表中显示验证错误。

using System;
using System.Linq;
using Microsoft.VisualStudio.Modeling;
using Microsoft.VisualStudio.Modeling.Validation;
namespace Company.FamilyTree
{
  partial class FamilyTreeDocData // Change name to your DocData.
  {
    // Register the store event handler:
    protected override void OnDocumentLoaded()
    {
      base.OnDocumentLoaded();
      DomainClassInfo observedLinkInfo = this.Store.DomainDataDirectory
         .FindDomainClass(typeof(ParentsHaveChildren));
      DomainClassInfo observedClassInfo = this.Store.DomainDataDirectory
         .FindDomainClass(typeof(Person));
      EventManagerDirectory events = this.Store.EventManagerDirectory;
      events.ElementAdded
         .Add(observedLinkInfo, new EventHandler<ElementAddedEventArgs>(ParentLinkAddedHandler));
      events.ElementDeleted.Add(observedLinkInfo, new EventHandler<ElementDeletedEventArgs>(ParentLinkDeletedHandler));
      events.ElementPropertyChanged.Add(observedClassInfo, new EventHandler<ElementPropertyChangedEventArgs>(BirthDateChangedHandler));
    }
    // Handler will be called after transaction that creates a link:
    private void ParentLinkAddedHandler(object sender,
                                ElementAddedEventArgs e)
    {
      this.ValidationController.Validate(e.ModelElement,
           ValidationCategories.Save);
    }
    // Called when a link is deleted:
    private void ParentLinkDeletedHandler(object sender,
                                ElementDeletedEventArgs e)
    {
      // Don't apply validation to a deleted item!
      // - Validate store to refresh the error list.
      this.ValidationController.Validate(this.Store,
           ValidationCategories.Save);
    }
    // Called when any property of a Person element changes:
    private void BirthDateChangedHandler(object sender,
                      ElementPropertyChangedEventArgs e)
    {
      Person person = e.ModelElement as Person;
      // Not interested in changes in other properties:
      if (e.DomainProperty.Id != Person.BirthYearDomainPropertyId)
          return;

      // Validate all parent links to and from the person:
      this.ValidationController.Validate(
        ParentsHaveChildren.GetLinksToParents(person)
        .Concat(ParentsHaveChildren.GetLinksToChildren(person))
        , ValidationCategories.Save);
    }
  }
}

在影响链接或元素的“撤消”或“重做”操作后,还将调用处理程序。

自定义验证类别

除了标准验证类别(如“菜单”和“打开”),还可以定义自己的类别。 可以从程序代码调用这些类别。 用户无法直接调用它们。

自定义类别通常用于定义测试模型是否满足特定工具的前提条件的类别。

若要将验证方法添加到特定类别,请为它添加带有如下特性的前缀:

[ValidationMethod(CustomCategory = "PreconditionsForGeneratePartsList")]
[ValidationMethod(ValidationCategory.Menu)]
private void TestForCircularLinks(ValidationContext context)
{...}

注意

你可以为方法添加带有任意数目的 [ValidationMethod()] 特性的前缀。 可以将方法同时添加到自定义类别和标准类别。

若要调用自定义验证,请执行以下操作:


// Invoke all validation methods in a custom category:
validationController.ValidateCustom
  (store, // or a list of model elements
   "PreconditionsForGeneratePartsList");

验证的替代方法

验证约束报告错误,但不更改模型。 相反,如果你想要防止模型变为无效,则可以使用其他技术。

但是,不建议使用这些技术。 通常,最好让用户决定如何更正无效的模型。

调整更改以还原模型有效性。 例如,如果用户将属性设置为允许的最大值之上,则可以将该属性重置为最大值。 若要实现此目的,请定义一个规则。 有关详细信息,请参阅规则在模型内部传播更改

如果尝试无效的更改,则回滚事务。 还可以定义用于此目的的规则,但在某些情况下,可以重写属性处理程序 OnValueChanging() 或重写 OnDeleted(). 等方法 若要回滚事务,请使用 this.Store.TransactionManager.CurrentTransaction.Rollback(). 有关详细信息,请参阅域属性值更改处理程序

警告

请确保用户知道更改已调整或已回滚。 例如,使用 System.Windows.Forms.MessageBox.Show("message").