如何:为 UML 模型定义验证约束

在 Visual Studio 旗舰版中,可以定义用于测试模型是否满足指定条件的验证约束。 例如,您可以定义一个约束来确保用户不创建继承关系的循环。 当用户尝试打开或保存模型时将会调用该约束,也可以手动调用该约束。 如果约束失败,则您定义的错误消息将添加到错误窗口中。 可以将这些约束打包到 Visual Studio 集成扩展 (VSIX) 中,并将其分发给其他 Visual Studio 旗舰版用户。

还可以定义根据外部资源(如数据库)验证模型的约束。

提示

若要对照层关系图验证程序代码,请参见向层关系图添加自定义体系结构验证

要求

应用验证约束

在三种情况下应用验证约束:在保存模型时;在打开模型时;在**“体系结构”菜单上单击“验证 UML 模型”**时。 对于每一种情况,只能应用已为该情况定义的那些约束,尽管通常会将每种约束定义为可在多种情况下应用。

Visual Studio 错误窗口中会报告验证错误,可以双击错误以选择出错的模型元素。

有关应用验证的更多信息,请参见如何:验证 UML 模型

定义验证扩展

若要为 UML 设计器创建验证扩展,您必须创建一个定义验证约束的类,并将此类嵌入 Visual Studio 集成扩展 (VSIX) 中。 VSIX 将用作可安装约束的容器。 有两种用于定义验证扩展的替代方法:

  • **使用项目模板在验证扩展的 VSIX 中创建它。**这是一种快速方法。 如果您不想将验证约束与其他类型的扩展(如菜单命令、自定义工具箱项或笔势处理程序)合并,则使用此方法。 可以在一个类中定义多个约束。

  • **创建单独的验证类和 VSIX 项目。**如果您想将多个类型的扩展并入同一个 VSIX 中,则请使用此方法。 例如,如果您的菜单命令需要模型遵守特定约束,则可以将菜单命令嵌入到验证方法所在的 VSIX 中。

在验证扩展自己的 VSIX 中创建它

  1. 在**“新建项目”对话框中的“建模项目”下,选择“验证扩展”**。

  2. 在新项目中打开 .cs 文件,并修改该类以实现验证约束。

    有关更多信息,请参见实现验证约束。

    重要说明重要事项

    确保 .cs 文件包含下列 using 语句:

    using Microsoft.VisualStudio.ArchitectureTools.Extensibility.Uml;

  3. 可以通过定义新的方法来添加其他约束。 若要将一个方法标识为验证方法,则必须使用特性按照标记初始验证方法的方式来标记该方法。

  4. 按 F5 测试约束。 有关更多信息,请参见执行验证。

  5. 通过复制由您的项目生成的 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;

在类库项目中创建单独的验证约束

  1. 创建一个类库项目(通过在现有 VSIX 解决方案中添加一个类库项目或创建一个新的解决方案)。

    1. 在**“文件”菜单上指向“新建”,然后单击“项目”**。

    2. 在**“已安装的模板”下,单击“Visual C#”“Visual Basic”,然后在中间列中单击“类库”**。

    3. 设置**“解决方案”**以指示您是希望创建新的解决方案,还是希望向已打开的 VSIX 解决方案添加组件。

    4. 设置项目的名称和位置,然后单击“确定”。

  2. 除非解决方案中已有一个 VSIX 项目,否则请创建一个 VSIX 项目。

    1. 在**“解决方案资源管理器”中,右击该解决方案,指向“添加”,然后单击“新建项目”**。

    2. 在**“已安装的模板”下,展开“Visual C#”“Visual Basic”,然后单击“扩展性”。 在中间列中,单击“VSIX 项目”**。

  3. 将 VSIX 项目设置为解决方案的启动项目。

    • 在解决方案资源管理器中,右击该 VSIX 项目,然后单击**“设为启动项目”**。
  4. source.extension.vsixmanifest 中的**“内容”**下,将类库项目添加为 MEF 组件。

    1. 打开 source.extension.vsixmanifest

    2. 单击**“添加内容”**。

    3. 在**“选择内容类型”处,选择“MEF 组件”**。

    4. 在**“选择源”处,单击“项目”**,并选择类库项目的名称。

  5. 单击**“选择版本”**,并选择要运行扩展的 Visual Studio 版本。

  6. 设置 VSIX 的名称字段和描述性字段。 保存该文件。

