MVC3 - merging htmlAttributes set in View with ones set in Helper - asp.net-mvc-3

I'm using Disable autocomplete on html helper textbox in MVC to add autocomplete to "off" on text boxes:
public static MvcHtmlString NoAutoCompleteTextBoxFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, object htmlAttributes)
{
return html.TextBoxFor(expression, new { autocomplete = "off" });
}
This is fine if there are no htmlAttributes on the view. Some of the textfields also have an extra CSS class added to them:
#Html.NoAutoCompleteTextBoxFor(m => m.UserName, new { #class = "ignore" })
Rather than specifically create another helper for this, is there a way to merge the sets of htmlAttributes?
Thanks in advance...

You can use RouteValueDictionary.
var attrs = new RouteValueDictionary(htmlAttributes);
attrs ["autocomplete"] = "off";
return html.TextBoxFor(expression, attrs);

Yes of cause. When passing an object into the helper, you can use reflection to read all the properties (which correspond to the html attributes you want to set) and pass the combined attributes as Dictionary to the helper:
Dictionary<string, object> attrs = new Dictionary<string, object>();
if (htmlAttributes != null)
{
foreach (var prop in htmlAttributes.GetType().GetProperties())
{
attrs[prop.Name] = prop.GetValue(htmlAttributes, null);
}
}
// now add your own attribute(s):
htmlAttributes["autocomplete"] = "off";
return html.TextBoxFor(expression, htmlAttributes);

Related

Extending MVC RadioButtonFor using HtmlHelper extensions

I'm trying to build an HtmlHelper to add functionality to an MVC RadioButtonFor control. I simply want to add a class and style attribute to the radio button given certain conditions. I can't seem to get my extension to include attributes. I've followed several examples I've found online, but no luck.
Here is my code:
public static MvcHtmlString GCSRadioButtonForHelper<TModel>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, bool?>> expression,
Object htmlAttributes,
object value )
{
bool isRequired = false;
bool isHidden = false;
object styleList;
object classList;
Dictionary<string, object> attributes = new Dictionary<string, object>();
attributes.TryGetValue("class", out classList);
attributes.TryGetValue("style", out styleList);
ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
var categoryName = string.Empty;
if (metadata.ContainerType != null)
categoryName = metadata.ContainerType.Name + "." + metadata.PropertyName;
isHidden = GetIsHidden(metadata);
isRequired = GetIsRequired(metadata);
if (isHidden)
classList = classList + " GCSHidden ";
if (isRequired)
{
classList = classList + " GCSRequired ";
}
attributes.Remove("class");
attributes.Remove("style");
attributes.Add("class", classList);
attributes.Add("style", styleList);
//Render HTML String
MvcHtmlString html = System.Web.Mvc.Html.InputExtensions.RadioButtonFor(htmlHelper, expression, attributes, value);
return html;
}
The problem is that the result of InputExtensions.RadioButtonFor() never includes the attributes class or style as I have specified. Any ideas?
TIA

Dynamically set the disabled html attribute for TextBoxFor HtmlHelper

