Views: 23498
Number of votes: 2
Average rating:

MVC ValidationAttribute ErrorMessage populated at runtime

I am working on a project using EPiServer 7 and MVC 4. One of the nice features in MVC is ValidationAttributes in the model such as RequiredAttribute and RegularExpressionAttribute. These attributes give you server side validation on your model, if combined with jquery.validate and jquery.validate.unobtrusive you also get client side validation without writing any javascript yourself. This is very nice and keeps the code clean and it looks something like this:

In my controller I create a viewmodel and pass it to the view, using a viewmodel is optional but I like it and follow the pattern described by Joel Abrehamsson here: http://joelabrahamsson.com/episerver-and-mvc-what-is-the-view-model/

public class EducationPlanSearchController : ProtectedPageControllerBase<EducationPlanSearchPage>
{
    [HttpGet]
    public ActionResult Index(EducationPlanSearchPage currentPage)
    {
        EducationPlanSearchPageViewModel viewModel = new EducationPlanSearchPageViewModel(currentPage);
        return View(viewModel);
    }
 
    ... HttpPost part left out 
}

My viewmodel has one property its required and has to have a correct format:

public class EducationPlanSearchPageViewModel : PageViewModel<EducationPlanSearchPage>
{
    public EducationPlanSearchPageViewModel(EducationPlanSearchPage currentPage) : base(currentPage)
    {
    }
 
    [Required(ErrorMessage = "Compiletime error required")]
    [RegularExpression(@"^\d{6,6}-?\d{4,4}$", ErrorMessage = "Compiletime error format")]
    public string Cpr { get; set; }
}

The view is very simple it is just a form with one input field and a submit button:

@model Pdk.Website.Models.ViewModels.EducationPlanSearchPageViewModel
 
<div class="content-block">
    <div class="formLine">
        @using (Html.BeginForm())
        {
            @Html.ValidationSummary()
            @Html.EditorFor(m => m.Cpr)
            <input type="submit" value="@Model.CurrentPage.SearchButtonText"/>
        }
    </div>
</div>

In my layout I include the needed jquery files:

<script src="/Scripts/jquery-1.10.1.js"></script>
<script src="/Scripts/jquery.validate.js"></script>
<script src="/Scripts/jquery.validate.unobtrusive.js"></script>

If someone press the submit button with an empty input field or one with the wrong format they will get an error message, and this happens client side, using the ErrorMessage defined in the attribute for the property in the viewmodel:

This is all standard MVC so far, there is just one problem with this because I would like to set the ErrorMessage on the ValidationAttribute runtime using an EPiServer page property. That would enable the editors to change the error message when they want to.

To do this I have to implement my own  model metadata provider which is used by  ASP.NET MVC to load the validation attributes, tell my IoC container to use this, and set the ErrorMessage when the viewmodel is created. First I create my own  model metadata provider:

public class MyModelMetaDataProvider : CachedDataAnnotationsModelMetadataProvider
{
    private readonly Dictionary<Tuple<Type, string>, List<Tuple<Type, string>>> _errorMessageDictionary = new Dictionary<Tuple<Type, string>, List<Tuple<Type, string>>>();
 
    public void SetValidationErrorMessage(Type containerType, string propertyName, Type validationAttribute, string errorMessage)
    {
        if (!string.IsNullOrWhiteSpace(errorMessage))
        {
            Tuple<Type, string> key = new Tuple<Type, string>(containerType, propertyName);
            if (!_errorMessageDictionary.ContainsKey(key))
            {
                _errorMessageDictionary[key] = new List<Tuple<Type, string>>();
            }
 
            Tuple<Type, string> value = new Tuple<Type, string>(validationAttribute, errorMessage);
            _errorMessageDictionary[key].Add(value);                
        }
    }
 
