How to create an ActionLink with Properties for the View Model
Asked Answered
P

2

6

I have a ViewModel with a Filter property that has many properties that I use to filter my data

Example:

class MyViewModel : IHasFilter
{
     public MyData[] Data { get; set; }
     public FilterViewModel Filter { get; set; }
}

class FilterViewModel
{
    public String MessageFilter { get; set; }
    //etc.
}

This works fine when using my View. I can set the properties of Model.Filter and they are passed to the Controller. What I am trying to do now, is create an ActionLink that has a query string that works with the above format.

The query string generated by my View from above looks like this:

http://localhost:51050/?Filter.MessageFilter=Stuff&Filter.OtherProp=MoreStuff

I need to generate an ActionLink in a different View for each row in a grid that goes to the View above.

I have tried:

Html.ActionLink(
    item.Message,
    "Index",
    "Home",
    new { Filter = new { MessageFilter = item.Message, }, },
    null);

I also tried setting the routeValues argument to:

new MyViewModel { Filter = new FilterViewModel { MessageFilter = item.Message, }, },

But these do not generate the query string like the above one.

Profuse answered 4/4, 2012 at 8:51 Comment(1)
thanks for the edit; I refactored the answer and in the replacement forgot to add the prefix!Lovett
L
1

You could create one RouteValueDictionary from a FilterViewModel instance and then use ToDictionary on that to pass to another RouteValues with all the keys prefixed with 'Filter.'.

Taking it further, you could construct a special override of RouteValueDictionary which accepts a prefix (therefore making it more useful for other scenarios):

public class PrefixedRouteValueDictionary : RouteValueDictionary
{
  public PrefixedRouteValueDictionary(string prefix, object o)
    : this(prefix, new RouteValueDictionary(o))
  { }

  public PrefixedRouteValueDictionary(string prefix, IDictionary<string, object> d)
    : base(d.ToDictionary(kvp=>(prefix ?? "") + kvp.Key, kvp => kvp.Value))
  { }
}

With that you can now do:

Html.ActionLink( 
  item.Message, 
  "Index", 
  "Home", 
  new PrefixedRouteValueDictionary("Filter.", 
    new FilterViewModel() { MessageFilter = item.Message }), 
  null); 

The caveat to this, though, is that the Add, Remove, TryGetValue and this[string key] methods aren't altered to take into account the prefix. That can be achieved by defining new versions of those methods, but because they're not virtual, they'd only work from callers that know they're talking to a PrefixedRouteValueDictionary instead of a RouteValueDictionary.

Lovett answered 4/4, 2012 at 12:58 Comment(1)
This was similar to my initial thought as well. One additional caveat is that you can only have one prefix per ActionLink method call and all items would have to have the prefix. I updated my answer with another possible solution depending on the exact needs and thought you may be interested. Cheers.Contortive
C
2

Interesting question (+1). I'm assuming that the purpose is to use the default model binder to bind the querystring parameters to to your Action parameters.

Out of the box I do not believe that the ActionLink method will do this for you (of course there is nothing stopping you from rolling your own). Looking in reflector we can see that when the object is added to the RouteValueDictionary, only key value pairs are added. This is the code that adds the key value pairs and as you can see there is no traversing the object properties.

foreach (PropertyDescriptor descriptor in TypeDescriptor.GetProperties(values))
{
    object obj2 = descriptor.GetValue(values);
    this.Add(descriptor.Name, obj2);
}

So for your object

var values = new { Filter = new Filter { MessageFilter = item.Message } }

the key being added is Filter and the value is your Filter object which will evaluate to the the fully qualified name of your object type.

The result of this is Filter=Youre.Namespace.Filter.

Edit possible solution depending on your exact needs


Extension Method does the work

Note that it uses the static framework methods ExpressionHelper and ModelMetadata (which are also used by the existing helpers) to determine the appropriate names that the default model binder will understand and value of the property respectively.

