In ASP.NET MVC, how to generate repeated name ('?v=1&v=2&v=3') query strings with Html.ActionLink
This is NOT a question about how to correctly bind a series of check boxes to a model property (a common questi开发者_如何学运维on) - my site works perfectly reading checkbox values from a request, either POST or GET + query string.
This is about how to use Html.ActionLink
to generate a link that formats multiple checkbox values correctly in the query string.
So I have the following model:
public class ModelType
{
public string[] V { get; set; }
}
And I bind, say, 3 checkboxes to that model in the view as I have three possible values (and, yes, combinations of said values).
Here is the final Html
<INPUT id="chk1" value="1" type="checkbox" name="V">
<INPUT id="chk2" value="2" type="checkbox" name="V">
<INPUT id="chk3" value="3" type="checkbox" name="V">
If all three values are checked when the form is submitted to the server as a GET then the generated query string is, of course, ?V=1&V=2&V=3
.
Model binding works beautifully, and everything is happy.
However, let's say I want to produce a link to the same action, passing either an anonymous type or RouteValueDictionary
to produce the same query string; logic states that you would do something like this:
Version 1: With an instance of ModelType
as the model on the view:
<%= Html.ActionLink("Test link", null /* action name */, new { V = Model.V }) %>
Version 2: Initialising the 'V' member directly as an array:
<%= Html.ActionLink("Test link", null /* action name */,
new { V = new string[] { "1", "2", "3" } }) %>
[In the question I say RouteValueDictionary
because in both cases both of these anonymous types are getting turned into such before link generation occurs.]
In both cases the query string that is produced is as follows: ?V=System.String%5B%5D
.
Now, I understand that this is because the link generator is simply calling ToString()
on the array; but it would seem that there is no way to pass MVC a value that will cause it to generate ?V=1&V=2&V=3
.
Since neither the language lets us do:
new { V="1", V="2", ... }
Nor RouteValueDictionary lets us do:
d["V"] = "1"; d["V"] = "2"; ...
I've also tried the desperate measure of pre-processing a RouteValueDictionary, so that for each IEnumerable<string>
, I produce a single string with each value separated by a comma, and then write that string back - so the query string ends up like this: ?V=1%2C2%2C3
.
But of course MVC doesn't automatically bind such a string to an array (you just get one string in the array with the commas in it); so each property that will be bound in this way has to be custom bound - which, for me, just seems like a few too many hoops to be jumping through.
So it seems that I'm stuck.
Am I missing something? Will I need to write special versions of ActionLink that do what I want?
Any help, as always, greatly appreciated.
So I've gone and researched this and this is my view. I hope somebody can come up with a more optimistic answer, but somehow I think not.
Following the Html.ActionLink
extension method all the way through, the core URL that it produces is generated by the (cough)sealed and (cough cough!)internal class System.Web.Mvc.ParsedRoute
and it's method Bind
in the System.Web.Routing
assembly.
At the very end of this method (this is a Reflector listing) - this is how it generates the query string:
if (unusedNewValues.Count > 0)
{
bool flag5 = true;
foreach (string str2 in unusedNewValues)
{
object obj5;
if (acceptedValues.TryGetValue(str2, out obj5))
{
builder.Append(flag5 ? '?' : '&');
flag5 = false;
builder.Append(Uri.EscapeDataString(str2));
builder.Append('=');
builder.Append(Uri.EscapeDataString(
Convert.ToString(obj5, CultureInfo.InvariantCulture)));
}
}
}
I should mention first that unusedNewValues
is a HashSet<string>
which means - unfortunately - that it will only ever render one name/value pairing.
You can't fudge the data string with embedded a=b&a=c(...)
strings because the string value is escaped - thus it is not possible to generate such query strings in Mvc at all unless you replace the routing framework.
As a result, an alternative will be to write a collection data type which does something like the following (this is a dirty implementation):
public class StringCollection : List<string>
{
public override string ToString()
{
//use the pipe character as a delimiter - but this doesn't work
//if the strings being carried around ccould naturally contain '|'!
return string.Join("|", this.ToArray());
}
}
And then implement a custom model binder (deriving from DefaultModelBinder
) for the type that first uses the default method and then checks to see if the resulting collection only has one string which has '|' characters in it. If so, it then post-processes the collection, replacing its contents with the expanded values from the single string.
This is horrid - but the least nasty of all the solutions I can currently think of.
Just now have to make a load of changes to all my existing model types that use collections of strings that might be bound to query string values...
Had a similar problem recently. I ended up ditching the HtmlHelper/RouteValueDictionary and just constructed my URL from scratch. Something like:
<a href="/Controller/Action?<%: string.Join("&", set.Select(i => "v=" + i))%>">Link</a>
May not appeal to everyone's tastes, but it worked for me.
精彩评论