共用方式為


網域指定的語言中的驗證

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

您可以在儲存模型時、開啟模型時,以及使用者明確執行 [驗證] 功能表命令時,執行驗證條件約束。 您也可以在程式控制下執行驗證。 例如,您可以執行驗證,以回應屬性值或關聯性的變更。

如果您要撰寫文字範本或其他工具來處理使用者的模型,驗證便特別重要。 驗證可確保模型滿足這些工具假設的前置條件。

警告

除了擴充功能的功能表命令和軌跡處理常式之外,您也可以將驗證條件約束定義為 DSL 的個別擴充功能。 當使用者安裝 DSL 時,可以另外選擇安裝這些擴充功能。 如需詳細資訊,請參閱使用 MEF 擴充您的 DSL

執行驗證

當使用者編輯模型 (亦即網域指定的語言執行個體) 時,下列動作會執行驗證:

  • 以滑鼠右鍵按一下圖表並選取 [全部驗證]

  • 以滑鼠右鍵按一下 [DSL 總管] 中的最上層節點,並選取 [全部驗證]

  • 儲存模型。

  • 開啟模型。

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

    任何驗證錯誤都會顯示在 [錯誤清單] 視窗中。 使用者可以按兩下錯誤訊息,選取造成錯誤的模型項目。

定義驗證條件約束

您可以將驗證方法加入至 DSL 的網域類別或關聯性,藉此定義驗證條件約束。 當使用者執行驗證或在程式控制下執行驗證時,即會執行部分或所有驗證方法。 每個方法都會套用至其類別的每個執行個體,並且每個類別中可以有數個驗證方法。

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

注意

驗證方法會報告錯誤,但不會變更模型。 如果您要調整或避免特定變更,請參閱驗證的替代方法

定義驗證條件約束

  1. 編輯器\驗證節點中啟用驗證:

    1. 開啟 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 Definition diagram - family tree model

驗證分類

ValidationMethodAttribute 屬性中,您可以指定何時應執行驗證方法。

類別 執行
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 總管] 的 [編輯器\驗證] 節點中啟用驗證時生效。

如果將網域關聯性角色的多重性設定為 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").