如何:为 UML 模型定义验证约束
在 Visual Studio 旗舰版中,可以定义用于测试模型是否满足指定条件的验证约束。 例如,您可以定义一个约束来确保用户不创建继承关系的循环。 当用户尝试打开或保存模型时将会调用该约束,也可以手动调用该约束。 如果约束失败,则您定义的错误消息将添加到错误窗口中。 可以将这些约束打包到 Visual Studio 集成扩展 (VSIX) 中,并将其分发给其他 Visual Studio 旗舰版用户。
还可以定义根据外部资源(如数据库)验证模型的约束。
提示
若要对照层关系图验证程序代码,请参见向层关系图添加自定义体系结构验证。
要求
Visual Studio 2010 Visualization and Modeling SDK(Visual Studio 2010 可视化和建模 SDK)。 有关更多信息,请参见代码库中的 Visual Studio Visualization and Modeling SDK(Visual Studio 可视化和建模 SDK)。
应用验证约束
在三种情况下应用验证约束:在保存模型时;在打开模型时;在**“体系结构”菜单上单击“验证 UML 模型”**时。 对于每一种情况,只能应用已为该情况定义的那些约束,尽管通常会将每种约束定义为可在多种情况下应用。
Visual Studio 错误窗口中会报告验证错误,可以双击错误以选择出错的模型元素。
有关应用验证的更多信息,请参见如何:验证 UML 模型。
定义验证扩展
若要为 UML 设计器创建验证扩展,您必须创建一个定义验证约束的类,并将此类嵌入 Visual Studio 集成扩展 (VSIX) 中。 VSIX 将用作可安装约束的容器。 有两种用于定义验证扩展的替代方法:
**使用项目模板在验证扩展的 VSIX 中创建它。**这是一种快速方法。 如果您不想将验证约束与其他类型的扩展(如菜单命令、自定义工具箱项或笔势处理程序)合并,则使用此方法。 可以在一个类中定义多个约束。
**创建单独的验证类和 VSIX 项目。**如果您想将多个类型的扩展并入同一个 VSIX 中,则请使用此方法。 例如,如果您的菜单命令需要模型遵守特定约束,则可以将菜单命令嵌入到验证方法所在的 VSIX 中。
在验证扩展自己的 VSIX 中创建它
在**“新建项目”对话框中的“建模项目”下,选择“验证扩展”**。
在新项目中打开 .cs 文件,并修改该类以实现验证约束。
有关更多信息,请参见实现验证约束。
重要事项 确保 .cs 文件包含下列 using 语句:
using Microsoft.VisualStudio.ArchitectureTools.Extensibility.Uml;
可以通过定义新的方法来添加其他约束。 若要将一个方法标识为验证方法,则必须使用特性按照标记初始验证方法的方式来标记该方法。
按 F5 测试约束。 有关更多信息,请参见执行验证。
通过复制由您的项目生成的 bin\*\*.vsix 文件,在其他计算机上安装菜单命令。 有关更多信息,请参见安装验证约束。
在添加其他 .cs 文件时,通常需要下列 using 语句:
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using Microsoft.VisualStudio.ArchitectureTools.Extensibility.Uml;
using Microsoft.VisualStudio.Modeling.Validation;
using Microsoft.VisualStudio.Uml.Classes;
在类库项目中创建单独的验证约束
创建一个类库项目(通过在现有 VSIX 解决方案中添加一个类库项目或创建一个新的解决方案)。
在**“文件”菜单上指向“新建”,然后单击“项目”**。
在**“已安装的模板”下,单击“Visual C#”或“Visual Basic”,然后在中间列中单击“类库”**。
设置**“解决方案”**以指示您是希望创建新的解决方案,还是希望向已打开的 VSIX 解决方案添加组件。
设置项目的名称和位置,然后单击“确定”。
除非解决方案中已有一个 VSIX 项目,否则请创建一个 VSIX 项目。
在**“解决方案资源管理器”中,右击该解决方案,指向“添加”,然后单击“新建项目”**。
在**“已安装的模板”下,展开“Visual C#”或“Visual Basic”,然后单击“扩展性”。 在中间列中,单击“VSIX 项目”**。
将 VSIX 项目设置为解决方案的启动项目。
- 在解决方案资源管理器中,右击该 VSIX 项目,然后单击**“设为启动项目”**。
在 source.extension.vsixmanifest 中的**“内容”**下,将类库项目添加为 MEF 组件。
打开 source.extension.vsixmanifest
单击**“添加内容”**。
在**“选择内容类型”处,选择“MEF 组件”**。
在**“选择源”处,单击“项目”**,并选择类库项目的名称。
单击**“选择版本”**,并选择要运行扩展的 Visual Studio 版本。
设置 VSIX 的名称字段和描述性字段。 保存该文件。
定义验证类
如果您已使用其自己的 VSIX 从验证项目模板创建验证类,则无需此过程。
在验证类项目中,添加对以下 .NET 程序集的引用:
Microsoft.VisualStudio.Modeling.Sdk.10.0
Microsoft.VisualStudio.ArchitectureTools.Extensibility.Uml
Microsoft.VisualStudio.Uml.Interfaces
System.ComponentModel.Composition
将一个文件添加到类库项目中,其中包含与以下示例类似的代码。
每个验证约束均包含在用特定特性标记的方法中。 该方法接受模型元素类型的参数。 调用验证时,验证框架将对符合其参数类型的每个模型元素应用每种验证方法。
您可以将这些方法置于任何类和命名空间中。 将它们变成您的首选项。
using System.Collections.Generic; using System.ComponentModel.Composition; using System.Linq; using Microsoft.VisualStudio.Modeling.Validation; using Microsoft.VisualStudio.ArchitectureTools.Extensibility.Uml; using Microsoft.VisualStudio.Uml.Classes; // You might also need the other Microsoft.VisualStudio.Uml namespaces. namespace Validation { public class MyValidationExtensions { // SAMPLE VALIDATION METHOD. // All validation methods have the following attributes. [Export(typeof(System.Action<ValidationContext, object>))] [ValidationMethod( ValidationCategories.Save | ValidationCategories.Open | ValidationCategories.Menu)] public void ValidateClassNames (ValidationContext context, // This type determines what elements // will be validated by this method: IClass elementToValidate) { // A validation method should not change the model. List<string> attributeNames = new List<string>(); foreach (IProperty attribute in elementToValidate.OwnedAttributes) { string name = attribute.Name; if (!string.IsNullOrEmpty(name) && attributeNames.Contains(name)) { context.LogError( string.Format("Duplicate attribute name '{0}' in class {1}", name, elementToValidate.Name), "001", elementToValidate); } attributeNames.Add(name); } } // Add more validation methods for different element types. } }
执行验证约束
仅为测试目的在调试模式下执行验证方法。
测试验证约束
按 F5,或在**“调试”菜单上单击“开始调试”**。
这将启动 Visual Studio 的实验实例。
疑难解答:如果新的 Visual Studio 未启动:
如果您有多个项目,请确保将 VSIX 项目设置为解决方案的启动项目。
在解决方案资源管理器中,右击启动项目,然后单击“属性”。 在项目属性编辑器中,单击**“调试”选项卡。 确保“启动外部程序”**字段中的字符串是 Visual Studio 的完整路径名,通常为:
C:\Program Files\Microsoft Visual Studio 10.0\Common7\IDE\devenv.exe
在实验性 Visual Studio 中,打开或创建一个建模项目,然后打开或创建一个建模图。
为上一节中给定的示例约束设置测试:
打开类关系图。
创建一个类,并添加具有同一名称的两个特性。
右击关系图上的任意位置,再单击**“验证”**。
错误窗口中将报告模型中的任何错误。
双击该错误报告。 如果报告中提及的元素出现在屏幕上,则会突出显示。
疑难解答:如果**“验证”**命令没有出现在菜单中,请确保:
该验证项目作为一个 MEF 组件在 VSIX 项目的 source.extensions.manifest 中的**“内容”**列表中列出。
正确的 Export 和 ValidationMethod 特性会附加到验证方法。
ValidationCategories.Menu 包含在 ValidationMethod 特性的参数中,并且它是由使用逻辑“或”(|) 的其他值构成的。
所有 Import 和 Export 特性的参数都有效。
评估约束
验证方法应确定需要应用的验证约束为 true 还是 false。 如果为 true,则不应执行任何操作。 如果为 false,则应使用 ValidationContext 参数提供的方法报告错误。
提示
验证方法不应更改模型。 无法保证将在何时按何顺序执行约束。 运行某个验证时,如果必须在验证方法的连续执行之间传递信息,则可使用协调多个验证中所述的上下文缓存。
例如,如果需要确保每个类型(类、接口或枚举器)具有的名称的长度至少为 3 个字符,则可以使用此方法:
public void ValidateTypeName(ValidationContext context, IType type)
{
if (!string.IsNullOrEmpty(type.Name) && type.Name.Length < 3)
{
context.LogError(
string.Format("Type name {0} is too short", type.Name),
"001", type);
}
}
有关可用于导航和读取模型的方法和类型的信息,请参见使用 UML API 编程。
关于验证约束方法
每个验证约束都由采用以下格式的方法定义:
[Export(typeof(System.Action<ValidationContext, object>))]
[ValidationMethod(ValidationCategories.Save
| ValidationCategories.Menu
| ValidationCategories.Open)]
public void ValidateSomething
(ValidationContext context, IClassifier elementToValidate)
{...}
每个验证方法的特性和参数如下所示:
[Export(typeof(System.Action <ValidationContext, object>))] |
使用托管扩展性框架 (MEF) 将该方法定义为验证约束。 |
[ValidationMethod (ValidationCategories.Menu)] |
指定何时将执行验证。 如果要组合多个选项,请使用按位“或”(|)。 Menu = 由“验证”菜单调用。 Save = 在保存模型时调用。 Open = 在打开模型时调用。 Load = 在保存模型时调用,但若出现冲突,则警告用户可能无法重新打开模型。 也在加载时调用,但会在分析模型之前。 |
public void ValidateSomething (ValidationContext context, IElement element) |
将第二个参数 IElement 替换为要将约束应用于的元素类型。 将对指定类型的所有元素调用约束方法。 方法的名称并不重要。 |
可以在第二个参数中使用不同类型来定义所需数量的验证方法。 调用验证时,将对每个符合参数类型的模型元素调用每种验证方法。
报告验证错误
若要创建一个错误报告,请使用 ValidationContext 提供的方法:
context.LogError("error string", errorCode, elementsWithError);
"error string" 显示在 Visual Studio 错误列表中
errorCode 是一个字符串,它应为错误的唯一标识符
elementsWithError 标识模型中的元素。 当用户双击错误报告时,表示此元素的形状将被选中。
LogError(), LogWarning() 和 LogMessage() 将消息放入错误列表的不同部分。
如何应用验证方法
对模型中的每个元素应用验证,包括更大元素的关系和各个部分,例如类的特性和操作的参数。
对符合其第二个参数中的类型的每个元素应用每种验证方法。 这意味着,如果使用 IUseCase 的第二个参数定义一种验证方法,并使用其超类型 IElement 定义另一种验证方法,则将对模型中的每个用例应用这两种方法。
模型元素类型中汇总了类型的层次结构。
也可以按下列关系访问元素。 例如,如果您要对 IClass 定义验证方法,则可以遍历其拥有的属性:
public void ValidateTypeName(ValidationContext context, IClass c)
{
foreach (IProperty property in c.OwnedAttributes)
{
if (property.Name.Length < 3)
{
context.LogError(
string.Format(
"Property name {0} is too short",
property.Name),
"001", property);
}
}
}
在模型上创建验证方法
如果您需要确保验证方法在每个验证运行期间刚好被调用一次,则可以验证 IModel:
using Microsoft.VisualStudio.Uml.AuxiliaryConstructs; ...
[Export(typeof(System.Action<ValidationContext, object>))]
[ValidationMethod(ValidationCategories.Menu)]
public void ValidateModel(ValidationContext context, IModel model)
{ foreach (IElement element in model.OwnedElements)
{ ...
验证形状和关系图
由于验证方法的主要用途是验证模型,因此不会对显示元素(如关系图和形状)调用验证方法。 不过,您可以使用关系图上下文来访问当前关系图。
在您的验证类内,将 DiagramContext 声明为导入的属性:
using Microsoft.VisualStudio.ArchitectureTools.Extensibility.Presentation;
...
[Import]
public IDiagramContext DiagramContext { get; set; }
在验证方法中,可以使用 DiagramContext 访问当前焦点关系图(如果有):
[Export(typeof(System.Action<ValidationContext, object>))]
[ValidationMethod(ValidationCategories.Menu)]
public void ValidateModel(ValidationContext context, IModel model)
{
IDiagram focusDiagram = DiagramContext.CurrentDiagram;
if (focusDiagram != null)
{
foreach (IShape<IUseCase> useCaseShape in
focusDiagram.GetChildShapes<IUseCase>())
{ ...
若要记录错误,您必须获取形状表示的模型元素,因为无法将形状传递给 LogError:
IUseCase useCase = useCaseShape.Element;
context.LogError(... , usecase);
协调多个验证
调用验证时(例如用户从关系图菜单调用),会对每个模型元素应用每种验证方法。 这意味着在验证框架的单次调用中,可能会对不同元素应用同一方法很多次。
这会给处理元素间关系的验证带来问题。 例如,您可能会编写一个验证,该验证从一个用例开始,然后遍历 include 关系来验证不存在循环。 但是,如果该方法应用于具有很多 include 链接的模型中的每个用例时,很有可能会重复处理模型的相同区域。
为了避免这种情况,提供了一个用于在验证运行期间在其中保存信息的上下文缓存。 可使用该缓存在验证方法的不同执行之间传递信息。 例如,您可以存储此次验证运行中已处理的元素列表。 该缓存在每次验证运行开始时创建,不能用于在不同的验证运行之间传递信息。
context.SetCacheValue<T> (name, value) |
存储值 |
context.TryGetCacheValue<T> (name, out value) |
获取值。 如果成功,则返回 true。 |
context.GetValue<T>(name) |
获取值。 |
Context.GetValue<T>() |
获取指定类型的值。 |
安装和卸载扩展
既可以在您自己的计算机上也可以在其他计算机上安装 Visual Studio 扩展。
安装扩展
在您的计算机中查找由 VSIX 项目生成的 .vsix 文件。
在**“解决方案资源管理器”中,右击 VSIX 项目,然后单击“在 Windows 资源管理器中打开文件夹”**。
找到文件 bin\*\您的项目.vsix
将 .vsix 文件复制到要安装扩展的目标计算机上。 该计算机可为您自己的计算机或其他计算机。
- 目标计算机必须装有 source.extension.vsixmanifest 中指定的 Visual Studio 版本之一。
在目标计算机上,双击 .vsix 文件。
**“Visual Studio Extension Installer”**将会打开并安装扩展。
启动或重新启动 Visual Studio。
卸载扩展
在**“工具”菜单上,单击“扩展管理器”**。
展开**“已安装的扩展”**。
选择扩展,然后单击**“卸载”**。
在极少数情况下,有错误的扩展无法加载并在错误窗口中创建报告,但不显示在扩展管理器中。 在此情况下,可以通过从以下位置删除文件来删除扩展,其中 %LocalAppData% 通常为 DriveName:\Users\UserName\AppData\Local:
%LocalAppData%\Microsoft\VisualStudio\10.0\Extensions
示例
此示例在元素间的依赖关系中查找循环。
它将对保存命令和验证菜单命令都进行验证。
/// <summary>
/// Verify that there are no loops in the dependency relationsips.
/// In our project, no element should be a dependent of itself.
/// </summary>
/// <param name="context">Validation context for logs.</param>
/// <param name="element">Element to start validation from.</param>
[Export(typeof(System.Action<ValidationContext, object>))]
[ValidationMethod(ValidationCategories.Menu
| ValidationCategories.Save | ValidationCategories.Open)]
public void NoDependencyLoops(ValidationContext context, INamedElement element)
{
// The validation framework will call this method
// for every element in the model. But when we follow
// the dependencies from one element, we will validate others.
// So we keep a list of the elements that we don't need to validate again.
// The list is kept in the context cache so that it is passed
// from one execution of this method to another.
List<INamedElement> alreadySeen = null;
if (!context.TryGetCacheValue("No dependency loops", out alreadySeen))
{
alreadySeen = new List<INamedElement>();
context.SetCacheValue("No dependency loops", alreadySeen);
}
NoDependencyLoops(context, element,
new INamedElement[0], alreadySeen);
}
/// <summary>
/// Log an error if there is any loop in the dependency relationship.
/// </summary>
/// <param name="context">Validation context for logs.</param>
/// <param name="element">The element to be validated.</param>
/// <param name="dependants">Elements we've followed in this recursion.</param>
/// <param name="alreadySeen">Elements that have already been validated.</param>
/// <returns>true if no error was detected</returns>
private bool NoDependencyLoops(ValidationContext context,
INamedElement element, INamedElement[] dependants,
List<INamedElement> alreadySeen)
{
if (dependants.Contains(element))
{
context.LogError(string.Format("{0} should not depend on itself", element.Name),
"Fabrikam.UML.NoGenLoops", // unique code for this error
dependants.SkipWhile(e => e != element).ToArray());
// highlight elements that are in the loop
return false;
}
INamedElement[] dependantsPlusElement =
new INamedElement[dependants.Length + 1];
dependants.CopyTo(dependantsPlusElement, 0);
dependantsPlusElement[dependantsPlusElement.Length - 1] = element;
if (alreadySeen.Contains(element))
{
// We have already validated this when we started
// from another element during this validation run.
return true;
}
alreadySeen.Add(element);
foreach (INamedElement supplier in element.GetDependencySuppliers())
{
if (!NoDependencyLoops(context, supplier,
dependantsPlusElement, alreadySeen))
return false;
}
return true;
}