5

I have two classes I'm using as the model of two different views. You can see that the second class contains an instance of the first. The first contains Remote validation attributes.

[MetadataType( typeof( ExceptionLogModel.EmailRecipientMetadata ) )]
public class EmailRecipientViewModel
{
    public int EmailRecipientID { get; set; }

    [Remote( "ValidateEmailRecipientNameUniqueness", "EmailRecipient", ErrorMessage = "Name is not unique." )]
    public string Name { get; set; }

    [Remote( "ValidateEmailRecipientEmailUniqueness", "EmailRecipient", ErrorMessage = "Email is not unique." )]
    public string Email { get; set; }
}

public class EmailRecipientChoices
{
    public List<EmailRecipient> UnselectedEmailRecipients { get; set; }
    public List<EmailRecipient> SelectedEmailRecipients { get; set; }
    public EmailRecipientViewModel EmailRecipient { get; set; }
}

When these validations trigger in the browser, two different requests are made depending on which class the view used. You can see that the query string parameter names are different:

http://localhost:55327/EmailRecipient/ValidateEmailRecipientNameUniqueness?Name=sdhsdgh

http://localhost:55327/EmailRecipient/ValidateEmailRecipientNameUniqueness?EmailRecipient.Name=sdhsdgh

Here is the current version of my action method which does not work with the second URL:

public JsonResult ValidateEmailRecipientNameUniqueness( string name )
{
    var isValid = !_emailRecipientRepo.NameExists( name );

    return Json( isValid, JsonRequestBehavior.AllowGet );
}

When the second URL is used, the name parameter will be null. I've read that I should be able to add a Bind attribute to that parameter and add a prefix, but this is not working either. I even tried setting the prefix to EmailRecipient. just in case it needed the dot. I also tried it with a capital N in Name just in case. No go. Adding this also breaks it for the other URL!

public JsonResult ValidateEmailRecipientNameUniqueness( [Bind( Prefix = "EmailRecipient")] string name )

Possible solutions

I could have the method take an instance of EmailRecipientViewModel and create an IModelBinder for it in which I could look for either naming convention and assign it to the instance. This seems like more work than it should be.

I could use the overload for @Html.EditorFor() and tell it to use "Name" for htmlFieldName, and also use @Html.ValidationMessage( "Name" ) rather than ValidationMessageFor. The only downside to this is potential naming conflicts, but that's not too big of a deal. I'd just have to use a unique name for all instances of this class being used. Update: Actually, if I do this, is breaks things when I post the form because I changed the names. That's no good.

...

I just figured out that I could have the method take no parameters, and access the Query String manually. This is a pretty simple solution, but I don't get the nice parameter.

string name = Request.QueryString[ "Name" ] ?? Request.QueryString[ "EmailRecipient.Name" ];

This is easy enough that I'm probably just going to use this. However, since I already have this question typed up, I'll ask, Is there a more elegant solution?

Glazed
  • 2,048
  • 18
  • 20

2 Answers2

3

Well, I know this is late but there is a more elegant solution:

public JsonResult ValidateEmailRecipientNameUniqueness (EmailRecipient recipient)
{
    string name = recipient.Name;
    var isValid = !_emailRecipientRepo.NameExists(name);
    return Json(isValid, JsonRequestBehavior.AllowGet);
}

In other words, use the model itself. It will correctly bind the name property and you only need this value.

SmartDev
  • 2,802
  • 1
  • 17
  • 22
  • That does not work when the text box is named just "name". That's the problem. I had two different text boxes that both wanted to user the same validation method. – Glazed Mar 05 '15 at 14:31
  • I've just tested it now on my side. I used Fiddler and I can confirm: it works without any prefix. – SmartDev Mar 05 '15 at 15:21
  • The reason it doesn't work is because I have two different models. I have model "A", and also model "B" that contains an instance of model "A". The remote validator is for a property on model "A". When I'm using model "B", the text box receives the name "EmailRecipient.Name" because it's a property on a nested object. When I'm using just model "A", the text box is named "Name". If I use `EmailRecipient` as the parameter then it won't work when the text box is named "EmailRecipient.Name". It only works when the text box is named "Name". You need to create two different views to test this. – Glazed Mar 05 '15 at 16:13
  • To be clear, I just tested this, and it does not work when my view's model is EmailRecipientChoices. – Glazed Mar 05 '15 at 16:15
1

There isn't really a clean way to do this without rolling your own validation or model binder. Think of it like model binding, the model binder needs to know the name of what is coming in, same for remote validation. One approach that you could take is to create two separate remote validation methods in your controller that end up calling the one method that actually does all of the validation work.

Joe Cartano
  • 2,997
  • 3
  • 22
  • 40