    protected override CachedDataAnnotationsModelMetadata CreateMetadataPrototype(IEnumerable<Attribute> attributes, Type containerType, Type modelType, string propertyName)
    {
        CachedDataAnnotationsModelMetadata model = base.CreateMetadataPrototype(attributes, containerType, modelType, propertyName);
 
        Tuple<Type, string> key = new Tuple<Type, string>(containerType, propertyName);
 
        if (_errorMessageDictionary.ContainsKey(key))
        {
            List<Tuple<Type, string>> errorMessageList = _errorMessageDictionary[key];
            foreach (ValidationAttribute attribute in attributes.OfType<ValidationAttribute>())
            {
                var value = errorMessageList.FirstOrDefault(t => t.Item1 == attribute.GetType());
                if (value != null)
                {
                    attribute.ErrorMessage = value.Item2;
                }                    
            }
        }
        return model;
    }
}

It contains a dictionary where I add the error messages I want to update, the key in the dictonary is the viewmodel type and property name, the value is the attribute type and error message. When a view with a model containing validation attributes is created  CreateMetadataPrototype is called and the ErrorMessages is set using this dictionary.

The viewmodel is updated to call SetValidationErrorMessage from the constructor, I get the new error messages from the PageData model, thus allowing the editors to edit it as a property on the page:

public class EducationPlanSearchPageViewModel : PageViewModel<EducationPlanSearchPage>
{
    public EducationPlanSearchPageViewModel(EducationPlanSearchPage currentPage) : base(currentPage)
    {
        MyModelMetaDataProvider metadataProvider = (MyModelMetaDataProvider)ModelMetadataProviders.Current;
        metadataProvider.SetValidationErrorMessage(GetType(), "Cpr", typeof(RequiredAttribute), currentPage.ErrorCprEmpty);
        metadataProvider.SetValidationErrorMessage(GetType(), "Cpr", typeof(RegularExpressionAttribute), currentPage.ErrorCprFormatWrong);
    }
 
    [Required]
    [RegularExpression(@"^\d{6,6}-?\d{4,4}$")]
    public string Cpr { get; set; }
}

Before it works I have to tell my IoC container to use my own ModelMetadataProvider:

container.For<ModelMetadataProvider>().Use<MyModelMetaDataProvider>();

Now when the Index action on EducationPlanSearchController is called the viewmodel is created and in its constructor the correct ErrorMessage is set on the current ModelMetadataProvider which is of the type MyModelMetaDataProvider and when the view is called the method CreateMetadataPrototype is called, and the correct ErrorMessage is set.

I hope this can help others in the same situation, I realize the code could use some more comments so don’t hesitate to ask if there is any problems with it. And if anybody has a cleaner solution to this problem, (apart from convincing the editor to give up runtime editing of error messages) then I would like the see it.

Jul 05, 2013

valdis
( By valdis, 7/9/2013 11:24:25 PM)

Great idea! :)
What I probably would be looking for is to elimate last 2 lines where you are manually "connecting" meta data provider cache dictionary content with current page property value for particular model validation message. This is pretty subject for automatization :) Anyway - thanks for an idea!

Drew Smith
( By Drew Smith, 9/4/2018 5:50:21 PM)

In case anyone references this later, the code above has a little bug in it.  The add statement to the list of error messages for each view model property fails to account for the fact that there should only be one error message per validation attribute type.  As is, the list will grow longer and longer when the page is loaded.  Here's my fix:

public void SetValidationErrorMessage(Type containerType, string propertyName, Type validationAttribute, string errorMessage)
		{
			if (!string.IsNullOrWhiteSpace(errorMessage))
			{
				Tuple key = new Tuple(containerType, propertyName);
				if (!_errorMessageDictionary.ContainsKey(key))
				{
					_errorMessageDictionary[key] = new List>();
				}

				Tuple value = new Tuple(validationAttribute, errorMessage);

				//Check if there is already a runtime error message for this validation attribute
				if (_errorMessageDictionary[key].Any(e => e.Item1 == validationAttribute))
				{
					int index = _errorMessageDictionary[key].FindIndex(e => e.Item1 == validationAttribute);
					_errorMessageDictionary[key][index] = value;
				}
				else
				{
					_errorMessageDictionary[key].Add(value); 
				}
			}
		}

Please login to comment.