I'm trying to dynamically set the disabled attribute for the TextBoxFor HtmlHelper
#Html.TextBoxFor(model => model.Street,
new
{
#class = "",
disabled = (Model.StageID==(int)MyEnum.Sth) ? "disabled" : ""
})
but even if there is disabled="" it is the same as disabled="disabled". How to get around of this ?
I have the same problem about month ago and I finished by using this extension method for it
public static class AttributesExtensions
{
public static RouteValueDictionary DisabledIf(
this object htmlAttributes,
bool disabled
)
{
var attributes = new RouteValueDictionary(htmlAttributes);
if (disabled)
{
attributes["disabled"] = "disabled";
}
return attributes;
}
}
And after that you can use it like this
#Html.TextBoxFor(
model => model.Street,
new { #class = "" }.DisabledIf(Model.StageID==(int)MyEnum.Sth)
)
EDIT (after Paul's comment):
The using of data-xxx html attributes may be mined by using the constructor of the System.Web.Routing.RouteValueDictionary class, since underscores will not be automatically converted to minus sign.
Use the method System.Web.Mvc.HtmlHelper.AnonymousObjectToHtmlAttributes instead: will solve this issue.
UPDATED CODE (Extension method body only)
var attributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
if (disabled)
{
attributes["disabled"] = "disabled";
}
return attributes;
Using the extension method below produces similar results, but this is perhaps more fragile:
#Html.TextBoxFor(
model => model.Street,
new { #class = "form-control" }
).DisabledIf(Model.IsReadOnly)
Extension:
using System.Text.RegularExpressions;
using System.Web.Mvc;
namespace xxx.HtmlHelpers
{
public static class MvcHtmlStringExtensions
{
private static readonly Regex OpeningTagPattern;
static MvcHtmlStringExtensions()
{
OpeningTagPattern = new Regex("<[a-zA-Z]*");
}
public static MvcHtmlString DisabledIf(this MvcHtmlString controlHtml, bool isDisabled)
{
if (!isDisabled) return controlHtml;
return
new MvcHtmlString(OpeningTagPattern.Replace(controlHtml.ToString(),
x => string.Format("{0} disabled=\"disabled\"", x.Groups[0])));
}
}
}
Probably your stage Id is not getting set
#{
if(Model.StageID != null && Model.StageID > 0)
{
#Html.TextBoxFor(model => model.Street,
new
{
#class = "",
disabled = (Model.StageID==(int)MyEnum.Sth) ? "disabled" : ""
})
}else{
#Html.TextBoxFor(model => model.Street,
new
{
#class = ""
})
}
}
We actually just ran into the same problem. We ended up implementing an extension method with overloaded parameters, which takes in a boolean indicating whether or not we want the control disabled. We just add the "disabled" attribute when appropriate, and let the built-in HtmlHelper handle the heavy lifting.
Extension class and method:
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Web.Mvc;
using System.Web.Mvc.Html;
using System.Web.Routing;
public static class OurHtmlHelpers
{
public const string DisabledAttribute = "disabled";
public static MvcHtmlString TextBoxFor<TModel, TProp>(this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProp>> expression,
object htmlAttributes,
bool canEdit)
{
var htmlAttributeDictionary = SetDisabledAttribute(htmlAttributes, canEdit);
return htmlHelper.TextBoxFor(expression, htmlAttributeDictionary);
}
private static RouteValueDictionary SetDisabledAttribute(object htmlAttributes, bool canEdit)
{
var htmlAttributeDictionary = new RouteValueDictionary(htmlAttributes);
if (!canEdit)
{
htmlAttributeDictionary.Add(DisabledAttribute, DisabledAttribute);
}
return htmlAttributeDictionary;
}
}
Then you just need to reference your new class and call #Html.TextBoxFor(m => m.SomeValue, new { #class = "someClass" }, <Your bool value>)
It's worth noting that you'd have to define these extensions for any of the TextBoxFor overloads you'd like to use, but it seems like a reasonable trade off. You can also utilize most of the same code for other HtmlHelpers you'd like to add the functionality to.

Any way to style text contained in DataAnnotation Attribute [Display(Name = "Text")]?

I want to do something like this:
[Display(Name = "Plain text. <span class=\"red strong\">Red text bolded.</span>")]
Is this possible (to style the text within the Display Attribute)? Currently it is just displaying the literal text.
Is this possible (to style the text within the Display Attribute)?
The problem doesn't lie within the [Display] attribute. It lies within the Html.LabelFor helper which you used to display. This attribute always HTML encodes the value. If you don't like this behavior you could write a custom helper that will not encode the value:
public static class HtmlExtensions
{
public static IHtmlString MyLabelFor<TModel, TProperty>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression
)
{
var metadata = ModelMetadata.FromLambdaExpression<TModel, TProperty>(expression, htmlHelper.ViewData);
var htmlFieldName = ExpressionHelper.GetExpressionText(expression);
var labelText = (metadata.DisplayName ?? (metadata.PropertyName ?? htmlFieldName.Split(new char[] { '.' }).Last<string>()));
if (string.IsNullOrEmpty(labelText))
{
return MvcHtmlString.Empty;
}
var label = new TagBuilder("label");
label.Attributes.Add("for", TagBuilder.CreateSanitizedId(htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(htmlFieldName)));
label.InnerHtml = labelText;
return new HtmlString(label.ToString());
}
}
and then:
#Html.MyLabelFor(x => x.Foo)

