Compartilhar via


Validação em um idioma Domain-Specific

Como autor de uma DSL (linguagem específica do domínio), você pode definir restrições de validação para verificar se o modelo criado pelo usuário é significativo. Por exemplo, se a DSL permitir que os usuários desenhem uma árvore familiar de pessoas e seus ancestrais, você poderá escrever uma restrição que garanta que as crianças tenham datas de nascimento após seus pais.

Você pode executar as restrições de validação quando o modelo é salvo, quando ele é aberto e quando o usuário executa explicitamente o comando de menu Validar . Você também pode executar a validação no controle do programa. Por exemplo, você pode executar a validação em resposta a uma alteração em um valor de propriedade ou relação.

A validação é particularmente importante se você estiver escrevendo modelos de texto ou outras ferramentas que processam os modelos dos usuários. A validação garante que os modelos atendam às pré-condições assumidas por essas ferramentas.

Aviso

Você também pode permitir que restrições de validação sejam definidas em extensões separadas para sua DSL, juntamente com comandos de menu de extensão e manipuladores de gestos. Os usuários podem optar por instalar essas extensões além da DSL. Para obter mais informações, consulte Estender sua DSL usando MEF.

Validação em execução

Quando um usuário está editando um modelo, ou seja, uma instância do idioma específico do domínio, as seguintes ações podem executar a validação:

  • Clique com o botão direito do mouse no diagrama e selecione Validar Tudo.

  • Clique com o botão direito do mouse no nó superior no Explorador da sua DSL e selecione Validar Tudo

  • Salve o modelo.

  • Abra o modelo.

  • Além disso, você pode escrever o código do programa que executa a validação, por exemplo, como parte de um comando de menu ou em resposta a uma alteração.

    Todos os erros de validação serão exibidos na janela Lista de Erros . O usuário pode clicar duas vezes em uma mensagem de erro para selecionar os elementos de modelo que são a causa do erro.

Definindo restrições de validação

Você define restrições de validação adicionando métodos de validação às classes de domínio ou relações de sua DSL. Quando a validação é executada, pelo usuário ou sob controle de programa, alguns ou todos os métodos de validação são executados. Cada método é aplicado a cada instância de sua classe e pode haver vários métodos de validação em cada classe.

Cada método de validação relata todos os erros encontrados.

Observação

Os métodos de validação relatam erros, mas não alteram o modelo. Se você quiser ajustar ou impedir determinadas alterações, consulte Alternativas à Validação.

Para definir uma restrição de validação

  1. Habilitar a validação no nó Editor\Validação :

    1. Abra Dsl\DslDefinition.dsl.

    2. No Explorador de DSL, expanda o nó Editor e selecione Validação.

    3. Na janela Propriedades, defina as propriedades Uses como true. É mais conveniente definir todas essas propriedades.

    4. Clique em Transformar Todos os Modelos na barra de ferramentas do Gerenciador de Soluções .

  2. Escreva definições parciais de classe para uma ou mais de suas classes de domínio ou relações de domínio. Escreva essas definições em um novo arquivo de código no projeto Dsl .

  3. Prefixe cada classe com este atributo:

    [ValidationState(ValidationState.Enabled)]
    
    • Por padrão, esse atributo também habilitará a validação para classes derivadas. Se você quiser desabilitar a validação para uma classe derivada específica, poderá usar ValidationState.Disabled.
  4. Adicione métodos de validação às classes. Cada método de validação pode ter qualquer nome, mas tem um parâmetro do tipo ValidationContext.

    Ele deve ser prefixado com um ou mais ValidationMethod atributos:

    [ValidationMethod (ValidationCategories.Open | ValidationCategories.Save | ValidationCategories.Menu ) ]
    

    As ValidationCategories especificam quando o método é executado.

    Por exemplo:

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);
    }
  }

Observe os seguintes pontos sobre este código:

  • Você pode adicionar métodos de validação a classes de domínio ou relações de domínio. O código para esses tipos está em Dsl\Generated Code\Domain*.cs.

  • Cada método de validação é aplicado a cada instância de sua classe e suas subclasses. No caso de uma relação de domínio, cada instância é um vínculo entre dois elementos de modelo.

  • Os métodos de validação não são aplicados em nenhuma ordem especificada e cada método não é aplicado às instâncias de sua classe em nenhuma ordem previsível.

  • Geralmente, é uma má prática para um método de validação atualizar o conteúdo do repositório, pois isso levaria a resultados inconsistentes. Em vez disso, o método deve relatar qualquer erro chamando context.LogError, LogWarning ou LogInfo.

  • Na chamada do LogError, você pode fornecer uma lista de elementos de modelo ou links de relação que serão selecionados quando o usuário clicar duas vezes na mensagem de erro.

  • Para obter informações sobre como ler o modelo no código do programa, consulte Navegando e atualizando um modelo no código do programa.

    O exemplo se aplica ao modelo de domínio a seguir. A relação ParentsHaveChildren tem papéis chamados Filho e Pai.

    Diagrama de definição de DSL – modelo de árvore genealógica

Categorias de validação

ValidationMethodAttribute No atributo, você especifica quando o método de validação deve ser executado.

Categoria Execution
ValidationCategories Quando o usuário invoca o comando de menu Validar.
ValidationCategories Quando o arquivo de modelo é aberto.
ValidationCategories Quando o arquivo é salvo. Se houver erros de validação, o usuário receberá a opção de cancelar a operação de salvamento.
ValidationCategories Quando o arquivo é salvo. Se houver erros de métodos nessa categoria, o usuário será avisado de que talvez não seja possível reabrir o arquivo.

