14

I'm trying to perform remote validation on a property of an item within a collection. The validation works OK on the first item of the collection. The http request to the validation method looks like:

/Validation/IsImeiAvailable?ImeiGadgets[0].ImeiNumber=123456789012345

However on the 2nd item where the url looks like below, the validation doesn't work

/Validation/IsImeiAvailable?ImeiGadgets[1].ImeiNumber=123456789012345

Now I'm pretty sure the reason for this, is that binding wont work on a collection that doesn't begin with a zero index.

My validation method has a signature as below:

public JsonResult IsImeiAvailable([Bind(Prefix = "ImeiGadgets")] Models.ViewModels.ImeiGadget[] imeiGadget)

Because I'm passing an item within a collection I have to bind like this yet what I'm really passing is just a single value.

Is there anyway I can deal with this other than just binding it as a plain old query string.

Thanks

Edit: This is the quick fix to get the Imei variable but I'd rather use the model binding:

string imeiNumber = Request.Url.AbsoluteUri.Substring(Request.Url.AbsoluteUri.IndexOf("=")+1);

Edit: Here is my ImeiGadget class:

public class ImeiGadget
{
    public int Id { get; set; }

    [Remote("IsImeiAvailable", "Validation")]
    [Required(ErrorMessage = "Please provide the IMEI Number for your Phone")]
    [RegularExpression(@"(\D*\d){15,17}", ErrorMessage = "An IMEI number must contain between 15 & 17 digits")]
    public string ImeiNumber { get; set; }
    public string Make { get; set; }
    public string Model { get; set; }
}
FloatLeft
  • 1,317
  • 3
  • 23
  • 40
  • Is this aspx? If so, please retag – Ja͢ck May 29 '12 at 13:36
  • fixed the tagging -- assumed aspnet-mvc when I saw the [Bind] attr – Josh E May 29 '12 at 13:41
  • Are you posting via an Ajax request? If so could we see that code? I have a hunch :-) – CD Smith May 29 '12 at 13:44
  • This is an MVC3 question. Did I tag it wrongly? – FloatLeft May 30 '12 at 06:26
  • I am posting via an Ajax request which is handled by the Remote Validation feature of MVC3 – FloatLeft May 30 '12 at 06:26
  • can you post a snippet of your Razor template that surrounds the rendering of the list? Perhaps if you modified it to `foreach` over the collection, making the individual item's template its own Partial, the greater control you'd have over the markup could allow you to simplify and solve – Josh E Jun 04 '12 at 14:56

5 Answers5

8

You could write a custom model binder:

public class ImeiNumberModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var modelName = bindingContext.ModelName;
        var request = controllerContext.HttpContext.Request;
        var paramName = request
            .Params
            .Keys
            .Cast<string>()
            .FirstOrDefault(
                x => x.EndsWith(modelName, StringComparison.OrdinalIgnoreCase)
            );

        if (!string.IsNullOrEmpty(paramName))
        {
            return bindingContext
                .ValueProvider
                .GetValue(request[paramName])
                .AttemptedValue;
        }

        return null;
    }
}

and then apply it to the controller action:

public ActionResult IsImeiAvailable(
    [ModelBinder(typeof(ImeiNumberModelBinder))] string imeiNumber
)
{
    return Json(!string.IsNullOrEmpty(imeiNumber), JsonRequestBehavior.AllowGet);
}

Now the ImeiGadgets[xxx] part will be ignored from the query string.

Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
0

