Popular Categories
Latest replies
« How to Change Namespace of .NET Assembly? | Main | Exception Handling in Windows Azure »
Thursday
Jan292009

DDD and Rule driven UI Validation in .NET

Recently I've introduced a first C# implementation sample to the Validation and Business Rules Application Block in response to question raised by Colin Jack.

Let's talk about it.

If you prefer to have solution open, while reading through the article, then just check out Samples\RulesUI in the latest version of Shared Libraries package.

Overview

  • This sample starts with modeling domain objects and defining some business rules to validate them. This is done only once, in a strongly-typed and rather simple manner.
  • Then we design our view that specializes in user-friendly validation of the input, using the rules it was provided with.
  • Finally, we write some controller code that uses a view to ask user for input that passes some predefined rules.

While doing this we will also:

  • get UI that is completely detached from any validation specifics (using things like RegexValidator to enforce business rules is simply wrong);
  • have all bindings between rules, objects and UI controls to be strongly-typed.

The UI, that we are aiming at, looks like this: Rules UI

Check out, how the UI-level validation rules change, when we select my home-country in the drop-down and try to save:

UI-level validation rules change

The provided sample attempts to demonstrate, how simple it is to achieve this. Let's start from the beginning and work up to the implementation.

Model

That's how our domain will look like:

  • Customer

    • Name
    • Email
    • Address

      • Country
      • Street1
      • Street2
      • Zip

C# class implementation is as simple as this.

Then we will define some generic address rules. First address rule is a static one. It just checks for some country-specific address requirements:

static class AddressIs
{
  public static void Valid(Address address, IScope scope)
  {
    scope.Validate(() => address.Street1, StringIs.Limited(10, 256));
    scope.Validate(() => address.Street2, StringIs.Limited(256));
    scope.Validate(() => address.Country, Is.NotDefault);
    scope.Validate(() => address.Zip, StringIs.Limited(10));

    switch (address.Country)
    {
      case Country.USA:
        scope.Validate(() => address.Zip, StringIs.Limited(5, 10));
        break;
      case Country.France:
        break;
      case Country.Russia:
        scope.Validate(() => address.Zip, StringIs.Limited(6, 6));
        break;
      default:
        scope.Validate(() => address.Zip, StringIs.Limited(1, 64));
        break;
    }
  }

Second address rule has slightly more complex syntax, since it is dynamic and uses lambdas and closures to avoid writing extra classes and methods. The rule simply ensures that the address belongs to the selected country:

  public static Rule<Address> In(Country country)
  {
    return (address, scope) => 
      scope.Validate(() => address.Country, (c, s) =>
      {
        if (c != country)
          s.Error("We are working only in {0} now.", country);
      });       
  }
}

And then we also define rules in C# for validating our customers. First rule will use country information to check for different contact rules:

public static class CustomerIs
{
  public static void Valid(Customer customer, IScope scope)
  {
    scope.Validate(() => customer.Name, StringIs.Limited(3, 64));

    if (customer.Address.Country == Country.Russia)
    {
      scope.Validate(() => customer.Email, (s, scope1) =>
      {
        if (!string.IsNullOrEmpty(s))
          scope1.Error("Russian customers do not have emails. "
                       +"Use pigeons instead.");
      });
    }
    else
    {
      scope.Validate(() => customer.Email, StringIs.ValidEmail);
    }
  }

Second customer rule simply allows to apply any address rules to customer in a fluent manner:

  // dynamic rule
  public static Rule<Customer> From(params Rule<Address>[] addressRules)
  {
    return (customer, scope) => 
      scope.Validate(() => customer.Address, addressRules);
  }
}

These rules already give us a simplified form of fluent API to express our requirements. For example, we can write readable composite rules and use them in enforce statements:

Enforce.That(customer, 
  CustomerIs.Valid, 
  CustomerIs.From(AddressIs.Valid));

or:

Enforce.That(customer,
  CustomerIs.Valid, 
  CustomerIs.From(AddressIs.Valid, AddressIs.In(Country.Russia)));

Nice thing about these rules is that they completely hide the implementation details and leave developers with the DSL in C#.

And these rules already contain all the domain-related information that we need in order to have UI that validates input, according to rules passed to it.

Let's implement the remaining pieces.

View

CustomerView is a .NET Windows Form that implements a simple interface:

public interface IEditorView<T> where T : class
{
  Result<T> GetData(params Rule<T>[] rules);
  void BindData(T customer);
  void SetTitle(string text);
}

T is Customer, in our situation.

Implementations of BindData and SetTitle are trivial:

public void BindData(Customer customer)
{
  _name.Text = customer.Name;
  _email.Text = customer.Email;

  // logically this could be moved to a nested address control
  _street1.Text = customer.Address.Street1;
  _street2.Text = customer.Address.Street2;
  _country.SelectedItem = customer.Address.Country;
  _zip.Text = customer.Address.Zip;      
}

