Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Serialize IList property on model when passed into Html.ActionLink

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

like image 933
lomaxx Avatar asked Nov 24 '11 19:11

lomaxx


1 Answers

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.

like image 176
João Angelo Avatar answered Oct 30 '22 09:10

João Angelo