6

I have a model ModelA with a member toBeRemoteChecked and a model MapToA with a member valueToMap. Whenever I create an instance of ModelA, I also need an instance of MapToA, so I have a model CreateModelA which includes a member modelA and a member valueToMap. When the form is submitted, I add the modelA to the database table ModelA and create and add an instance to MapToA which consists of an id of modelA and the valueToMap. In Terms of code

public class ModelA
{
    [Key]
    public int ID { get; set; }
    [Required, Remote("isValid", "MyController", ErrorMessage = "not valid")]
    public string toBeRemoteChecked { get; set; }
}

public class MapToA
{
    [Key]
    public int Map_ID { get; set; }
    [Required]
    public int modelAID { get; set; }
    [Required]
    public int valueToMap { get; set; }
}

public class CreateModelA
{
    public ModelA modelA { get; set; };
    public int valueToMap { get; set; };
}

When I edit an instance of ModelA, values in MapToA don't matter (and in most cases there's more than one instance of mapToA with the same modelA id), but the remote validation of toBeRemoteChecked remains important.

My Problem: binding for the validation method:

public ActionResult isValid(string toBeRemoteChecked) { ... }

If I leave it as it is, it is working when editing a ModelA, but not when I'm creating a ModelA via CreateModelA (I always get null value in toBeRemoteChecked). When I use the BindPrefix

public ActionResult isValid([Bind(Prefix = "modelA.toBeRemoteChecked")] string toBeRemoteChecked) { ... }

it is working when I create a ModelA, but not when I'm editing it.

When I try to change the "name" in the Create.cshtml by adding a ... @Name = "toBeRemoteChecked" ... (instead of the modelA.toBeRemoteChecked that's created by the HTML helper) in the htmlAttributes of the @Html.TextBoxFor, then validation is working, but the binding of the value to the table get's lost and I get the error when the values are saved to the database (null value).

So, how do I achieve the different binding for creating and editing?

So far, my workaround is to make ModelA and CreateModelA : IValidatableObject and check the member toBeRemoteChecked in my public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) method. But that one displays the error messages on top of the form and not at the place of the TextFor box.

So: best solution: how to do the binding that the remote validation works in both cases?

Second best: how to display the error messages of IValidatableObject near the object where it belongs to (and get the error messages right at hand, not after submitting)

Different ideas or solutions: welcome.

Thanks.

outofmind
  • 1,430
  • 1
  • 20
  • 37

2 Answers2

3

An interesting issue, and similar to this question, which as an result I reported a issue at Codeplex, but it has not been resolved yet. The link includes a suggested modification to the jquery.validate.js file which would solve this (it strips the prefix) but that means you would need to maintain it whenever you update the script so not really desirable.

One option would be to change CreateModelA to inherit from ModelA and just add the int valueToMap property so that you never have a prefix - your always using @Html.TextBoxFor(m => m.toBeRemoteChecked) instead of @Html.TextBoxFor(m => m.modelA.toBeRemoteChecked)

Also, [Remote] is client side only validation, which means you still need to perform the validation in the server when you post. So you could just accept that you don't have client side validation for the property, and instead add a ModelState error in the POST methods(s) for the property and return the view so that its displayed in the associated ValidationMessageFor() element

Side note: The fact your model has a [Key] attribute suggests this is a data model, not a view model, and [Remote] is a view specific attribute. You should be using view models, especially when editing data. (refer What is ViewModel in MVC?)

Community
  • 1
  • 1
  • Thanks. As for client side validation, I tried the inherit option - also got a demo project working, but when I `db.ModelAset.Add(createModelA)` I either end up with the unwanted additional columns automatically added in the database or I get an `invalid column name` error after I manually deleted those columns from the database. I can `db.ModelAset.Add(modelA)` when I manually create the `modelA`in the controller by copying all necessary members from `createModelA` to `modelA`. Is there a short cut for this? (my real model has about 20 members, so it's possible, but not very nice). Thanks. – outofmind Aug 02 '15 at 12:23
  • You need to read my last paragraph again :) A view model has no relationship to a database. –  Aug 02 '15 at 12:50
  • Ok. so I have the additional effort when moving the data from view model to data model, thanks for your help :-) – outofmind Aug 02 '15 at 18:29
  • I'm not really happy yet - view models double my code at some places (copying 20 members from a to b and back, not necessary when I use my data model) - without view models (and the inherit option to get binding for client side validation), entitiy framework generates this unwanted discriminator column in the db and wants to use it every here and then (the `createModel` was never intended to be written to the db), so deleting it ends up in non-working code - to me it's a big workaround just to get a simple binding to my validation routine - so: is there another method for the binding problem? – outofmind Aug 04 '15 at 07:55
  • It not a work around at all. Using view models is the recommended and correct way to represent the data you display in a view. And view specific attribute should never be in data model. And no, there is not another way - just do it the right way! –  Aug 04 '15 at 08:44
  • I found another way, see my answer below - thanks for your valuable help - it made me look in other directions and so I found the solution below – outofmind Aug 07 '15 at 06:59
2

I found a solution without inheritance (and without view models) that solves my binding problem with just little change to my code.

There's two ways of binding for remote validation, you can either just pass the member that has to be remote checked

public ActionResult isValid(string toBeRemoteChecked) { ... }

or you can pass the instance of the class of that member.

public ActionResult isValid(ModelA modelA) { ... }

Inside the second variant, of course, you have to replace toBeRemoteChecked with modelA.toBeRemoteChecked. On this second version the binding works in both cases - when editing and also when creating my instance of ModelA in the context above. In order to make the binding work, it's crucial that the parameter name of the remote validation method matches the member name in the CreateModelA, i.e. modelA in my case.

In case you have a very complex model, you can just initialize the parameter modelA with the members you want to use by using bind/include, i.e. in my case I'd use

public ActionResult isValid([Bind(Include = "toBeRemoteChecked")] ModelA modelA) { ... }

By default (without Include), all other members will remain null or have a default value - so you need to use Include only if you need other members for validation as well - in my case, I would have the same when omitting the Include)

outofmind
  • 1,430
  • 1
  • 20
  • 37