htmlAttributes in ActionLink Extension method MVC5 - asp.net-mvc

I'm using an extension method to maintain a css class on active links on the menu.
However I've got an issue where the htmlAttributes and the object values are causing errors.
I have the below in my Razor page but I don't understand how I'm meant to be parsing the htmlAttributes.
#Html.MenuLink("Summary", "Summary", "Graphs", null, new { #class = "dropdown-toggle caret", data_target = "#", data_toggle = "dropdown" })
From looking at the HtmlHelper the method should have IDictionary<object, string> as the type for the htmlAttributes. The new { #class = "dropdown-toggle caret", data_target = "#", data_toggle = "dropdown" } syntax isn't typical for dictionaries so is this correct?
Obviously I'm doing something wrong as it's returning the below error:
Argument 6: cannot convert from '<anonymous type: string class, string data_target, string data_toggle>' to 'System.Collections.Generic.IDictionary<object, string>'
Extension method I'm trying to get working below:
public static MvcHtmlString MenuLink(this HtmlHelper htmlHelper, string text, string action, string controller, RouteValueDictionary routeValues, IDictionary<object, string> htmlAttributes)
{
var routeData = htmlHelper.ViewContext.RouteData.Values;
var currentController = routeData["controller"];
var currentAction = routeData["action"];
if (string.Equals(action, currentAction as string, StringComparison.OrdinalIgnoreCase) &&
string.Equals(controller, currentController as string, StringComparison.OrdinalIgnoreCase))
{
return htmlHelper.ActionLink(text, action, controller, null, new { #class = "currentMenu" });
}
return htmlHelper.ActionLink(text, action, controller);
}

Change the parameter from IDictionary<object, string> htmlAttributes to object htmlAttributes since your passing the attributes as an object.
You can then convert the object using
var attributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
However, no where in your extension method do you ever use the attributes. All your generating is class = "currentMenu" depending on the current controller and action names. If you intention is to add the attributes plus the class name (depending on the condition), you can then use
attributes.Add("class", "currentMenu");
Your complete method to allow defining both route values and html attributes, and to conditionally include the "currentMenu" class name should be
public static MvcHtmlString MenuLink(this HtmlHelper htmlHelper, string text, string action, string controller, object routeValues, object htmlAttributes)
{
var routeData = htmlHelper.ViewContext.RouteData.Values;
string currentController = (string)routeData["controller"];
string currentAction = (string)routeData["action"];
if (string.Equals(action, currentAction, StringComparison.OrdinalIgnoreCase) && string.Equals(controller, currentController, StringComparison.OrdinalIgnoreCase))
{
if (htmlAttributes == null)
{
return htmlHelper.ActionLink(text, action, controller, routeValues, new { #class = "currentMenu" });
}
else
{
// convert object to RouteValueDictionary
var attributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
if (attributes.ContainsKey("class"))
{
// append the class name
attributes["class"] = string.Format("{0} currentMenu", attributes["class"]);
}
else
{
// add the class name
attributes.Add("class", "currentMenu");
}
return htmlHelper.ActionLink(text, action, controller, new RouteValueDictionary(routeValues), attributes);
}
}
return htmlHelper.ActionLink(text, action, controller, routeValues, htmlAttributes);
}
Side note: You should also consider including other overloads to accept RouteValueDictionary routeValues and IDictionary<String, Object>) htmlAttributes as per the in-built ActionLink() methods and you can inspect the source code to see how the various overloads fall through to the other overloads.

Related

How to enable the addition of an extra parameter to URL.Action via an Extension method

I am trying to understand how I can add an extra parameter to URL.Action, and have it as part of the resultant link.
Lets assume the following:
myParm = "myTestParameterValue";
#Url.Action("Edit", "Order", new { id=item.Id}, null,myParm)
which would result in:
/Order/Edit/1/myTestParameterValue
I would really appreciate some sample code of the extension method for this Action Sample to see how the parameters are taken in and how the link is generated.
I guess it would start something like:
public static MvcHtmlString Action(this HtmlHelper helper, string actionName, string controllerName, object routeValues, boolean IsHashRequired)
If (IsHashRequired)
{
String myHash = GetHash();
}
// Pseudocode .... string myNewLink = ... + myHash
Many thanks in advance
EDIT
I need to calculate hash to add to resultant link. A better parameter would be a boolean. I have edited code accordingly.
EDIT2:
public static IHtmlString Action(this UrlHelper urlHelper, string actionName, string controllerName, object routeValues, string protocol, bool isHashRequired )
{
if (isHashRequired)
{
routeValues["hash"] = "dskjdfhdksjhgkdj"; //Sample value.
}
return urlHelper.Action(???); // Resultant URL = /Order/Edit/1/dskjdfhdksjhgkdj
}
EDIT3:
Struggling with :
return urlHelper.Action(actionName, controllerName, routeValues, protocol);
Apparently needs converting to IHtmlString??
EDIT4:
public static String Action(this UrlHelper urlHelper, string actionName, string controllerName, object routeValues, string protocol, bool isHashRequired )
{
RouteValueDictionary rvd = new RouteValueDictionary(routeValues);
if (isHashRequired)
{
string token = "FDSKGLJDS";
rvd.Add("urltoken", token);
}
return urlHelper.Action(actionName, controllerName, rvd, protocol); //rvd is incorrect I believe
}
EDIT5
return urlHelper.Action(actionName, controllerName, rvd, protocol,null);
where
rvd is the RouteValueDictionary
hostname is null.
Thanks...
You should consider modifying your routes
Where you have your routing configured add something like this:
routes.MapRoute(
"hash", // Route name
"{controller}/{action}/{id}/{hash}", // URL with parameters
new { controller = "Home", action = "Index", id = "", hash = "" } // Parameter defaults
);
And use URL.Action like this:
myParm = "myTestParameterValue";
#Url.Action("Edit", "Order", new { id=item.Id, hash = myParm}, null);
You can easily add this with a new extension method class
public static class MyExtensions
{
public static IHtmlString ActionWithHash(this UrlHelper urlHelper, ....)
{
if (hashRequired)
{
routeParameters["hash"] = ...
}
return urlHelper.Action(...);
}
}

How do I remove the link text with #html.actionlink

I am trying to add a css class to a #html.actionlink and do not want to use the text that does in the link. I want to use a graphic instead.
Here is my code:
#Html.ActionLink("Edit", "PopupReferenceEdit", new { id = item.VolunteerReferenceID }, new { #class = "Grid-editor" })
When I delete the"Edit" I get an error. Is it possible to use this statement and have an icon/image for the link?
Thanks for answers to this newbie question.
Andy
i think that a more nicer approach for this would be to create an extension method for it with these overloads in your helper folder and then use it in your views. just depends upon the personal preference
public static class ImageActionLinkHelper
{
public static string ImageActionLink(this HtmlHelper helper, string ImageUrl, string altText, string actionName, object routeValues)
{
var builder = new TagBuilder("img");
builder.MergeAttribute("src", ImageUrl);
builder.MergeAttribute("alt", altText);
builder.MergeAttribute("title", altText);
var link = helper.ActionLink("[replaceme]", actionName, routeValues, new { #class = "imgicon" });
return link.ToString().Replace("[replaceme]", builder.ToString(TagRenderMode.SelfClosing));
}
public static string ImageActionLink(this HtmlHelper helper, string ImageUrl, string altText, string actionName, object routeValues, string Id, string display)
{
var builder = new TagBuilder("img");
builder.MergeAttribute("src", ImageUrl);
builder.MergeAttribute("alt", altText);
builder.MergeAttribute("title", altText);
var link = helper.ActionLink("[replaceme]", actionName, routeValues, new { #class = "imgicon", id = Id, style = display });
return link.ToString().Replace("[replaceme]", builder.ToString(TagRenderMode.SelfClosing));
}
using it
#Html.ImageActionLink("../../Content/images/edit.png", "Edit", "Edit", new { id = item.UserId})

How to create an ActionLink with Properties for the View Model

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.
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.
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.

Variation on Adding HTML Attributes to Html.BeginForm()

I need a form on my ASP.NET MVC Razor page. My preference would be to use the following syntax:
#using (Html.BeginForm())
{
}
However, I need several attributes added to the form. So I ended up with something like the following:
#using (Html.BeginForm(null, null, FormMethod.Post, new { name = "value" }))
{
}
However, this has an undesired side effect. If there are query arguments in this page's request, the first form passes them along when the form is submitted. However, the second version does not.
I really don't know why BeginForm() doesn't support attributes, but is there a straight-forward way to add attributes to BeginForm() and still pass along any query arguments when the for is submitted?
EDIT:
After looking into this, it would seem the best solution is something like this:
<form action="#Request.RawUrl" method="post" name="value">
</form>
However, when using this syntax, client-side validation is disabled. It seems there is no good solution to this situation without more complicated and potentially unreliable constructs.
That's indeed true, but I would go with a custom helper in order to preserve the form context inside which is used for client side validation:
public static class FormExtensions
{
private static object _lastFormNumKey = new object();
public static IDisposable BeginForm(this HtmlHelper htmlHelper, object htmlAttributes)
{
string rawUrl = htmlHelper.ViewContext.HttpContext.Request.RawUrl;
return htmlHelper.FormHelper(rawUrl, FormMethod.Post, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
}
private static int IncrementFormCount(IDictionary items)
{
object obj2 = items[_lastFormNumKey];
int num = (obj2 != null) ? (((int)obj2) + 1) : 0;
items[_lastFormNumKey] = num;
return num;
}
private static string DefaultFormIdGenerator(this HtmlHelper htmlhelper)
{
int num = IncrementFormCount(htmlhelper.ViewContext.HttpContext.Items);
return string.Format(CultureInfo.InvariantCulture, "form{0}", new object[] { num });
}
private static IDisposable FormHelper(this HtmlHelper htmlHelper, string formAction, FormMethod method, IDictionary<string, object> htmlAttributes)
{
var builder = new TagBuilder("form");
builder.MergeAttributes<string, object>(htmlAttributes);
builder.MergeAttribute("action", formAction);
builder.MergeAttribute("method", HtmlHelper.GetFormMethodString(method), true);
bool flag = htmlHelper.ViewContext.ClientValidationEnabled && !htmlHelper.ViewContext.UnobtrusiveJavaScriptEnabled;
if (flag)
{
builder.GenerateId(htmlHelper.DefaultFormIdGenerator());
}
htmlHelper.ViewContext.Writer.Write(builder.ToString(TagRenderMode.StartTag));
var form = new MvcForm(htmlHelper.ViewContext);
if (flag)
{
htmlHelper.ViewContext.FormContext.FormId = builder.Attributes["id"];
}
return form;
}
}
which could be used like this:
#using (Html.BeginForm(htmlAttributes: new { name = "value" }))
{
...
}
I had a similar problem and here is quick solution (it works with MVC4).
Declare the extension method:
public static MvcForm BeginForm(this HtmlHelper helper, object htmlAttributes)
{
return helper.BeginForm(helper.ViewContext.RouteData.Values["Action"].ToString(),
helper.ViewContext.RouteData.Values["Controller"].ToString(),
FormMethod.Post, htmlAttributes);
}
and use it in your page:
#using (Html.BeginForm(htmlAttributes: new {#class="form-horizontal"}))
{
...
}
Small modification to source code:
http://aspnetwebstack.codeplex.com/SourceControl/latest#src/System.Web.Mvc/Html/FormExtensions.cs
public static MvcForm BeginForm(this HtmlHelper htmlHelper, object htmlAttributes)
{
// generates <form action="{current url}" method="post">...</form>
string formAction = htmlHelper.ViewContext.HttpContext.Request.RawUrl;
return FormHelper(htmlHelper, formAction, FormMethod.Post, new RouteValueDictionary(htmlAttributes));
}
private static MvcForm FormHelper(this HtmlHelper htmlHelper, string formAction, FormMethod method, IDictionary<string, object> htmlAttributes)
{
TagBuilder tagBuilder = new TagBuilder("form");
tagBuilder.MergeAttributes(htmlAttributes);
// action is implicitly generated, so htmlAttributes take precedence.
tagBuilder.MergeAttribute("action", formAction);
// method is an explicit parameter, so it takes precedence over the htmlAttributes.
tagBuilder.MergeAttribute("method", HtmlHelper.GetFormMethodString(method), true);
bool traditionalJavascriptEnabled = htmlHelper.ViewContext.ClientValidationEnabled
&& !htmlHelper.ViewContext.UnobtrusiveJavaScriptEnabled;
if (traditionalJavascriptEnabled)
{
// forms must have an ID for client validation
tagBuilder.GenerateId(htmlHelper.ViewContext.FormIdGenerator());
}
htmlHelper.ViewContext.Writer.Write(tagBuilder.ToString(TagRenderMode.StartTag));
MvcForm theForm = new MvcForm(htmlHelper.ViewContext);
if (traditionalJavascriptEnabled)
{
htmlHelper.ViewContext.FormContext.FormId = tagBuilder.Attributes["id"];
}
return theForm;
}

HtmlHelper methods and RouteValueDictionary

When writing an htmlhelper extension if I want to support the similarly structured ctors for my htmlhelper extension method, I use RouteValueDictionary as follows:
public static string ListBoxDict(this HtmlHelper htmlHelper,
string name,
object value,
object htmlAttributes)
{
return ListBoxDict(htmlHelper,
name,
value,
((IDictionary<string, object>)
new RouteValueDictionary(htmlAttributes)));
}
My question really is why the need for RouteValueDictionary ... I know you can't just cast the htmlAttributes to IDictionary<string, object> ... though I'm not sure why and that might be where I'm confused. Shouldn't RouteValueDictionary be to do with Routing and therefore nothing to do with HtmlHelper methods? Like I say, I'm probably missing the point so I'd be glad if someone could tell me what I've missed.
Cheers...
edit: in response to Dan's answer -->
I was just following what I had seen in use in the mvc source code for input helpers...
see "src\SystemWebMvc\Mvc\Html\InputExtensions.cs"
It does as follows:
public static string TextBox(this HtmlHelper htmlHelper,
string name,
object value,
object htmlAttributes)
{
return TextBox(htmlHelper,
name,
value,
new RouteValueDictionary(htmlAttributes))
}
Clearly a shortcut but is it a bastardization or is it ok to do it?
I would strongly recommend looking at Rob Conery's blog post about something like this.
The meat and veg of it is this:
Codedump:
public static string ToAttributeList(this object list)
{
StringBuilder sb = new StringBuilder();
if (list != null)
{
Hashtable attributeHash = GetPropertyHash(list);
string resultFormat = "{0}=\"{1}\" ";
foreach (string attribute in attributeHash.Keys)
{
sb.AppendFormat(resultFormat, attribute.Replace("_", ""),
attributeHash[attribute]);
}
}
return sb.ToString();
}
public static string ToAttributeList(this object list,
params object[] ignoreList)
{
Hashtable attributeHash = GetPropertyHash(list);
string resultFormat = "{0}=\"{1}\" ";
StringBuilder sb = new StringBuilder();
foreach (string attribute in attributeHash.Keys)
{
if (!ignoreList.Contains(attribute))
{
sb.AppendFormat(resultFormat, attribute,
attributeHash[attribute]);
}
}
return sb.ToString();
}
public static Hashtable GetPropertyHash(object properties)
{
Hashtable values = null;
if (properties != null)
{
values = new Hashtable();
PropertyDescriptorCollection props =
TypeDescriptor.GetProperties(properties);
foreach (PropertyDescriptor prop in props)
{
values.Add(prop.Name, prop.GetValue(properties));
}
}
return values;
}
Usage:
public static string ListBoxDict(this HtmlHelper htmlHelper,
string name,
object value,
object htmlAttributes)
{
return htmlHelper.ListBoxDict(name,
value,
htmlAttributes.ToAttributeList()));
}
What .ToAttributeList() does is convert your htmlAttribute object to
name = "value"
Hope this makes sense.

Resources