GetData() is slightly more complex. Here's the C# pieces for it:

public Result<Customer> GetData(params Rule<Customer>[] rules)
{
  _rules = rules;

  // we pretend to ask workspace (retrieved from IoC)
  // for the current presentation framework (windows.forms)
  // to display us
  if (DialogResult.OK == ShowDialog())
  {
    return Result.Success(GetData());
  }

  return Result<Customer>.Error("User declined");
}

The call to ShowDialog() will return only after user cancels editing or after he manages to enter the data that complies with the current rules.

We'll be simple here and will run the validation when the user has done editing and clicks on the OK button:

private void _ok_Click(object sender, EventArgs e)
{
  var customer = GetData();
  if (_validator.RunRules(customer, _rules) == RuleLevel.None)
  {
    DialogResult = DialogResult.OK;
    Close();
  }
}

Customer GetData()
{
  var address = new Address(_street1.Text, _street2.Text,
    _zip.Text, EnumUtil.Parse<Country>(_country.Text));
  return new Customer(_name.Text, address, _email.Text);
}

In the snippet above we instantiate new Customer from the UI and then run it against validator. Validator is a reusable class that knows about bindings between properties of the domain objects and controls. While running the rules, it uses this knowledge to associate any errors to the editor that logically "owns" them.

That's how we instantiate this validator in the default constructor of the CustomerView:

InitializeComponent();

_validator = new Validator<Customer>(_name, errorProvider1);

_validator
  .Bind(_name, i => i.Name)
  .Bind(_email, i => i.Email)
  .Bind(_street1, i => i.Address.Street1)
  .Bind(_street2, i => i.Address.Street2)
  .Bind(_country, i => i.Address.Country)
  .Bind(_zip, i => i.Address.Zip);

As you can see, all bindings are strongly-typed here.

Controller

That's how our controller code in this .NET MVC scenario might look like:

// normally we get this through the IoC
IEditorView<Customer> view = new CustomerView();

// ask user for a new customer that complies with some rules
view.SetTitle("New customer");
var result = view.GetData(
  CustomerIs.Valid,
  CustomerIs.From(AddressIs.Valid));

if (!result.IsSuccess)
{
  MessageBox.Show("Exiting. Reason: " + result.ErrorMessage);
  return;
}


MessageBox.Show("Let's edit customer now with more strict rule");

var customer = result.Value;

// display customer in the view and ask user to make it
// comply with the rule set that is more strict
view.SetTitle("Editing " + customer.Name);
view.BindData(customer);

result = view.GetData(
  CustomerIs.Valid,
  CustomerIs.From(AddressIs.Valid, AddressIs.In(Country.Russia)));

if (result.IsSuccess)
{
  MessageBox.Show("Congratulations for getting through rule-driven sample");
}

In this snippet we ask user to create a customer that passes some validation checks. Rules are self-explanatory:

CustomerIs.Valid,
CustomerIs.From(AddressIs.Valid)

Then we present user with his entry, while coming up with more strict set of validation rules. Additional rule speaks for itself:

CustomerIs.Valid,
CustomerIs.From(AddressIs.Valid, AddressIs.In(Country.Russia))

Details

  • In this scenario I've gone functional (that's the side effect of prolonged exposure to F#) and keep my domain objects immutable. This is possible, since we do not use any classical data-binding. It simplifies the development quite a bit.
  • CustomerView that we've created, does not contain any business logic (except for the knowledge about the object). And it is so decoupled from the Controller, so we might simply swap CustomerView for a view implemented in a completely different presentation framework (WPF, Silverlight or DXperience) and still have working application.
  • This MVC implementation is rather simplified (we are concentrating on rules, after all): the controller code is actually placed in the Program.cs and we just pretend to have IoC container and UI composition framework around.
  • If we want to push the concept of DDD even further, we can use Domain Specific Languages (i.e. implement with Boo) to define our rules in a form that's even more readable by humans. But so far it feels like pushing everything a bit over the edge and introducing unnecessary complexity (which will result in increased development friction). C# rule definitions are rather readable on their own.

What next?

I'd be really interested to hear what you think.

References (2)

References allow you to track sources for this article, as well as articles that were written in response to this article.

Reader Comments (2)

Any chance that you could post the sample code?

Cheers

April 8, 2009 | Unregistered CommenterKane

Kane,

this sample could be located in Samples.zip from the "Merged binaries" download of Lokad Shared Libraries. It is also available in the source control repository of this project.

Rinat

April 9, 2009 | Registered CommenterRinat Abdullin
Comments for this entry have been disabled. Additional comments may not be added to this entry at this time.