6

I'm trying to generate an Html.ActionLink with the following viewmodel:

public class SearchModel
{
    public string KeyWords {get;set;}
    public IList<string> Categories {get;set;}
}

To generate my link I use the following call:

@Html.ActionLink("Index", "Search", Model)

Where Model is an instance of the SearchModel

The link generated is something like this:

http://www.test.com/search/index?keywords=bla&categories=System.Collections.Generic.List

Because it obviously is only calling the ToString method on every property.

What I would like to see generate is this:

http://www.test.com/search/index?keywords=bla&categories=Cat1&categories=Cat2

Is there any way I can achieve this by using Html.ActionLink

lomaxx
  • 113,627
  • 57
  • 144
  • 179
  • Looks like a duplicate: http://stackoverflow.com/q/1752721/25727. See the first answer for a solution with a custom HtmlHelper. – Jan Nov 24 '11 at 19:13

2 Answers2

2

In MVC 3 you're just out of luck because the route values are stored in a RouteValueDictionary that as the name implies uses a Dictionary internally which makes it not possible to have multiple values associated to a single key. The route values should probably be stored in a NameValueCollection to support the same behavior as the query string.

However, if you can impose some constraints on the categories names and you're able to support a query string in the format:

http://www.test.com/search/index?keywords=bla&categories=Cat1|Cat2

then you could theoretically plug it into Html.ActionLink since MVC uses TypeDescriptor which in turn is extensible at runtime. The following code is presented to demonstrate it's possible, but I would not recommend it to be used, at least without further refactoring.

Having said that, you would need to start by associating a custom type description provider:

[TypeDescriptionProvider(typeof(SearchModelTypeDescriptionProvider))]
public class SearchModel
{
    public string KeyWords { get; set; }
    public IList<string> Categories { get; set; }
}

The implementation for the provider and the custom descriptor that overrides the property descriptor for the Categories property:

class SearchModelTypeDescriptionProvider : TypeDescriptionProvider
{
    public override ICustomTypeDescriptor GetTypeDescriptor(
        Type objectType, object instance)
    {
        var searchModel = instance as SearchModel;
        if (searchModel != null)
        {
            var properties = new List<PropertyDescriptor>();

            properties.Add(TypeDescriptor.CreateProperty(
                objectType, "KeyWords", typeof(string)));
            properties.Add(new ListPropertyDescriptor("Categories"));

            return new SearchModelTypeDescriptor(properties.ToArray());
        }
        return base.GetTypeDescriptor(objectType, instance);
    }
}
class SearchModelTypeDescriptor : CustomTypeDescriptor
{
    public SearchModelTypeDescriptor(PropertyDescriptor[] properties)
    {
        this.Properties = properties;
    }
    public PropertyDescriptor[] Properties { get; set; }
    public override PropertyDescriptorCollection GetProperties()
    {
        return new PropertyDescriptorCollection(this.Properties);
    }
}

Then we would need the custom property descriptor to be able to return a custom value in GetValue which is called internally by MVC:

class ListPropertyDescriptor : PropertyDescriptor
{
    public ListPropertyDescriptor(string name)
        : base(name, new Attribute[] { }) { }

    public override bool CanResetValue(object component)
    {
        return false;
    }
    public override Type ComponentType
    {
        get { throw new NotImplementedException(); }
    }
    public override object GetValue(object component)
    {
        var property = component.GetType().GetProperty(this.Name);
        var list = (IList<string>)property.GetValue(component, null);
        return string.Join("|", list);
    }
    public override bool IsReadOnly { get { return false; } }
    public override Type PropertyType
    {
        get { throw new NotImplementedException(); }
    }
    public override void ResetValue(object component) { }
    public override void SetValue(object component, object value) { }
    public override bool ShouldSerializeValue(object component)
    {
        throw new NotImplementedException();
    }
}

And finally to prove that it works a sample application that mimics the MVC route values creation:

static void Main(string[] args)
{
    var model = new SearchModel { KeyWords = "overengineering" };

    model.Categories = new List<string> { "1", "2", "3" };

    var properties = TypeDescriptor.GetProperties(model);

    var dictionary = new Dictionary<string, object>();
    foreach (PropertyDescriptor p in properties)
    {
        dictionary.Add(p.Name, p.GetValue(model));
    }

    // Prints: KeyWords, Categories
    Console.WriteLine(string.Join(", ", dictionary.Keys));
    // Prints: overengineering, 1|2|3
    Console.WriteLine(string.Join(", ", dictionary.Values));
}

Damn, this is probably the longest answer I ever give here at SO.

João Angelo
  • 56,552
  • 12
  • 145
  • 147
  • Thanks for this answer. It gets pretty close to getting it to spit out the right values, it's just that if you use | as the delimiter you have to use a custom model binder on the way back in, so I changed my version so the delimiter was string.Format("&{0}", property.Name)... works like a charm! – lomaxx Nov 25 '11 at 03:07
  • 2
    the word "overengineering" really have its place here :) this is ridiculously complex solution for something solvable with a line of linq :) but nice proof of concept.. – rouen Nov 25 '11 at 08:07
  • @rouen it's not overengineering and it's not actually solvable by linq. The joining of string is, but that isn't the whole problem, it's only a part of the problem. The other part is getting it to be used as a first class citizen in the routing model. That is what Joao is trying to demonstrate – lomaxx Dec 01 '11 at 23:43
0

with linq of course...

string.Join("", Model.Categories.Select(c=>"&categories="+c))
rouen
  • 5,003
  • 2
  • 25
  • 48