定义验证类

  1. 如果您已使用其自己的 VSIX 从验证项目模板创建验证类,则无需此过程。

  2. 在验证类项目中,添加对以下 .NET 程序集的引用:

    Microsoft.VisualStudio.Modeling.Sdk.10.0

    Microsoft.VisualStudio.ArchitectureTools.Extensibility.Uml

    Microsoft.VisualStudio.Uml.Interfaces

    System.ComponentModel.Composition

  3. 将一个文件添加到类库项目中,其中包含与以下示例类似的代码。

    • 每个验证约束均包含在用特定特性标记的方法中。 该方法接受模型元素类型的参数。 调用验证时,验证框架将对符合其参数类型的每个模型元素应用每种验证方法。

    • 您可以将这些方法置于任何类和命名空间中。 将它们变成您的首选项。

    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.
      }
    }
    

执行验证约束

仅为测试目的在调试模式下执行验证方法。

测试验证约束

  1. F5,或在**“调试”菜单上单击“开始调试”**。

    这将启动 Visual Studio 的实验实例。

    疑难解答:如果新的 Visual Studio 未启动:

    • 如果您有多个项目,请确保将 VSIX 项目设置为解决方案的启动项目。

    • 在解决方案资源管理器中,右击启动项目,然后单击“属性”。 在项目属性编辑器中,单击**“调试”选项卡。 确保“启动外部程序”**字段中的字符串是 Visual Studio 的完整路径名,通常为:

      C:\Program Files\Microsoft Visual Studio 10.0\Common7\IDE\devenv.exe

  2. 在实验性 Visual Studio 中,打开或创建一个建模项目,然后打开或创建一个建模图。

  3. 为上一节中给定的示例约束设置测试:

    1. 打开类关系图。

    2. 创建一个类,并添加具有同一名称的两个特性。

  4. 右击关系图上的任意位置,再单击**“验证”**。

  5. 错误窗口中将报告模型中的任何错误。

  6. 双击该错误报告。 如果报告中提及的元素出现在屏幕上,则会突出显示。

    疑难解答:如果**“验证”**命令没有出现在菜单中,请确保:

    • 该验证项目作为一个 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 扩展。

安装扩展

  1. 在您的计算机中查找由 VSIX 项目生成的 .vsix 文件。

    1. 在**“解决方案资源管理器”中,右击 VSIX 项目,然后单击“在 Windows 资源管理器中打开文件夹”**。

    2. 找到文件 bin\*\您的项目.vsix

  2. .vsix 文件复制到要安装扩展的目标计算机上。 该计算机可为您自己的计算机或其他计算机。

    • 目标计算机必须装有 source.extension.vsixmanifest 中指定的 Visual Studio 版本之一。
  3. 在目标计算机上,双击 .vsix 文件。

    **“Visual Studio Extension Installer”**将会打开并安装扩展。

  4. 启动或重新启动 Visual Studio。

卸载扩展

  1. 在**“工具”菜单上,单击“扩展管理器”**。

  2. 展开**“已安装的扩展”**。

  3. 选择扩展,然后单击**“卸载”**。

在极少数情况下,有错误的扩展无法加载并在错误窗口中创建报告,但不显示在扩展管理器中。 在此情况下,可以通过从以下位置删除文件来删除扩展,其中 %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;
}

请参见

概念

使用 UML API 编程

其他资源

如何:定义和安装建模扩展