DSL Tools #5 - Implementing Custom Validations

Implementing custom validations in the DSL Tools for a specific domain model is simple and (to some extent) documented.

On my sample model, I wanted to add the following validations:

  1. Ensure that a given domain property is specified.
  2. Ensure that some domain property value matches a specific pattern.
  3. Ensure that for each element on the model, there is a unique value for a given domain property.
  4. Ensure that specific domain classes are included in the model.
  5. Ensure that flow elements are well connected.

First, let me start by explaining how you can add a new validation to a domain model. It's very simple:

1. Create a partial for the domain class you want to add the validation

If you have a class called Contract that you want to add a new validation, you will need to add a new partial class called Contract:

public partial class Contract
{
    // ...
}

2. Add the new validation logic as a private method of this class

This method can have any name but should always have the following prototype:

private void MyValidation(ValidationContext context)

3. Decorate the partial class with a ValidationState attribute and the validation method with the ValidationMethod attribute

The ValidationState attribute (in the class) instructs the framework to activate validation for this domain class.

[ValidationState(ValidationState.Enabled)]
public partial class Contract

On the other hand, ValidationMethod identifies the method as a validation and indicates to the framework when this validation should be performed.

[ValidationMethod(ValidationCategories.Menu |
ValidationCategories.Open |
ValidationCategories.Save)] private void ValidateProperties(ValidationContext context)

4. Activate custom validation

Well, creating the custom class will not activate validation per se. You need to alter your domain model by changing some properties in the editor designer:

  1. Open your DSL definition file (*.dsl)
  2. Activate the DSL Explorer
  3. Browse to Editor and select the Validation (Validation) node
  4. Change the following properties to true: "Uses Menu", "Uses Open", and "Uses Save".

Note that these properties match to the values passed in the ValidationMethod attribute.

5. Write your own validation code in the validation method

The following code is one simple example:

using System;
using System.Collections;
using Microsoft.VisualStudio.Modeling.Validation;

namespace MyModel.Processes
{
    /// <summary>
    /// Provides methods to validate the Contract domain class.
    /// </summary>
    [ValidationState(ValidationState.Enabled)]
    public partial class Contract
    {
        /// <summary>
        /// Validates the Properties collection.
        /// </summary>
        /// <param name="context">Validation context.</param>
        [ValidationMethod(ValidationCategories.Menu |
ValidationCategories.Open |
ValidationCategories.Save)] private void ValidateProperties(ValidationContext context) { // Each property should have a unique name Hashtable propertyNames = new Hashtable(); foreach (ContractProperty property in this.Properties) { if (propertyNames.ContainsKey(property.Name)) { string error = GeneralHelper.FormatString(
Properties.Resources.PropertyAlreadyExists,
property.Name); context.LogError(error,
"ContracPropertyRepeated", property); } else { propertyNames.Add(property.Name, property.Name); } } } } }

You can in fact traverse the whole domain model from the validation method and implement all kind of different validations, being errors (context.LogError) or warnings (context.LogWarning). The following examples implement the 5 different kind of validations that I wanted for my model.

(1) Required Domain Property, and (2) Domain Property Value Pattern

using System;
using Microsoft.VisualStudio.Modeling.Validation;

namespace MyModel.Processes
{
    /// <summary>
    /// Provides methods to validate the NamedElement domain class.
    /// </summary>
    [ValidationState(ValidationState.Enabled)]
    public partial class NamedElement
    {
        /// <summary>
        /// Validates the Name property.
        /// </summary>
        /// <param name="context">Validation context.</param>
        [ValidationMethod(ValidationCategories.Menu |
ValidationCategories.Open |
ValidationCategories.Save)] private void ValidateName(ValidationContext context) { if (this.Name.Length == 0) { // Is it the diagram? string error; if ((this as ProcessGraph) != null) { error = Properties.Resources.ProcessNameMissing; } else if ((this as Contract) != null) { error = Properties.Resources.ContractNameMissing; } else { error = GeneralHelper.FormatString(
Properties.Resources.ElementNameNotDefined,
this.Id.ToString()); } context.LogError(error,
"NamedElementNameNotDefined", this); } else if (!GeneralHelper.IsMatch(this.Name,
"^[a-zA-Z0-9_]+$")) { string error = GeneralHelper.FormatString(
Properties.Resources.ElementNameInvalid,
this.Name); context.LogError(error,
"NamedElementInvalidName", this); } } } }

(3) Unique Domain Property Value

using System;
using System.Collections;
using System.Text;
using Microsoft.VisualStudio.Modeling.Validation;