public static class ExtentionMethods
{
    public static MvcHtmlString ActionLink<TModel, TProperty>(
        this HtmlHelper<TModel> helper,
        string linkText,
        string actionName,
        string controllerName,
        params Expression<Func<TModel, TProperty>>[] expressions)
    {
        var urlHelper = new UrlHelper(helper.ViewContext.HttpContext.Request.RequestContext);

        var url = urlHelper.Action(actionName, controllerName);

        if (expressions.Any())
        {
            url += "?";

            foreach (var expression in expressions)
            {
                var result = ExpressionHelper.GetExpressionText(expression);

                var metadata = ModelMetadata.FromLambdaExpression<TModel, TProperty>(expression, helper.ViewData);

                url = string.Concat(url, result, "=", metadata.SimpleDisplayText, "&");
            }

            url = url.TrimEnd('&');
        }

        return new MvcHtmlString(string.Format("<a href='{0}'>{1}</a>", url, linkText));
    }
}

Sample Models

public class MyViewModel
{
    public string SomeProperty { get; set; }

    public FilterViewModel Filter { get; set; }
}

public class FilterViewModel
{
    public string MessageFilter { get; set; }
}

Action

public ActionResult YourAction(MyViewModel model)
{
    return this.View(
        new MyViewModel
        {
            SomeProperty = "property value",
            Filter = new FilterViewModel
            {
                MessageFilter = "stuff"
            }
        });
}

Usage

Any number of your view model properties can be added to the querystring through that last params parameter of the method.

@this.Html.ActionLink(
    "Your Link Text",
    "YourAction",
    "YourController",
    x => x.SomeProperty,
    x => x.Filter.MessageFilter)

Markup

<a href='/YourAction/YourController?SomeProperty=some property value&Filter.MessageFilter=stuff'>Your Link Text</a>

Instead of using string.Format you could use TagBuilder, the querystring should be encoded to be safely passed in a URL and this extension method would need some additional validation but I think it could be useful. Note also that, though this extension method is built for MVC 4, it could be easily modified for previous versions. I didn't realize that that one of the MVC tags was was for version 3 until now.

Contortive answered 4/4, 2012 at 8:51 Comment(3)
of course there is nothing stopping you from rolling your own - and how might I do that? :) And you are right, I was seeing the "Filter=Your.Nam...".Profuse
@Profuse though you may not agree, I feel as though this is a valuable answer to your question. cheers.Contortive
@Profuse was thinking about this on my way home from work and took a stab at it. This solution could be matured as you work with it but I think it could be useful.Contortive
L
1

You could create one RouteValueDictionary from a FilterViewModel instance and then use ToDictionary on that to pass to another RouteValues with all the keys prefixed with 'Filter.'.

Taking it further, you could construct a special override of RouteValueDictionary which accepts a prefix (therefore making it more useful for other scenarios):

public class PrefixedRouteValueDictionary : RouteValueDictionary
{
  public PrefixedRouteValueDictionary(string prefix, object o)
    : this(prefix, new RouteValueDictionary(o))
  { }

  public PrefixedRouteValueDictionary(string prefix, IDictionary<string, object> d)
    : base(d.ToDictionary(kvp=>(prefix ?? "") + kvp.Key, kvp => kvp.Value))
  { }
}

With that you can now do:

Html.ActionLink( 
  item.Message, 
  "Index", 
  "Home", 
  new PrefixedRouteValueDictionary("Filter.", 
    new FilterViewModel() { MessageFilter = item.Message }), 
  null); 

The caveat to this, though, is that the Add, Remove, TryGetValue and this[string key] methods aren't altered to take into account the prefix. That can be achieved by defining new versions of those methods, but because they're not virtual, they'd only work from callers that know they're talking to a PrefixedRouteValueDictionary instead of a RouteValueDictionary.

Lovett answered 4/4, 2012 at 12:58 Comment(1)
This was similar to my initial thought as well. One additional caveat is that you can only have one prefix per ActionLink method call and all items would have to have the prefix. I updated my answer with another possible solution depending on the exact needs and thought you may be interested. Cheers.Contortive

© 2022 - 2024 — McMap. All rights reserved.