3

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.

sebastiaan
  • 5,870
  • 5
  • 38
  • 68
  • Please show your view. It is possible to make a custom `DataAnnotationsModelValidatorProvider` to inject the validators you want, but I cannot provide an example without seeing the specifics of the view. – counsellorben Nov 28 '11 at 17:32
  • Thanks I'll look into that provider, but if you can help out that would be great, I've added the edit view and the editortemplate that I'm using. – sebastiaan Nov 28 '11 at 20:32

3 Answers3

1

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.

Rich O'Kelly
  • 41,274
  • 9
  • 83
  • 114
  • I have trouble wording what I'm trying to do anyway, but can you point me to any posts in which something similar is being done? I can't find anything that sort-of applies validation rules after I get the attribute list from the database. Any pointers would be welcome. – sebastiaan Nov 28 '11 at 14:52
  • See http://fluentvalidation.codeplex.com/wikipage?title=mvc for example usage, but in your particular case fluent validation will allow you to conditionally apply validation rules based on the data you retrieved from the DB. – Rich O'Kelly Nov 28 '11 at 15:10
1

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()); 
counsellorben
  • 10,924
  • 3
  • 40
  • 38
  • Sorry for the late reply, I had some trouble getting this to work with Ninject, but that has been fixed now: http://stackoverflow.com/questions/8356811/how-to-stop-ninject-from-overriding-custom-dataannotationsmodelvalidatorprovider – sebastiaan Dec 13 '11 at 14:21
0

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.

wayne.blackmon
  • 714
  • 13
  • 24
  • I understand that this is the default way of doing it in MVC3. However, I am getting a list of properties and have no way of providing any DataAnnotations. If I set a [Required] attribute on SelectedValue, than all of the 20 properties (or attributes, as I have called them) will be required as I'm for-eaching over them. So I can't use this technique unfortunately. – sebastiaan Nov 28 '11 at 20:21