特定领域语言中的验证

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

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

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

警告

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

运行验证

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

  • 右键单击图表,然后选择“全部验证”。

  • 右键单击 DSL 资源管理器中的顶部节点,然后选择“验证所有

  • 保存模型。

  • 打开模型。

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

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

定义验证约束

通过将验证方法添加到 DSL 的域类或关系来定义验证约束。 在验证运行时,无论是由用户执行还是在程序控制下执行,某些或全部验证方法都会被执行。 每个方法都应用于其类的每个实例,每个类中可以有多个验证方法。

每个验证方法都会报告它找到的任何错误。

注释

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

定义验证约束

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

    1. 打开 Dsl\DslDefinition.dsl

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

    3. 在“属性”窗口中,将 “使用 属性”设置为 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 定义图 - 家庭树模型

验证类别

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

类别 Execution
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 资源管理器的 Editor\Validation 节点中启用验证时,这些方法生效。

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

例如,如果你的 DSL 有类 Person 和 Town,并且有一个关系 PersonLivesInTown,在角色“城镇”上的关系为1..\*,那么对于每个没有“城镇”的 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;
...

有关详细信息,请参阅 How to: Add a Command to the Shortcut Menu.

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

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

撤消或重做操作后也会调用这些处理程序,这些操作会影响链接或元素。

自定义验证类别

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

自定义类别的典型用途是定义一个类别,用于测试模型是否满足特定工具的前置条件。

若要向特定类别添加验证方法,请使用如下所示的属性作为前缀:

[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").