MVC Model Binding to a collection where collection does not begin with a 0 index
Asked Answered
P

5

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; }
}
Prady answered 25/5, 2012 at 19:3 Comment(6)
Is this aspx? If so, please retagNicolettenicoli
fixed the tagging -- assumed aspnet-mvc when I saw the [Bind] attrAnacrusis
Are you posting via an Ajax request? If so could we see that code? I have a hunch :-)Masbate
This is an MVC3 question. Did I tag it wrongly?Prady
I am posting via an Ajax request which is handled by the Remote Validation feature of MVC3Prady
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 solveAnacrusis
P
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.

Poll answered 29/5, 2012 at 14:53 Comment(1)
this seems to be the most relevant answer. Will have a go at this when I get to workPrady
A
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.

Anacrusis answered 29/5, 2012 at 13:33 Comment(5)
would be IsImeiAvailable(int id) using the default routesKumasi
BTW, my assumption is that all the OP is needing is that int ID, judging from /Validation/IsImeiAvailable?ImeiGadgets[1].ImeiNumber=123456789012345Anacrusis
Remember I'm using the Remote Validation feature of MVC3. This means the ID of the input and validation conform to certain naming rules: <input class="inputboxmedium idleField" data-val="true" data-val-regex="An IMEI number must contain 15 digits" data-val-regex-pattern="(\D*\d){15,17}" data-val-remote="&amp;#39;ImeiNumber&amp;#39; is invalid." data-val-remote-additionalfields="*.ImeiNumber" data-val-remote-url="/Validation/IsImeiAvailable" data-val-required="Please provide the IMEI Number for your Phone" id="ImeiGadgets_0__ImeiNumber" name="ImeiGadgets[0].ImeiNumber" type="text" value="">Prady
@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.Prady
@Josh: The validation attribute is on the ImeiNumber property already.Prady
K
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)
Kumasi answered 29/5, 2012 at 14:11 Comment(9)
any reason why you'd rather have imeiNumber bound to string rather than int?Anacrusis
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)Kumasi
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.Anacrusis
@Kumasi I don't have any control over how the GET link is formed.Prady
Custom model binder is the way to go then I'd sayKumasi
@Prady - 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?Anacrusis
@Josh, Yes I can change the data annotations. Currently it is decorated with: [Remote("IsImeiAvailable", "Validation")]Prady
@Prady - 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 placeAnacrusis
@Josh - Attribute was already placed on the ImeiNumber property. Have updated my question to show the ImeiGadget class.Prady
I
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);
}
Impulsion answered 31/5, 2012 at 6:15 Comment(3)
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"Prady
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);"Prady
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.Impulsion
A
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

Army answered 27/2, 2020 at 8:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.