Print

Integrating Validation Application Block with ASP.NET part 2

This post describes how to take integration of Validation Application Block with ASP.NET Web Forms to the next level by introducing extension methods that centralize the creation of PropertyProxyValidator controls and enable compile time support. This post build on the code in the previous article and allows users to define value conversions.

The code I presented in my previous post took integration between the Enterprise Library Validation Application Block and ASP.NET WebForms to the next level. There is one short come to this solution that you will soon notice; the PropertyProxyValidator throws an exception when trying to validate anything else than plain text. This is a problem the designers of VAB tried to solve. What they came up with was the ValueConvert event on the PropertyProxyValidator. When hooked to the PropertyProxyValidator, the validator will route conversion of values to that event.

While this mechanism works fine, it has the same problem as I tried to solve in my previous article; It takes a lot of code to hook up, as you can see in this example.

I like to have an API were I can easily supply a conversion method and optionally supply an error message that should be displayed when the conversion fails. To be able to do this in a compile time friendly way, we need to alter the old API. The old API is used like this:

this.LastNameTextBox.AddValidatorFor<Person>(p => p.LastName);

Now we want to allow a convertion method to be specified like this:

this.LastNameTextBox
.AddValidatorFor<Person>(p => p.Age, Int32.Parse);

This however, will not work, because of limitations of the C# compiler. We would have to rewrite the previous example as follows:

this.LastNameTextBox
.AddValidatorFor<Person, int>(p => p.Age, Int32.Parse);

While this isn't bad, I don't like that I have to specify the int, because the compiler should be able to infer it. We will have to come up with another design that allows this type to be inferred. Here is an example of something that will work:

this.LastNameTextBox.For<Person>()
.AddValidator(p => p.Age, Int32.Parse);

Let's cut to the chase. Here is the new implementation:

public static class AspNetValidationIntegration
{
public static AspNetValidationIntegrator<TEntity>
For<TEntity>(this Control controlToValidate)
{
return new AspNetValidationIntegrator<TEntity>(
controlToValidate);
}
}

