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 to 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 have 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.
Reference: How to dynamically modify model meta data in ASP.NET MVC from our NGC partner Jonas Gauffin at the jgauffin’s coding den blog.