作为域特定语言(DSL)的作者,可以定义验证约束以验证用户创建的模型是否有意义。 例如,如果你的 DSL 允许用户绘制人及其祖先的家庭树,则可以编写一个约束,确保孩子在父母之后有出生日期。
可以在保存模型、打开模型时以及用户显式运行 “验证 ”菜单命令时执行验证约束。 还可以在程序控制下执行验证。 例如,可以执行验证以响应属性值或关系中的更改。
如果要编写处理用户模型的文本模板或其他工具,验证尤其重要。 验证可确保模型满足这些工具假定的前提条件。
警告
还可以允许在 DSL 的单独扩展中定义验证约束,以及扩展菜单命令和手势处理程序。 除了 DSL 之外,用户可以选择安装这些扩展。 有关详细信息,请参阅 使用 MEF 扩展 DSL。
运行验证
当用户正在编辑模型(即你的领域特定语言的一个实例)时,以下操作可以运行验证:
右键单击图表,然后选择“全部验证”。
右键单击 DSL 资源管理器中的顶部节点,然后选择“验证所有”
保存模型。
打开模型。
此外,还可以编写运行验证的程序代码,例如,作为菜单命令的一部分或响应更改。
任何验证错误都会显示在 “错误列表 ”窗口中。 用户可以双击错误消息以选择导致错误的模型元素。
定义验证约束
通过将验证方法添加到 DSL 的域类或关系来定义验证约束。 在验证运行时,无论是由用户执行还是在程序控制下执行,某些或全部验证方法都会被执行。 每个方法都应用于其类的每个实例,每个类中可以有多个验证方法。
每个验证方法都会报告它找到的任何错误。
注释
验证方法报告错误,但不更改模型。 如果要调整或阻止某些更改,请参阅 “验证替代项”。
定义验证约束
在 编辑器\验证 节点中启用验证:
打开 Dsl\DslDefinition.dsl。
在 DSL 资源管理器中,展开 编辑器 节点并选择 “验证”。
在“属性”窗口中,将 “使用 属性”设置为
true。 设置所有这些属性最方便。单击“转换所有模板”在解决方案资源管理器工具栏中。
为一个或多个域类或域关系编写分部类定义。 在 Dsl 项目中的新代码文件中编写这些定义。
为每个类添加此属性的前缀:
[ValidationState(ValidationState.Enabled)]- 默认情况下,此属性还将为派生类启用验证。 如果要禁用特定派生类的验证,可以使用
ValidationState.Disabled。
- 默认情况下,此属性还将为派生类启用验证。 如果要禁用特定派生类的验证,可以使用
向类添加验证方法。 每个验证方法可以具有任何名称,但具有一个类型 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.LogErrorLogWarning或LogInfo报告任何错误。在 LogError 调用中,可以提供在用户双击错误消息时选择的模型元素或关系链接列表。
有关如何在程序代码中读取模型的信息,请参阅 在程序代码中导航和更新模型。
该示例适用于以下域模型。 ParentsHaveChildren 关系中的角色分别被命名为 Child 和 Parent。
验证类别
在属性中 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").