HtmlHelper methods and RouteValueDictionary - asp.net-mvc

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.

Related

htmlAttributes in ActionLink Extension method MVC5

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.

How can I convert anonymous object HTML attributes to a Dictionary<string, object>

I'm providing an extra overload to RadioButtonFor and want to add a Key Value pair to the HTML Attributes that are passed in.
As an example I am passing in something like:
new { id = "someID" }
When i the use the HtmlHelper.AnonymousObjectToHtmlAttributes method as seems to be the suggestions I'm finding), its resulting in a dictionary with 4 items with Keys of "Comparer", "Count", "Keys", "Values". I then try to use Reflection to iterate over the values in both "Keys" and "Values", but cannot get that to work either.
Essentially all I want to do is to be able to cast the htmlAttributes to an IDictionary , add a item and then pass it on to a regular RadioButtonFor method.
Edit:
Heres what Im actually trying to do. Provide an overload called isDisabled to be able to set the disabled state of the radio button as this cant be easily done directly using HTML attributes because disabled = false stillr esults in disabled being rendered to tag and disables the radio.
public static MvcHtmlString RadioButtonFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object value, bool isDisabled, object htmlAttributes)
{
var linkAttributes = System.Web.Mvc.HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
Dictionary<string, object> htmlAttributesDictionary = new Dictionary<string, object>();
foreach (var a in linkAttributes)
{
if (a.Key.ToLower() != "disabled")
{
htmlAttributesDictionary.Add(a.Key, a.Value);
}
}
if (isDisabled)
{
htmlAttributesDictionary.Add("disabled", "disabled");
}
return InputExtensions.RadioButtonFor<TModel, TProperty>(htmlHelper, expression, value, htmlAttributesDictionary);
}
Looks like you might be applying the AnonymousObjectToHtmlAttributes either twice or to the wrong item.
Without more of your code, it's hard to tell
var attributes = HtmlHelper.AnonymousObjectToHtmlAttributes(new { id = "someID" });
attributes.Count = 1
attributes.Keys.First() = id
compared with
var attributes = HtmlHelper.AnonymousObjectToHtmlAttributes(HtmlHelper.AnonymousObjectToHtmlAttributes(new { id = "someID" }));
attributes.Count = 3
attributes.Keys.Join = Count,Keys,Values
When writing your overload, make sure your parameter is: object htmlAttributes for the new { } part with an overload with the IDictionary, eg:
Public static MvcHtmlString MyRadioButtonFor(..., object htmlAttributes)
{
return MyRadioButtonFor(...., HtmlHelper.AnonymousObjectToHtmlAttrbites(htmlAttributes);
}
public static MvcHtmlString MyRadioButtonFor(..., IDictionary<string, object> htmlAttributes)
{
htmlAttributes.Add("item", item);
return RadioButtonFor(..., htmlAttributes);
}
(just to be clear, never use My... - it's just for illustration)
Its unclear why you would not just use and existing overload that accepts object htmlAttributes to add the disabled="disabled" attribute, however the following should work
public static MvcHtmlString RadioButtonFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object value, bool isDisabled, object htmlAttributes)
{
IDictionary<string, object> attributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
if (isDisabled && !attributes.ContainsKey("disabled"))
{
attributes.Add("disabled", "disabled");
}
return InputExtensions.RadioButtonFor<TModel, TProperty>(htmlHelper, expression, value, attributes);
}

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(...);
}
}

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;
}

Return url by routename and routevalues

I'm trying to programatically generate a link by routename and routevalues. I store the routes and values in the database.
How can I adjust this helper in order to make it work?
public static string GenerateLink(this HtmlHelper helper, string routeName, Dictionary<string, string> parameters)
{
// ??
RouteValueDictionary rd = new RouteValueDictionary();
foreach (var item in parameters)
{
rd.Add(item.Key, item.Value);
}
// string url = ??
return url;
}
to use it like:
<%= Html.GenerateLink(Model.SomeLinkName, Model.RouteName, ..?) %>
/M
Actually this method already exists for you.
Html.RouteLink(string LinkText, string routeName, RoutevalueDictionary routeValues)
The only thing you need to do is turn your IDictionary into a RouteValueDictionary which again is quite simple as the constructor for a RVD can take an IDictionary which saves you from doing the foreach loop in your example.
So finally all you need is
Html.RouteLink(string LinkText, string routeName, new RoutevalueDictionary(parameters) )

Resources