Correctly making an ActionLink extension with htmlAttributes

I use a custom extension for my ActionLinks. I have added an attribute data_url which is meant to be translated to an attribute of data-url. This is, replacing the underscaore with a dash.
Here is link 1 using my custom extension:
#Ajax.ActionLink("Add", MyRoutes.GetAdd(), new AjaxOptions()
, new { data_url = Url.Action(...)})
Result: data_url
Here is link 2 using the framework ActionLink:
#Ajax.ActionLink("Add 2", "x", "x", null, new AjaxOptions()
, new { data_url = Url.Action(...) })
Result: data-url
Here is the extension, simple enough, except that the only way to pass the htmlAttributes through that I know of is by using the ToDictionaryR() extension. I suspect this is the problem, so I am wondering if I should be using something else. I have supplied that extension below too.
public static MvcHtmlString ActionLink(this AjaxHelper helper, string linkText
, RouteValueDictionary routeValues, AjaxOptions ajaxOptions
, object htmlAttributes = null)
{
return helper.ActionLink(linkText, routeValues["Action"].ToString()
, routeValues["Controller"].ToString(), routeValues, ajaxOptions
, (htmlAttributes == null ? null : htmlAttributes.ToDictionaryR()));
}
public static IDictionary<string, object> ToDictionaryR(this object obj)
{
return TurnObjectIntoDictionary(obj);
}
public static IDictionary<string, object> TurnObjectIntoDictionary(object data)
{
var attr = BindingFlags.Public | BindingFlags.Instance;
var dict = new Dictionary<string, object>();
foreach (var property in data.GetType().GetProperties(attr))
{
if (property.CanRead)
{
dict.Add(property.Name, property.GetValue(data, null));
}
}
return dict;
}
thank you
You could use the AnonymousObjectToHtmlAttributes method which does exactly what you want and you don't need any custom extension methods:
public static MvcHtmlString ActionLink(
this AjaxHelper helper,
string linkText,
RouteValueDictionary routeValues,
AjaxOptions ajaxOptions,
object htmlAttributes = null
)
{
return helper.ActionLink(
linkText,
routeValues["Action"].ToString(),
routeValues["Controller"].ToString(),
routeValues,
ajaxOptions,
htmlAttributes == null ? null : HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)
);
}

Render HTML tags inside of HTML.ValidationMessageFor in MVC3