If you are sending up a single value to the server for validation, then your Action Method should only accept a scalar (single-value) parameter, not a collection. Your URL would then look like this (assuming default routing table for {controller}/{action}/{id}:

/Validation/IsImeiAvailable?ImeiNumber=123456789012345

the corresponding action method signature could look like this:

/* note that the param name has to matchthe prop name being validated */
public ActionResult IsImeiAvailable(int ImeiNumber)

EDIT: which you could then use to lookup whether that particular ID is available.

if you want to change the name of the parameter, you can modify the routing table, but that's a different topic.

The long story short of it is that if you wanted to do validate a collection of ImeiGadget, you'd GET or POST that full collection. For a single value, it doesn't make much sense to send up or to expect an entire collection.

UPDATE: Based on new info, I would look at where the remote validation attribute is being placed. It sounds like it might be placed on something like an IEnumerable<IMEiGadgets>, like this:

[Remote("IsImeiAvailable", "Validation", "'ImeiNumber' is invalid"]
public IEnumerable<ImeiGadget> ImeiGadgets { get; set;}

Would it be possible to move that attribute and modify it to be on the ImeiGadget class instead, to be something like this?

[Remote("IsImeiAvailable", "Validation", "'ImeiNumber is invalid"]
public int ImeiNumber { get; set;}

In theory, you shouldn't have to change anything on your HTML templates or scripts to get this working if you also make the change suggested in my answer above. In theory.

Josh E
  • 7,390
  • 2
  • 32
  • 44
  • would be IsImeiAvailable(int id) using the default routes – Kaido May 29 '12 at 14:15
  • BTW, my assumption is that all the OP is needing is that int ID, judging from `/Validation/IsImeiAvailable?ImeiGadgets[1].ImeiNumber=123456789012345` – Josh E May 29 '12 at 14:39
  • Remember I'm using the Remote Validation feature of MVC3. This means the ID of the input and validation conform to certain naming rules: – FloatLeft May 30 '12 at 06:24
  • @Josh: I'm not intending to validate the entire collection, but the html for the controls are rendered like id="ImeiGadgets_0__ImeiNumber" which results in a remote validation call of /Validation/IsImeiAvailable?ImeiGadgets[1].ImeiNumber=123456789012345. I cant have a period in the method sigature so to get the correct binding I have to bind to a collection. – FloatLeft May 30 '12 at 06:32
  • @Josh: The validation attribute is on the ImeiNumber property already. – FloatLeft May 31 '12 at 11:34
0

If you are posting the whole collection, but have a nonsequential index, you could consider binding to a dictionary instead http://www.hanselman.com/blog/ASPNETWireFormatForModelBindingToArraysListsCollectionsDictionaries.aspx

If you're only posting a single item or using a GET link, then you should amend to

/Validation/IsImeiAvailable?ImeiNumber=123456789012345

and

public JsonResult IsImeiAvailable(string imeiNumber)
Kaido
  • 3,383
  • 24
  • 34
  • any reason why you'd rather have `imeiNumber` bound to `string` rather than `int`? – Josh E May 29 '12 at 14:49
  • wasn't sure it would always be an int / long. Depends on database setup, but my gut feeling was that it was like a telephone number (more of a reference number, you aren't going to math over it) – Kaido May 29 '12 at 15:11
  • makes sense, just depends on whether you need to parse the string into a number type yourself or let the model binder do it, or however the OP intends to do the lookup. – Josh E May 29 '12 at 15:15
  • @Kaido I don't have any control over how the GET link is formed. – FloatLeft May 30 '12 at 06:29
  • 1
    Custom model binder is the way to go then I'd say – Kaido May 30 '12 at 09:52
  • @FloatLeft - are you able to change the HTML template (razor, I'd wager) at all? Can you change the DataAnnotations (the `[Remote(...)]` attribs) on your model? – Josh E May 30 '12 at 16:21
  • @Josh, Yes I can change the data annotations. Currently it is decorated with: [Remote("IsImeiAvailable", "Validation")] – FloatLeft May 30 '12 at 20:06
  • @FloatLeft - did you see the update to my answer? Can you move the annotation from `Models.ViewModels.ImeiGadget[]` to `ImeGadget.ImeiNumber`? I think your data annotation is in the wrong place – Josh E May 30 '12 at 23:05
  • @Josh - Attribute was already placed on the ImeiNumber property. Have updated my question to show the ImeiGadget class. – FloatLeft May 31 '12 at 11:32
0

Unless you need this binding feature in many places and you control the IsImeiAvailable validation method then I think creating a custom model binder is an over-head.

Why don't you try a simple solution like this,

// need little optimization?
public JsonResult IsImeiAvailable(string imeiNumber)
{
  var qParam = Request.QueryString.Keys
     .Cast<string>().FirstOrDefault(a => a.EndsWith("ImeiNumber"));

  return Json(!string.IsNullOrEmpty(imeiNumber ?? Request.QueryString[qParam]), JsonRequestBehavior.AllowGet);
}
VJAI
  • 32,167
  • 23
  • 102
  • 164
  • Mark - At the moment, the URL of the validation request looks like "/Validation/IsImeiAvailable?ImeiGadgets[1].ImeiNumber=123456789012345". Therefore the method you suggest would not be called as the signature differs. For your suggestion to work I'd need the validation URL to look like "/Validation/IsImeiAvailable?ImeiNumber=123456789012345" – FloatLeft May 31 '12 at 11:40
  • Also, I've already got in place a quick fix that just parses the URL: "string imeiNumber = Request.Url.AbsoluteUri.Substring(Request.Url.AbsoluteUri.IndexOf("=")+1);" – FloatLeft May 31 '12 at 11:41
  • I had tested the code for the URL you currently have and it worked. Quick fixes are fine in case of single-simple-peculiar-scenarios. – VJAI Jun 01 '12 at 16:33
0

You can add an extra hidden field with .Index suffix to allow arbitrary indices.

View:

<form method="post" action="/Home/Create">
    <input type="hidden" name="products.Index" value="cold" />
    <input type="text" name="products[cold].Name" value="Beer" />
    <input type="text" name="products[cold].Price" value="7.32" />

    <input type="hidden" name="products.Index" value="123" />
    <input type="text" name="products[123].Name" value="Chips" />
    <input type="text" name="products[123].Price" value="2.23" />

    <input type="hidden" name="products.Index" value="caliente" />
    <input type="text" name="products[caliente].Name" value="Salsa" />
    <input type="text" name="products[caliente].Price" value="1.23" />

    <input type="submit" />
</form>

Model:

public class Product{
    public string Name{get; set;}
    public decimal Price{get; set;}
}

Controller:

public ActionResult Create(Product[] Products)
{
    //do something..
}

For more information refer to : You've Been Haacked: Model Bindig To A List

electron
  • 801
  • 8
  • 18