Use essa categoria para métodos de validação que testam nomes ou IDs duplicados ou outras condições que possam causar erros de carregamento.
ValidationCategories Quando o método ValidateCustom é chamado. As validações nessa categoria só podem ser invocadas do código do programa.

Para obter mais informações, consulte Categorias de Validação Personalizada.

Onde colocar métodos de validação

Geralmente, você pode obter o mesmo efeito colocando um método de validação em um tipo diferente. Por exemplo, você pode adicionar um método à classe Person em vez da relação ParentsHaveChildren e iterá-lo por meio dos links:

[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)
        { ...

Agregando restrições de validação. Para aplicar a validação em uma ordem previsível, defina um único método de validação em uma classe de proprietário, como o elemento raiz do modelo. Essa técnica também permite agregar vários relatórios de erro em uma única mensagem.

As desvantagens são que o método combinado é menos fácil de gerenciar e que todas as restrições devem ter o mesmo ValidationCategories. Portanto, recomendamos que você mantenha cada restrição em um método separado, se possível.

Passando valores no cache de contexto. O parâmetro de contexto tem um dicionário no qual você pode colocar valores arbitrários. O dicionário persiste durante toda a duração da execução da validação. Um método de validação específico poderia, por exemplo, manter uma contagem de erros no contexto e usá-lo para evitar inundar a janela de erros com mensagens repetidas. Por exemplo:

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( ... ); }

Validação de multiplicidades

Os métodos de validação para verificar a multiplicidade mínima são gerados automaticamente para sua DSL. O código é gravado em Dsl\Generated Code\MultiplicityValidation.cs. Esses métodos têm efeito quando você habilita a validação no nó Editor\Validação no Gerenciador de DSL.

Se você definir a multiplicidade de uma função de uma relação de domínio como 1..* ou 1..1, mas o usuário não criar um link dessa relação, uma mensagem de erro de validação será exibida.

Por exemplo, se sua DSL tiver classes Person e Town e uma relação PersonLivesInTown com uma relação 1..\* na função Cidade, em seguida, para cada Pessoa que não tem Cidade, uma mensagem de erro será exibida.

Executando a validação do código do programa

Você pode executar a validação acessando ou criando um ValidationController. Se você quiser que os erros sejam exibidos ao usuário na janela de erro, use o ValidationController anexado ao DocData do diagrama. Por exemplo, se você estiver escrevendo um comando de menu, CurrentDocData.ValidationController estará disponível na classe de conjunto de comandos:

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

Para obter mais informações, consulte Como adicionar um comando ao menu de atalho.

Você também pode criar um controlador de validação separado e gerenciar os erros por conta própria. Por exemplo:

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

Executando a validação quando ocorre uma alteração

Se você quiser garantir que o usuário seja avisado imediatamente se o modelo se tornar inválido, você poderá definir um evento de repositório que executa a validação. Para obter mais informações sobre eventos de repositório, consulte Os Manipuladores de Eventos propagam alterações fora do modelo.

Além do código de validação, adicione um arquivo de código personalizado ao seu projeto DslPackage , com conteúdo semelhante ao exemplo a seguir. Esse código usa o ValidationController que está anexado ao documento. Esse controlador exibe os erros de validação na lista de erros do 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);
    }
  }
}

Os manipuladores também são chamados após as operações de desfazer ou de refazer que afetam os links ou elementos.

Categorias de validação personalizadas

Além das categorias de validação padrão, como Menu e Abrir, você pode definir suas próprias categorias. Você pode invocar essas categorias do código do programa. O usuário não pode invocá-los diretamente.

Um uso típico para categorias personalizadas é definir uma categoria que testa se o modelo satisfaz as pré-condições de uma ferramenta específica.

Para adicionar um método de validação a uma categoria específica, prefixe-o com um atributo como este:

[ValidationMethod(CustomCategory = "PreconditionsForGeneratePartsList")]
[ValidationMethod(ValidationCategory.Menu)]
private void TestForCircularLinks(ValidationContext context)
{...}

Observação

Você pode prefixar um método com quantos [ValidationMethod()] atributos desejar. Você pode adicionar um método a categorias personalizadas e padrão.

Para invocar uma validação personalizada:


// Invoke all validation methods in a custom category:
validationController.ValidateCustom
  (store, // or a list of model elements
   "PreconditionsForGeneratePartsList");

Alternativas à validação

As restrições de validação relatam erros, mas não alteram o modelo. Se, em vez disso, você quiser impedir que o modelo se torne inválido, poderá usar outras técnicas.

No entanto, essas técnicas não são recomendadas. Geralmente, é melhor permitir que o usuário decida como corrigir um modelo inválido.

Ajuste a alteração para restaurar o modelo para a validade. Por exemplo, se o usuário definir uma propriedade acima do máximo permitido, você poderá redefinir a propriedade para o valor máximo. Para fazer isso, defina uma regra. Para obter mais informações, consulte Regras propagar alterações dentro do modelo.

Reverta a transação se uma alteração inválida for realizada. Você também pode definir uma regra para essa finalidade, mas em alguns casos é possível substituir um manipulador de propriedades OnValueChanging(), ou substituir um método como OnDeleted(). Reverter uma transação, use this.Store.TransactionManager.CurrentTransaction.Rollback(). Para obter mais informações, consulte Manipuladores de Alteração de Valor de Propriedade de Domínio.

Aviso

Verifique se o usuário sabe que a alteração foi ajustada ou revertida. Por exemplo, use System.Windows.Forms.MessageBox.Show("message").