How to add MetaData to a dynamically build MVC3 ViewModel?
Asked Answered
L

2

1

One of the key features of a project I'm working on is the ability for the user to configure Forms (as in "Forms" to fill-up) based on a pool of pre-existing field types (well known types, for instance "user name", "date of birth" etc. but also "generic types" like "string", "DateTime" etc.).

We used to have a static ViewModel that worked fine for the "well known" types and looked like this:

public class UserInputModel
{
    [StringLength(200)]
    public string Name { get; set; }

    [Required(ErrorMessageResourceName = "BirthDateEmptyError", ErrorMessageResourceType = typeof(Resources.ErrorMessages))]
    public DateTime BirthDate { get; set; }

    //Here comes a lot of other properties
}

All the known properties were listed and we were showing or hiding them given the context.

But the last requirement came and changed all that. The user shall now be able to add as many generic type fields as he wants. In order to do this, we decided to make this InputModel entirely dynamic. It now looks like this:

public class UserInputModel
{
    // Each ModelProperty has an "Id" and a "Value" property
    public ICollection<ModelProperty> Properties { get; set; }
}

This works like a charm. The razor view only has to iterates over the collection, create the corresponding controls for each property of the collection in a more than standard way:

@Html.TextBoxFor(m => m.Properties[index].Value);

... and we nicely get the data back as a filled form.

=> This works fine, but we don't have any client-side validation. For this, we would need some Metadata... which we don't have via annotations anymore since we're dynamically creating the model.

In order to provide those MetaData, I created a CustomModelMetadataProvider that inherits from DataAnnotationsModelMetadataProvider and registered it as the new ModelMetadataProvider in the Global.asax. The CreateMetadata() function gets called upon creation of the ViewModel, and that for each of the properties of my ViewModel... sofar so good.

Where the problem starts: in order to add some metadata to the current property, I first need to identify which property I am currently looking at ("Name" has a maxlength of 200, "date of birth" hasn't so I cannot assign a maxlength to every property per default). And somewhow I didn't manage to do that yet since all the properties have the same name Value and the same container type ModelProperty.

I tried accessing the container of the property via reflection, but since the ModelAccessor's target is the ViewModel itself (because of the lambda expression m => m.Properties), the following construct gives me the ViewModel as a whole, not just the ModelProperty:

var container = modelAccessor.Target.GetType().GetField("container");
var containerObject = (UserInputModel)container.GetValue(modelAccessor.Target);

I've been flipping this over and over but cannot find a way to identify which ModelProperty I have in hand. Is there a way to do this?

Update: after flipping this in every possible direction for a while, we finally went another way. We are basically using unobstrusive javascript to use MVC's validation capabilities without touching attributes nor metadata. In short, we add HTML attributes like value-data="true" (and all other required attributes) to the @Html.TextBoxFor() statements. This works wonderfully for all the atomic validations (required, stringlength etc.).

Lewandowski answered 14/9, 2012 at 14:32 Comment(0)
T
0

Tim, you can leverage what appears to be client-side validation through Ajax with the Remote attribute on your properties.

Basically, you'll need to set up a validation controller and then write some smarts into that controller. But at least you'd be able to write some helper methods and keep it all in one place. You would have a series of validators, based on the meta data that you are presenting to the end users, and each validator method would work for a particular type with good re-use.

The one pitfall to this approach would be that you would need to write a validation method for each type and condition that you want to support. Sounds like you're having to go down that road anyways, though.

Hope this helps.

Tendance answered 14/9, 2012 at 14:47 Comment(1)
Thanks for your answer, unfortunately - unless I misunderstood you - I think you're missing the point. The whole problem here isn't the validation in itself but the injection of the metadata for the properties.Frangible
L
0

See if this article help you: Technique for carrying metadata to View Models with AutoMapper.

Also use this one for ideas (custom model metadata provider): changing viewmodel's MetadataType attribute at runtime

Fluent validation is probably the best option for you in my mind, but its obviously up to you to select the best match among those above.

Update

Try use ModelMetadata and override ModelMetadataProvider: Dive Deep Into MVC: ModelMetadata and ModelMetadataProvider. This way you completely customize your model metadata (this replaces data annotations) and you have complete control on what is happening, rather than relying on ASP.NET MVC.

Another good place to look at it is Creating your own ModelMetadataProvider to handle custom attributes.

Hope this all is of help to you.

Loudmouth answered 14/9, 2012 at 15:35 Comment(5)
Thanks for your answer. About the first link, the author still seems to use the PropertyName as a discriminator... which is exactly what I cannot do (all my properties are called "Value"). About the second link, they're using the type of the property to discriminate it... but again, all my properties of the same kind...Frangible
How about "fluent validation"? I know (I haven't used it) many people did and quite successfully and praising it big time. Try to go with totally different approach: which is overriding (providing your own) ModelMetadataProvider. See the update in my answer.Loudmouth
I also haven't used Fluent validation, but as far as I can tell, it's using lambdas that rely on property types... so that's again not for me. About your update, that's exactly what I'm doing ; I created my CustomModelMetadataProvider and am trying to modify the flow of things. And that's exactly where I have some problems since I cannot identify a property based on its name or on its type...Frangible
Hi Tim, was it of any help for you please?Loudmouth
Unfortunately not, I'm still stuck there.Frangible

© 2022 - 2024 — McMap. All rights reserved.