共用方式為


領域專屬語言驗證

身為網域特定語言 (DSL) 的作者,您可以定義驗證限制,以驗證使用者建立的模型是否有意義。 例如,如果您的 DSL 允許使用者繪製人員及其祖先的家譜,您可以撰寫條件約束,確保孩子的出生日期在其父母之後。

您可以讓在儲存模型、開啟模型時,以及使用者明確執行「 驗證」 功能表指令時執行驗證限制。 您也可以在程式控制下執行驗證。 例如,您可以執行驗證以回應屬性值或關聯性的變更。

如果您正在撰寫文字範本或其他處理使用者模型的工具,驗證尤其重要。 驗證可確保模型滿足這些工具所假設的先決條件。

警告

您也可以允許在 DSL 的個別延伸模組中定義驗證條件約束,以及延伸功能表命令和手勢處理常式。 除了您的 DSL 之外,使用者還可以選擇安裝這些擴充功能。 如需詳細資訊,請參閱 使用 MEF 擴充 DSL

執行驗證

當使用者編輯模型 (也就是網域特定語言的執行個體) 時,下列動作可以執行驗證:

  • 以滑鼠右鍵按一下圖表,並選擇 全部驗證。

  • 以滑鼠右鍵按一下 DSL 檔案總管中的頂端節點,然後選取 [全部驗證]

  • 儲存模型。

  • 開啟模型。

  • 此外,您可以撰寫執行驗證的程式代碼,例如,作為功能表指令的一部分,或回應變更。

    任何驗證錯誤都會出現在 [錯誤清單] 視窗中。 使用者可以按兩下錯誤訊息,以選取導致錯誤的模型元素。

定義驗證條件約束

您可以將驗證方法新增至 DSL 的網域類別或關聯性,以定義驗證限制。 當驗證由使用者或程式控制下執行時,會執行部分或所有驗證方法。 每個方法都會套用至其類別的每個實例,且每個類別中可以有數個驗證方法。

每個驗證方法都會報告它找到的任何錯誤。

備註

驗證方法會報告錯誤,但不會變更模型。 如果您想要調整或防止某些變更,請參閱 驗證的替代方案

定義驗證約束

  1. Editor\Validation 節點中啟用驗證:

    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 定義圖 - 家譜模型

驗證類別

在屬性中 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。 因此,建議您盡可能將每個條件約束保留在個別的方法中。

在上下文快取中傳遞值。 context 參數有一個字典,您可以在其中放置任意值。 字典會在驗證執行的生命週期內持續存在。 例如,特定的驗證方法可以在上下文中維護錯誤計數,以避免錯誤視窗充斥著重複的訊息。 例如:

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" 的關係,且 "Town" 角色具有關係1..\*,則針對每個沒有 "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").