public sealed class AspNetValidationIntegrator<TEntity>
{
private readonly Control control;

public AspNetValidationIntegrator(Control control)
{
this.control = control;
}

public BaseValidator AddValidator<TValue>(
Expression<Func<TEntity, TValue>> propertySelector)
{
return this.AddValidator(propertySelector,
string.Empty, null, null);
}

public BaseValidator AddValidator<TValue>(
Expression<Func<TEntity, TValue>> propertySelector,
Func<string, TValue> converter)
{
return this.AddValidator(propertySelector,
string.Empty, converter, null);
}

public BaseValidator AddValidator<TValue>(
Expression<Func<TEntity, TValue>> propertySelector,
Func<string, TValue> converter,
string convertionErrorMessage)
{
return this.AddValidator(propertySelector,
string.Empty, converter, convertionErrorMessage);
}

public BaseValidator AddValidator<TValue>(
Expression<Func<TEntity, TValue>> propertySelector,
string rulesetName)
{
return this.AddValidator(propertySelector,
rulesetName, null, null);
}

public BaseValidator AddValidator<TValue>(
Expression<Func<TEntity, TValue>> propertySelector,
string rulesetName, Func<string, TValue> converter)
{
return this.AddValidator(propertySelector,
rulesetName, converter, null);
}

public BaseValidator AddValidator<TValue>(
Expression<Func<TEntity, TValue>> propertySelector,
string rulesetName, Func<string, TValue> converter,
string convertionErrorMessage)
{
var validator = this.CreateValidator(propertySelector,
rulesetName, converter, convertionErrorMessage);

this.AddValidatorToPageJustAfterControl(validator);

return validator;
}

public BaseValidator CreateValidator<TValue>(
Expression<Func<TEntity, TValue>> propertySelector)
{
return this.CreateValidator(propertySelector,
string.Empty, null, null);
}

public BaseValidator CreateValidator<TValue>(
Expression<Func<TEntity, TValue>> propertySelector,
Func<string, TValue> converter)
{
return this.CreateValidator(propertySelector,
string.Empty, converter, null);
}

public BaseValidator CreateValidator<TValue>(
Expression<Func<TEntity, TValue>> propertySelector,
Func<string, TValue> converter,
string convertionErrorMessage)
{
return this.CreateValidator(propertySelector,
string.Empty, converter, convertionErrorMessage);
}

public BaseValidator CreateValidator<TValue>(
Expression<Func<TEntity, TValue>> propertySelector,
string rulesetName)
{
return this.CreateValidator(propertySelector,
rulesetName, null, null);
}

public BaseValidator CreateValidator<TValue>(
Expression<Func<TEntity, TValue>> propertySelector,
string rulesetName, Func<string, TValue> converter,
string convertionErrorMessage)
{
PropertyProxyValidator proxy =
CreateNewProxyValidator();

proxy.ControlToValidate = this.control.ID;

BindProxyValidatorToDomainProperty(proxy,
propertySelector, rulesetName);

AddConverter(proxy, converter, convertionErrorMessage);

return proxy;
}

private static void AddConverter<TValue>(
PropertyProxyValidator proxy,
Func<string, TValue> converter, string errorMessage)
{
if (converter != null)
{
proxy.ValueConvert += (sender, e) =>
{
string value = e.ValueToConvert as string;
try
{
e.ConvertedValue = converter(value);
}
catch (Exception ex)
{
if (string.IsNullOrEmpty(errorMessage))
{
e.ConversionErrorMessage = ex.Message;
}
else
{
e.ConversionErrorMessage = errorMessage;
}

e.ConvertedValue = null;
}
};
}
}

private static void BindProxyValidatorToDomainProperty<TValue>(
PropertyProxyValidator proxy,
Expression<Func<TEntity, TValue>> propertySelector,
string rulesetName)
{
proxy.PropertyName = GetPropertyName(propertySelector);
proxy.SourceTypeName = typeof(TEntity).AssemblyQualifiedName;
proxy.RulesetName = rulesetName;
}

private void AddValidatorToPageJustAfterControl(
BaseValidator validator)
{
int index =
this.control.Parent.Controls.IndexOf(this.control);

try
{
this.control.Parent.Controls.AddAt(index + 1, validator);
}
catch (HttpException ex)
{
throw BuildMoreExpressiveException(control, ex);
}
}

private static string GetPropertyName(
LambdaExpression propertySelector)
{
var body = propertySelector.Body;

var member = body as MemberExpression ??
((MemberExpression)((UnaryExpression)body).Operand);

return member.Member.Name;
}

private static PropertyProxyValidator CreateNewProxyValidator()
{
var proxy = new PropertyProxyValidator()
{
Display = ValidatorDisplay.Static,
};

// Let's make the proxy more fancy :-)
proxy.Text = "<img src='status_failed_small.gif' />";
proxy.PreRender += (s, e) =>
{
proxy.Attributes["title"] =
ReformatErrorMessageForTitle(proxy.ErrorMessage);
};

return proxy;
}

private static string ReformatErrorMessageForTitle(
string errorMessage)
{
errorMessage = errorMessage.Replace("<br/>", ",");

// Remove the last ',', if any.
if (errorMessage.Length > 0 &&
errorMessage[errorMessage.Length - 1] == ',')
{
return errorMessage.Substring(0, errorMessage.Length - 1);
}

return errorMessage;
}

private static Exception BuildMoreExpressiveException(
Control control, HttpException exception)
{
return new InvalidOperationException(string.Format(
"Sorry, you have encountered a rare .NET bug. " +
"Control '{1}', which is the parent control of " +
"'{0}', contains '<% %>' tags. Because of the " +
"existance of those tags, this parent control " +
"can not be modified and it is impossible to " +
"dynamically add validators to it. You can " +
"fix this by wrapping '{0}' in another control" +
". For instance: " +
"<asp:{2} runat='server' ID='{1}'>" +
"<asp:PlaceHolder runat='server'>" +
"<asp:{3} runat='server' id='{0}' />" +
"</asp:PlaceHolder>" +
"</asp:{2}>. {4}", control.ID, control.Parent.ID,
control.Parent.GetType().Name,
control.GetType().Name, exception.Message));
}
}

