How to dynamically modify model meta data in ASP.NET MVC
Normally you just add the [Required] attribute to a view model to make it required. But I needed a way to configure whether a field should be required or not. The requirement was that it should be configured through web.config:
<appSettings>
<add key="ticket-cat1-required" value="true" />
</appSettings>
Having to modify the view or the controller would not be very clean. Instead it’s much better to take advantage of the ModelValidatorProvider. I could have just done like this:
public class ConfigurableModelValidatorProvider : LocalizedModelValidatorProvider
{
protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<System.Attribute> attributes)
{
bool isRequired = metadata.ContainerType == typeof (CreateViewModel)
&& ConfigurationManager.AppSettings["ticket-cat1-required"] == "true";
var theAttributes = attributes.ToList();
if (!theAttributes.Any(x => x is RequiredAttribute) && isRequired)
theAttributes.Add(new RequiredAttribute());
return base.GetValidators(metaDataContext.Metadata, context, attributes);
}
}
And then assigned it in global.asax:
protected void Application_Start()
{
ModelValidatorProviders.Providers.Clear();
ModelValidatorProviders.Providers.Add(new ConfigurableModelValidatorProvider());
//...
}
But that would have created a tightly coupled provider.
The loosely coupled way
Instead I decided to take advantage of my inversion of control container and define some interfaces.
/// <summary>
/// Can adapt the generated metadata before it's sent to the view
/// </summary>
public interface IModelMetadataAdapter
{
/// <summary>
/// Adapt the meta data
/// </summary>
/// <param name="context">Context information</param>
void Adapt(MetadataContext context);
}
The context used to modify the meta data:
/// <summary>
/// context for <see cref="IModelMetadataAdapter"/>
/// </summary>
public class MetadataContext
{
/// <summary>
/// Initializes a new instance of the <see cref="MetadataContext"/> class.
/// </summary>
/// <param name="metadata">The metadata.</param>
public MetadataContext(ModelMetadata metadata)
{
if (metadata == null) throw new ArgumentNullException("metadata");
Metadata = metadata;
}
/// <summary>
/// See MSDN for info
/// </summary>
public ModelMetadata Metadata { get; set; }
}
Which allowed me to create this class (which is automatically registered in Griffin.Container):
[Component]
public class ToggleRequiredOnCreateModel : IModelMetadataAdapter
{
public void Adapt(MetadataContext context)
{
if (context.Metadata.ContainerType != typeof(CreateViewModel))
return;
context.Metadata.IsRequired = false;
if (context.Metadata.PropertyName != "Category1")
return;
context.Metadata.IsRequired = ConfigurationManager.AppSettings["ticket-cat1-required"] == "true";
}
}
To make it all possible I’ve also had to modify the validator provider:
public class ConfigurableModelValidatorProvider : LocalizedModelValidatorProvider
{
protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<System.Attribute> attributes)
{
var services = DependencyResolver.Current.GetServices<IModelMetadataAdapter>();
var metaDataContext = new MetadataContext(metadata);
foreach (var service in services)
{
service.Adapt(metaDataContext);
}
var theAttributes = attributes.ToList();
if (!theAttributes.Any(x => x is RequiredAttribute) && metaDataContext.Metadata.IsRequired)
theAttributes.Add(new RequiredAttribute());
return base.GetValidators(metaDataContext.Metadata, context, attributes);
}
}
In my case I’m using my Griffin.MvcContrib project to handle the localization, that’s why I inherit LocalizedModelValidatorProvider and not DataAnnotationsModelValidatorProvider.