namespace MyModel.Processes
{
    /// <summary>
    /// Provides methods to validate the ProcessGraph domain class.
    /// </summary>
    [ValidationState(ValidationState.Enabled)]
    public partial class ProcessGraph
    {
        /// <summary>
        /// Validates the names of all the elements in the graph.
        /// </summary>
        /// <param name="context">Validation context.</param>
        [ValidationMethod(ValidationCategories.Menu |
ValidationCategories.Open |
ValidationCategories.Save)] private void ValidateNames(ValidationContext context) { // Search repeated names in the elements collection Hashtable names = new Hashtable(); Hashtable repeated = new Hashtable(); foreach (FlowElement element in this.Elements) { if (element.Name.Length > 0) { // Already in the hashtable? if (names.ContainsKey(element.Name)) { repeated.Add(element.Name, element.Name); } else { // Add to the hashtable names.Add(element.Name, element.Name); } } } // At least one name repeated if (repeated.Count > 0) { StringBuilder sb = new StringBuilder(); foreach (string name in repeated.Values) { sb.Append(name + ", "); } string namesStr = sb.ToString(); namesStr = namesStr.Substring(0, namesStr.Length - 2); string error = GeneralHelper.FormatString(
Properties.Resources.RES_ElementsWithNamesRepeated,
namesStr); context.LogError(error,
"GraphRepeatedElementName", this); } } } }

(4) Missing Domain Classes

using System;
using System.Collections;
using System.Text;
using Microsoft.VisualStudio.Modeling.Validation;

namespace MyModel.Processes
{
    /// <summary>
    /// Provides methods to validate the ProcessGraph domain class.
    /// </summary>
    [ValidationState(ValidationState.Enabled)]
    public partial class ProcessGraph
    {
        /// <summary>
        /// Validates the elements defined in the graph.
        /// </summary>
        /// <param name="context">Validation context.</param>
        [ValidationMethod(ValidationCategories.Menu |
ValidationCategories.Open |
ValidationCategories.Save)] private void ValidateElements(ValidationContext context) { // Graph should have exactly one Start point if (CodeGenerationHelper.FindStartPoint(this) == null) { string error = Properties.Resources.StartPointMissing; context.LogError(error,
"GraphStartPointNotFound", this); } // Graph should have at least one End point if (CodeGenerationHelper.FindEndPoints(this).Count == 0) { string error = Properties.Resources.EndPointsMissing; context.LogError(error,
"GraphEndPointsNotFound", this); } } } }

(5) Flow Connections

using System;
using Microsoft.VisualStudio.Modeling.Validation;

namespace MyModel.Processes
{
    /// <summary>
    /// Provides methods to validate the FlowElement domain class.
    /// </summary>
    [ValidationState(ValidationState.Enabled)]
    public partial class FlowElement
    {
        /// <summary>
        /// Validates the flow between elements.
        /// </summary>
        /// <param name="context">Validation context.</param>
        [ValidationMethod(ValidationCategories.Menu |
ValidationCategories.Open |
ValidationCategories.Save)] private void ValidateFlows(ValidationContext context) { // Start points Start startElement = this as Start; if (startElement != null) { // Start should have at least one flow out if (startElement.FlowTo.Count == 0) { string error = GeneralHelper.FormatString(
Properties.Resources.StartOutFlowMissing,
startElement.Name); context.LogError(error,
"StartOutFlowMissing", this); } } // End points End endElement = this as End; if (endElement != null) { // End should have at least one flow in if (endElement.FlowFrom.Count == 0) { string error = GeneralHelper.FormatString(
Properties.Resources.EndInFlowMissing,
endElement.Name); context.LogError(error,
"EndInFlowMissing", this); } } // Activities ActivityElement activityElement = this as ActivityElement; if (activityElement != null) { // Activity should have at least one flow in if (activityElement.FlowFrom.Count == 0) { string error = GeneralHelper.FormatString(
Properties.Resources.ActivityInFlowMissing,
activityElement.Name); context.LogError(error,
"ActivityInFlowMissing", this); } // Activity should have at least one flow out if (activityElement.FlowTo.Count == 0) { string error = GeneralHelper.FormatString(
Properties.Resources.ActivityOutFlowMissing,
activityElement.Name); context.LogError(error,
"ActivityOutFlowMissing", this); } } // Gateways GatewayElement gatewayElement = this as GatewayElement; if (gatewayElement != null) { // Gateway should have at least one flow in if (gatewayElement.FlowFrom.Count == 0) { string error = GeneralHelper.FormatString(
Properties.Resources.GatewatInFlowMissing,
gatewayElement.Name); context.LogError(error,
"GatewayInFlowMissing", this); } // Gateway should have at least one flow out if (gatewayElement.FlowTo.Count == 0) { string error = GeneralHelper.FormatString(
Properties.Resources.GatewayOutFlowMissing,
gatewayElement.Name); context.LogError(error,
"GatewayOutFlowMissing", this); } } } } }
Published 16 August 07 10:39 by hgr
Filed under: ,

Comments

# Hugo Ribeiro said on August 16, 2007 10:40 AM:

Last weekend I found some time to mess with the Microsoft DSL Tools to learn how this thing really works.

# Hugo Ribeiro said on July 15, 2008 6:43 PM:

Since I've been doing a number of posts on the DSL Tools, I thought it would be helpful to have a list

Anonymous comments are disabled