The trick here is that the AspNetValidationIntegration class now only has one single extension method named For<TEntity> which will return a new AspNetValidationIntegrator<TEntity>. The magic happens in the AddConverter method. In this method the PropertyProxyValidator's ValueConvert event is hooked. Here is that method again:

     private static void AddConverter<TValue>(
PropertyProxyValidator proxy,
Func<string, TValue> converter, string errorMessage)
{
if (converter != null)
{
proxy.ValueConvert += (sender, e) =>
{
string value = e.ValueToConvert as string;
try
{
e.ConvertedValue = converter(value);
}
catch (Exception ex)
{
if (string.IsNullOrEmpty(errorMessage))
{
e.ConversionErrorMessage = ex.Message;
}
else
{
e.ConversionErrorMessage = errorMessage;
}

e.ConvertedValue = null;
}
};
}
}

The AddConverter method hooks an anonymous method to the ValueConvert event. The converter and errorMessage arguments are used as closures in this anonymous methods and used to do the actual convertion and display an message when the conversion fails.

Just as before this code hides the use of the PropertyProxyValidator and even the use of the Validation Application block from the presentation layer. Validation Application Block is now just an implementation detail, which, from an architectural perspective, is a good thing.

Happy validating!

- ASP.NET, C#, Enterprise Library, Validation Application Block - four comments / No trackbacks - §

The code samples on my weblog are colorized using javascript, but you disabled javascript (for my website) on your browser. If you're interested in viewing the posted code snippets in color, please enable javascript.

four comments:

Looks good, think I'll give it a shot in something I'm working on. What technique are you using for client-side validation?
Scott Williams - 16 09 10 - 17:05

Hi Scott. I currently only do server-side validation. A technique like described in this article however, should allow us to enable client-side validation by changing it in a single place. I planned to take a more thorough look at client-side validation in the future. When I do, I'll write a post on it. Keep an eye on my blog.
Steven (URL) - 17 09 10 - 18:55

HI Steve,

Again, this is a great post of yours. This value conversion errors gave me hard time and it took a while for me to figure out the correct solution. Your approach is really great as we are adding value conversion at just one place.

When I saw your post I thought there must be a more simple approach to this and I am going to help you.

In the CreateValidatorFor function from the previous post please add following line
proxy.ValueConvert += (sender, e) => { };
and that's it, you do not need to do anything from this post.

To simplify more, you can move code from BindProxyValidatorToDomainProperty to CreateValidatorFor and delete BindProxyValidatorToDomainProperty function.

Again, I really appreciate your efforts and your are the one who have shown us this guidance to make validation really simple, easy to implement. Adding PropertyProxyValidator and ServerSideValidationExtender for each control in the markup was a pain and error prone. Code was getting cluttered with lot of crap.

Thanks,
Mandeep.
Mandeep (URL) - 30 07 11 - 16:27

The valueconvert function will show the error message defined in the TypeConversionValidator attribute

[TypeConversionValidator(typeof(int), MessageTemplate = "Please enter numeric values only")]
public int Age
{
get;
set;
}
Mandeep (URL) - 30 07 11 - 16:35


No trackbacks:

Trackback link:

Please enable javascript to generate a trackback url


  
Remember personal info?

/

Before sending a comment, you have to answer correctly a simple question everyone knows the answer to. This completely baffles automated spam bots.
 

  (Register your username / Log in)

Notify:
Hide email:

Small print: All html tags except <b> and <i> will be removed from your comment. You can make links by just typing the url or mail-address.