I'm trying to show a link as part of the validation message for a field.
I'm using data attributes with custom error messages to set it:
[Required(ErrorMessage = "Message <a href='#'>link</a>")]
public string Field{ get; set; }
But when it renders the tags are escaped and literally prints:
Message <a href='#'>link</a>
Is it possible to have the link as part of the validation message but render correctly?
In case anyone's interested, here's how I accomplished it
public static MvcHtmlString ValidationHTMLMessageFor<TModel, TProperty>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TProperty>> expression)
{
return ValidationHTMLMessageFor(helper, expression, (object)null);
}
public static MvcHtmlString ValidationHTMLMessageFor<TModel, TProperty>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TProperty>> expression, object htmlAttributes)
{
return ValidationHTMLMessageFor(helper, expression, new RouteValueDictionary(htmlAttributes));
}
public static MvcHtmlString ValidationHTMLMessageFor<TModel, TProperty>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TProperty>> expression, IDictionary<string, object> htmlAttributes)
{
string propertyName = ExpressionHelper.GetExpressionText(expression);
string name = helper.AttributeEncode(helper.ViewData.TemplateInfo.GetFullHtmlFieldName(propertyName));
if (helper.ViewData.ModelState[name] == null ||
helper.ViewData.ModelState[name].Errors == null ||
helper.ViewData.ModelState[name].Errors.Count == 0)
{
return MvcHtmlString.Empty;
}
string errors = "";
foreach (ModelError error in helper.ViewData.ModelState[name].Errors)
{
TagBuilder tag = new TagBuilder("span");
tag.Attributes.Add("class", HtmlHelper.ValidationMessageCssClassName);
tag.MergeAttributes(htmlAttributes);
tag.Attributes.Add("data-valmsg-for", name);
tag.Attributes.Add("data-valmsg-replace", "true");
var text = tag.ToString(TagRenderMode.StartTag);
text += error.ErrorMessage;
text += tag.ToString(TagRenderMode.EndTag);
errors += text;
}
return MvcHtmlString.Create(errors);
}
Thanks Darin for pointing me in the right direction. I also found this that I used as a template Customize Html.ValidationMessageFor doesn't work in client side.
I'm new to this, so if anyone has any suggestions, please post.
Thanks!
The code above doesn't work with client-side validation since it doesn't produce the tags required for client-side validation.
Here's an improvement on it that does that:
public static MvcHtmlString ValidationHTMLMessageFor<TModel, TProperty>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TProperty>> expression, IDictionary<string, object> htmlAttributes)
{
string propertyName = ExpressionHelper.GetExpressionText(expression);
string name = helper.AttributeEncode(helper.ViewData.TemplateInfo.GetFullHtmlFieldName(propertyName));
TagBuilder tag = new TagBuilder("span");
tag.Attributes.Add("class", HtmlHelper.ValidationMessageCssClassName);
tag.MergeAttributes(htmlAttributes);
tag.Attributes.Add("data-valmsg-for", name);
tag.Attributes.Add("data-valmsg-replace", "true");
var returnTag = new StringBuilder(tag.ToString(TagRenderMode.StartTag));
if (helper.ViewData.ModelState[name] != null &&
helper.ViewData.ModelState[name].Errors != null &&
helper.ViewData.ModelState[name].Errors.Count > 0)
{
foreach (ModelError error in helper.ViewData.ModelState[name].Errors)
{
returnTag.Append(error.ErrorMessage);
}
}
returnTag.Append(tag.ToString(TagRenderMode.EndTag));
return MvcHtmlString.Create(returnTag.ToString());
}
Thanks for the original post - it was very helpful!
Yes, it is possible but not with the standard helpers (ValidationSummary and ValidationMessageFor). You will have to write a custom helper to render those messages if you want to achieve that. You may take a look at the following post for an example of how to write a custom ValidationSummary helper that doesn't HTML encode the error messages as the standard one.
I'm using MVC4 so I can't say for sure about MVC3.
The only way I was able to insert a <br> tag (in my case), was to start with Jerode's method, but I also had to put a marker in the message string and replace it in the razor. So the sent message was "Line 1[BR]Line2". The razor was #Html.Raw(Html.ValidationMessageFor(x => Model.MyProperty).ToString().Replace("[BR]", "<br>"))
Hope that helps.
Another approach would be to just put the validation item inside of the Html.Raw.
#Html.Raw(Html.ValidationMessageFor(x => Model.MyProperty))
This is not as nice as the approach as the suggestions but should accomplish what you are looking for.
Building on the answer from #shycohen:
public static MvcHtmlString HtmlValidationMessageFor<TModel, TProperty>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TProperty>> expression, object htmlAttributes = null)
{
string propertyName = ExpressionHelper.GetExpressionText(expression);
string name = helper.AttributeEncode(helper.ViewData.TemplateInfo.GetFullHtmlFieldName(propertyName));
TagBuilder tag = new TagBuilder("span");
tag.Attributes.Add("data-valmsg-for", name);
tag.Attributes.Add("data-valmsg-replace", "true");
if (htmlAttributes != null)
{
tag.MergeAttributes((IDictionary<string, object>)HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
}
tag.AddCssClass(HtmlHelper.ValidationMessageCssClassName);
var returnTag = new StringBuilder(tag.ToString(TagRenderMode.StartTag));
if (helper.ViewData.ModelState[name] != null &&
helper.ViewData.ModelState[name].Errors != null &&
helper.ViewData.ModelState[name].Errors.Count > 0)
{
foreach (ModelError error in helper.ViewData.ModelState[name].Errors)
{
returnTag.Append(error.ErrorMessage);
}
}
returnTag.Append(tag.ToString(TagRenderMode.EndTag));
return MvcHtmlString.Create(returnTag.ToString());
}
The htmlAttributes parameter is now optional and passed as an anonymous object to closer match ValidationMessageFor.
The other issue was not being able to add extra classes via htmlAttributes, as TagBuilder.MergeAttributes doesn't merge attribute values. This is solved by using TagBuilder.AddCssClass after merging the attributes to set the validation class.

Resources