When I wrote my Extended MVP Pattern article back in November, it got some criticism. Specifically:
Each 'thing' in the model should include it's own business rules, validation rules, security, and persistance [sic] rules.
I argued that I've seen times with having all of the validation (and all other logic) in the domain layer doesn't pan out. However, what I didn't talk about in detail was that my extended MVP pattern actually satisfies the desire to put the validation logic into the domain layer.
When I started creating my pattern, having the validation defined by the domain entities was actually part of the ultimate goal. But I strongly wanted to be able to override the domain's rules where necessary. This proved to be an excellent way to meet the standard requirements as well as the exceptional ones.
The project at hand was using LLBLGen to generate the domain entities and the data access layer. LLBLGen offers some very powerful access to the data model definition, via the domain entities. When I started creating my MVP pattern, I knew that I'd end up being able to leverage LLBLGen's information to drive validation, but I had no idea how to make it happen.
Some of the information that LLBLGen makes available follows:
- DataType
- IsForeignKey
- IsNullable
- IsPrimaryLey
- IsReadOnly
- MaxLength
- Name
- Precision
- Scale
I showed in my original post how I could define a view interface like this:
1: public ICustomerEditView
2: {
3: [Validation.Required]
4: ITextBox txtFirstName;
5:
6: ITextBox txtMiddleName;
7:
8: [Validation.Required]
9: ITextBox txtLastName;
10:
11: ITextBox txtAddress;
12: ITextBox txtCity;
13: IComboBox cboState;
14: ITextBox txtZipCode;
15: ICheckBox chkOnMailingList;
16: }
Having this in place, I wanted to replace the [Validation.Required] tags with something that tells the validation engine to look up the details of the First Name field from the Customer entity and determine whether or not it should be required. While this proved to be a little trickier than I had hoped, it came together quite nicely, using a recursive factory pattern.
I ended up with a view definition that looked like this:
1: public ICustomerEditView
2: {
3: [Validation.DataModelValidatorFactory(EntityType.CustomerEntity, CustomerFieldIndex.FirstName)]
4: ITextBox txtFirstName;
5:
6: [Validation.DataModelValidatorFactory(EntityType.CustomerEntity, CustomerFieldIndex.MiddleName)]
7: ITextBox txtMiddleName;
8:
9: [Validation.DataModelValidatorFactory(EntityType.CustomerEntity, CustomerFieldIndex.LastName)]
10: ITextBox txtLastName;
11:
12: [Validation.DataModelValidatorFactory(EntityType.CustomerEntity, CustomerFieldIndex.Address)]
13: ITextBox txtAddress;
14:
15: [Validation.DataModelValidatorFactory(EntityType.CustomerEntity, CustomerFieldIndex.City)]
16: ITextBox txtCity;
17:
18: [Validation.DataModelValidatorFactory(EntityType.CustomerEntity, CustomerFieldIndex.State)]
19: IComboBox cboState;
20:
21: [Validation.DataModelValidatorFactory(EntityType.CustomerEntity, CustomerFieldIndex.ZipCode)]
22: ITextBox txtZipCode;
23:
24: [Validation.DataModelValidatorFactory(EntityType.CustomerEntity, CustomerFieldIndex.IsOnMailingList)]
25: ICheckBox chkOnMailingList;
26: }
27:
Each field on the view is mapped to a field on the Customer entity through the DataModelValidatorFactoryAttribute. When the validation engine processes the validation attributes assigned to the interface fields, it recursively processes any factory attributes that are encountered, including this one.
Several interfaces and abstract classes were used along the way to wire all of this up. Here's the definition of the IFieldValidatorFactory interface:
1: /// <summary>
2: /// Interface for defining a validator factory. Extends IValidatorBase.
3: /// </summary>
4: public interface IFieldValidatorFactory : IFieldValidatorBase
5: {
6: /// <summary>
7: /// Create and return the validators that result from this validator factory
8: /// </summary>
9: /// <returns>
10: /// An array of IValidatorBase objects, which can be either an IFieldValidator or an IFieldValidatorFactory
11: /// </returns>
12: IFieldValidatorBase[] CreateValidators();
13: }
CreateValidators() returns an IFieldValidatorBase[] array, which can contain a combination of FieldValidatorAttribute objects and FieldValidatorFactoryAttribute objects. This allows a factory to return a list of validators that need to be applied and/or more factories. We never saw the recursive factory return in our implementation, but I anticipated how it could be used.
Here's how our DataModelValidatorFactoryAttribute implemented the CreateValidators method:
1: /// <summary>
2: /// Create the validators that apply to this data model field
3: /// </summary>
4: /// <returns></returns>
5: public override IFieldValidatorBase[] CreateValidators()
6: {
7: List<IFieldValidatorBase> validators = new List<IFieldValidatorBase>();
8:
9: // Add a data type validator for validated types
10: if (DataTypeAttribute.IsTypeRecognized(_field.DataType))
11: validators.Add(new DataTypeAttribute(_field.DataType));
12:
13: // If the field is not nullable, it is required
14: if (!_field.IsNullable)
15: validators.Add(new RequiredAttribute());
16:
17: // Only use the max length validator if we have a max length to validate
18: // LLBLGen reports a 0 length for numeric fields, so this is a safe assumption
19: if (_field.MaxLength > 0)
20: validators.Add(new MaxLengthAttribute(_field.MaxLength));
21:
22: return validators.ToArray();
23: }
Here's what this gives us, from a single attribute applied to a field on the interface:
- Data Type validation -- the UI validates that the value entered is of the correct type. For an integer field, if the user enters text, they get an error.
- Required Field validation -- the UI validates that any field that is not nullable in the domain model is required on the screen.
- Max Length enforcement -- the UI automatically enforces and validates maximum lengths for text fields.
We're not utilizing the Precision and Scale, but we had planned to. We had to cut a little scope at the end of the project, and that was a feature that was dropped. We had planned to create some sort of a validator for decimal value entries, ensuring the correct number of digits before and after the decimal point.
The validation engine is completely decoupled from the user interface (or user experience as we called it), but using dependency injection techniques, the UI is capable of being extremely rich. In fact, I had stated that the projects using this MVP pattern had the richest UI of any applications I've built. Here's a screen grab for our simple customer edit form. In this example, the cursor was hovering over the "First Name" label (not the textbox, but the label).
For all fields on all forms, we are able to show information to the user about what the validation rules are. We show this up-front and without consuming any valuable real estate or cluttering up the UI. The customer loved this.
We also show the user error messages when validation rules are broken. As soon as we hit Save, we get this (the cursor is hovering over the error provider next to the First Name field):
Again, the actual validation engine is completely decoupled with the UI. So, the same functionality could be implemented in a Web application as well as in a Windows Forms application as shown here.
In the end, my Extended MVP Pattern gave me everything I wanted it to give me. The Model defines the standard validation rules; the View interface implements those validation rules and the Presenter enforces them. But then our UI architecture can add lots of UI richness to the application to inform the user of what the validation rules are before they enter their data, and show them errors when rules are violated. And to top it off, the error message shown is directly based on the field's related label and not a hard-coded field name. While there were many components involved in getting this all set up, the DataModelValidatorFactory tied it all together very nicely.
There's some significant code in place to do all of the work, but it's actually very clean and isolated. In fact, I was able to extract the MVP Validation framework from the live project in less than 1 hour. I did that to demo the framework to coworkers during our internal users group meeting. The framework later plugged into 2 other projects very nicely too.
While I think this was some of the best work I've ever done, I still admit that it's no silver bullet. I've broken the mold for Model-View-Presenter by defining fields as specific control types, but that hasn't proven to be a disadvantage yet--it's actually kept things very tidy. And the richness offered in the UI is going to be tough to top.