sebastiaan
sebastiaan

Reputation: 5917

In ASP.NET MVC3, how do I manually apply validation on generated properties

Here's the situation, I have a list of about 20 properties (called Attributes) that I've defined in my database. This consists of a name, possible values, an optional regex, a boolean that indicates the field is required, etc.

In my ViewModel I get the list of attributes and in my view as List I have a nice EditorTemplate for AttributeViewModel to show them using Steve Sanderson's cool BeginCollectionItem to make sure the post gets bound back to a list of AttributeViewModel (this works just fine).

My AttributeViewModel looks like this:

public class AttributeViewModel
{
    public string Description { get; set; }
    public IEnumerable<SelectListItem> Values { get; set; }
    public string SelectedValue { get; set; }
    public byte RenderAs { get; set; }
    public int AttributeID { get; set; }
    public int ID { get; set; }
    public int RegexValidation { get; set; }
    public bool IsRequired { get; set; }
}

My View looks like this (edit.cshtml):

@model Company.Services.ViewModels.StaffMemberViewModel

<h2>Edit</h2>
@using (Html.BeginForm())
{
    Some fields here, nothing of interest.

    @Html.EditorFor(model => model.AttributeValues)

    <input type="submit" value="Send" />
 }

Here's the interesting bit though, this is my EditorTemplate for AttributeValues:

@using Company.Web.Helpers // This is where "BeginCollectionItem" lives
@model Company.Services.ViewModels.AttributeViewModel

using (Html.BeginCollectionItem("attributes"))
{
    <div class="editor-label">
        @Model.Description
    </div>
    <div class="editor-field">
       @Html.DropDownListFor(m => m.SelectedValue, new SelectList(Model.Values, "Value", "Text"), "-- Select --")
       @Html.HiddenFor(model => model.AttributeID) 
    </div>
}

What I would like to do is use the IsRequired and RegexValidation to make sure the SelectedValue for each attribute is valid. How would I go about doing so? If possible, I'd really like to take advantage of the MVC3 validation framework and unobtrusive validation like I "normally" would.

I obviously can't dynamically add a RequiredAttribute or a RegularExpressionAttribute as these differ for each of my attribute objects in the list.

Upvotes: 3

Views: 711

Answers (3)

counsellorben
counsellorben

Reputation: 10924

This is untested. You may have to play with this to get your desired result.

First, create your custom DataAnnotationsModelValidatorProvider class:

public class MyModelMetadataValidatorProvider : DataAnnotationsModelValidatorProvider
{
    internal static DataAnnotationsModelValidationFactory DefaultAttributeFactory = Create;
    internal static Dictionary<Type, DataAnnotationsModelValidationFactory> AttributeFactories = 
        new Dictionary<Type, DataAnnotationsModelValidationFactory>() 
        {
            {
                typeof(RequiredAttribute),
               (metadata, context, attribute) => new RequiredAttributeAdapter(metadata, context, (RequiredAttribute)attribute)
            },
            {
                typeof(RegularExpressionAttribute),
               (metadata, context, attribute) => new RegularExpressionAttributeAdapter(metadata, context, (RegularExpressionAttribute)attribute)
            }
        };

    internal static ModelValidator Create(ModelMetadata metadata, ControllerContext context, ValidationAttribute attribute)     
    {
        return new DataAnnotationsModelValidator(metadata, context, attribute);
    }

    protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes)
    {
        List<ModelValidator> vals = base.GetValidators(metadata, context, attributes).ToList(); 
        if (metadata.ModelType.Name == "SelectedValue")
        {
            // get our parent model
            var parentMetaData = ModelMetadataProviders.Current.GetMetadataForProperties(context.Controller.ViewData.Model, 
                metadata.ContainerType);

            // get the associated AttributeId
            var attributeId = Convert.ToInt32(parentMetaData.FirstOrDefault(p => p.PropertyName == "AttributeId").Model);

            // get AttributeViewModel with specified AttributeId from repository
            var attributeViewModel = _db.AttributeViewModels.FirstOrDefault(x => x.AttributeId == attributeId);

            DataAnnotationsModelValidationFactory factory;

            // check if required
            if (attributeViewModel.IsRequired)
            {
                // must be marked as required
                var required = new RequiredAttribute();
                required.ErrorMessage = attributeViewModel.Description.Trim() +
                    " is Required";
                if (!AttributeFactories.TryGetValue(required.GetType(), out factory))
                    factory = DefaultAttributeFactory;

                vals.Add(factory(metadata, context, required));
            }

            // check for regex
            if (attributeViewModel.RegexValidation > 0)
            {
                // get regex from repository
                var regexValidation = _db.attributeViewModels.
                    FirstOrDefault(x => x.RegexValidation == attributeViewModel.RegexValidation);
                var regex = new RegularExpressionAttribute(regexValidation.Pattern);
                regex.ErrorMessage = attributeViewModel.Description.Trim() +
                    " is not in a valid format";
                if (!AttributeFactories.TryGetValue(regex.GetType(), out factory))
                    factory = DefaultAttributeFactory;

                vals.Add(factory(metadata, context, regex));
            }
        }
        return vals.AsEnumerable();
    }
}

Then, add the following to Application_Start in Global.asax.cs:

ModelValidatorProviders.Providers.Clear();
ModelValidatorProviders.Providers.Add(new MyModelMetadataValidatorProvider()); 

Upvotes: 1

wayne.blackmon
wayne.blackmon

Reputation: 761

I hope I am understanding your question correctly. You want to add custom validation attributes, annotation and validation logic to your views?

If so, you want to go to the System.ComponentModel.DataAnnotation namespace. Your validation logic will be placed in a class deriving from ValidationAttribute:

using System.ComponentModel.DataAnnotation;

public class MyValidationAttribute : ValidationAttribute
{
    string readonly _validationParameter;

    public MyValidationAttribute(string validationParameter)
    {
        _validationParameter = validationParameter;
    }

    protected override ValidationResult IsValid(object value,
        ValidationContext validationContext)
    {
        // add validation logic here
        if (//not valid)
        {
            var errorMessage = FormatErrorMessage(validationContext.DisplayName);
            return new ValidationResult(errorMessage);
        }
        return ValidationResult.Success;
    }
}

You can apply the attribute to any model property

[Required]
[MyValidationAttribute("parameter", ErrorMessage="Error in {0}")]
public string MyProperty { get; set; }

I hope this helps. See

Professional ASP.NET MVC 3

page 127 for more info.

Upvotes: 0

Rich O&#39;Kelly
Rich O&#39;Kelly

Reputation: 41757

Consider using FluentValidation.Net (which is available via NuGet from the following Install-Package FluentValidation.MVC3). It makes any sort of relatively complex data validation far simpler and more intuitive than a declarative style. There is support for client-side validation too.

Upvotes: 1

Related Questions