Extending MVC RadioButtonFor using HtmlHelper extensions - model-view-controller

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

Related

MVC 3 helper for radiobuttonlist enum

I need some help. I'm trying to build a view where I need groups of radiobuttons of enum types.
I have several enum types(classes) like this:
[DataContract(Namespace = Constants.SomeDataContractNamespace)]
public enum OneEnumDataContract
{
[Display(Name = "Text_None", Description = "Text_None", ResourceType = typeof(TextResource))]
[EnumMember]
None = 0,
[Display(Name = "Text_Medium", Description = "Text_Medium", ResourceType = typeof(TextResource))]
[EnumMember]
Medium = 1,
[Display(Name = "Text_Very", Description = "Text_Very", ResourceType = typeof(TextResource))]
[EnumMember]
Very = 2
}
In my model(a datacontract, using WCF) I have this property for the enum datacontract:
[DataMember(Order = 23)]
[Display(Name = "EnumValue", Description = "EnumValue_Description", ResourceType = typeof(TextResource))]
public OneEnumDataContract EnumClass1 { get; set; }
In my view I would try to make the group of radiobuttons like this(with a helper):
#Html.RadioButtonListEnum("EnumList1", Model.EnumClass1)
My helper:
public static MvcHtmlString RadioButtonListEnum<TModel>(this HtmlHelper<TModel> helper, string NameOfList, object RadioOptions)
{
StringBuilder sb = new StringBuilder();
//som other code for pairing with resourcefile...
foreach(var myOption in enumTexts.AllKeys)
{
sb.Append("<p>");
sb.Append(enumTexts.GetValues(myOption)[0]);
sb.Append(helper.RadioButton(NameOfList, System.Convert.ToInt16(myOption)));
sb.Append("</p>");
}
return MvcHtmlString.Create(sb.ToString());
}
This gives me the first enumvalue in OneEnumDataContract, None, as the parameter RadioOptions.
How can I get all the enumvalues in the datacontract into the helper?
This is one I created recently. It won't work if you try it on a non-enum but works for my enum needs. I copied bit's and pieces from different DropDownList helpers like nikeaa posted.
#region RadioButtonList
public static MvcHtmlString RadioButtonListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IDictionary<string, object> htmlAttributes = null) where TModel : class
{
ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
String field = ExpressionHelper.GetExpressionText(expression);
String fieldname = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(field);
var inputName = fieldname;
TProperty val = GetValue(htmlHelper, expression);
var divTag = new TagBuilder("div");
divTag.MergeAttribute("id", inputName);
divTag.MergeAttribute("class", "radio");
foreach (var item in Enum.GetValues(val.GetType()))
{
DisplayAttribute[] attr = (DisplayAttribute[])item.GetType().GetField(item.ToString()).GetCustomAttributes(typeof(DisplayAttribute), true);
if (attr == null || attr.Length == 0 || attr[0].Name != null)
{
string name = attr != null && attr.Length > 0 && !string.IsNullOrWhiteSpace(attr[0].Name) ? attr[0].Name : item.ToString();
var itemval = item;
var radioButtonTag = RadioButton(htmlHelper, inputName, new SelectListItem { Text = name, Value = itemval.ToString(), Selected = val.Equals(itemval) }, htmlAttributes);
divTag.InnerHtml += radioButtonTag;
}
}
return new MvcHtmlString(divTag.ToString());
}
public static string RadioButton(this HtmlHelper htmlHelper, string name, SelectListItem listItem,
IDictionary<string, object> htmlAttributes)
{
var inputIdSb = new StringBuilder();
inputIdSb.Append(name)
.Append("_")
.Append(listItem.Value);
var sb = new StringBuilder();
var builder = new TagBuilder("input");
if (listItem.Selected) builder.MergeAttribute("checked", "checked");
builder.MergeAttribute("type", "radio");
builder.MergeAttribute("value", listItem.Value);
builder.MergeAttribute("id", inputIdSb.ToString());
builder.MergeAttribute("name", name);
builder.MergeAttributes(htmlAttributes);
sb.Append(builder.ToString(TagRenderMode.SelfClosing));
sb.Append(RadioButtonLabel(inputIdSb.ToString(), listItem.Text, htmlAttributes));
sb.Append("<br>");
return sb.ToString();
}
public static string RadioButtonLabel(string inputId, string displayText,
IDictionary<string, object> htmlAttributes)
{
var labelBuilder = new TagBuilder("label");
labelBuilder.MergeAttribute("for", inputId);
labelBuilder.MergeAttributes(htmlAttributes);
labelBuilder.InnerHtml = displayText;
return labelBuilder.ToString(TagRenderMode.Normal);
}
public static TProperty GetValue<TModel, TProperty>(HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression) where TModel : class
{
TModel model = htmlHelper.ViewData.Model;
if (model == null)
{
return default(TProperty);
}
Func<TModel, TProperty> func = expression.Compile();
return func(model);
}
#endregion
I use it like this
#Html.RadioButtonListFor(m => m.PlayFormat)
You may need to more code to set the correct element name for more complicated uses.
If the enum items have a Display attribute, the name is displayed. Otherwise the enum item is displayed. If the Display name is null, that value is not shown as an option. In this enum, "None" isn't displayed, "Singles" is displayed from the enum value, "Men's Doubles" and all the other's have text from [Display(Name="Men's Doubles")]
public enum PlayFormat
{
[Display(Name=null)]
None = 0,
Singles = 1,
[Display(Name = "Men's Doubles")]
MenDoubles = 2,
[Display(Name = "Women's Doubles")]
WomenDoubles = 3,
[Display(Name = "Mixed Doubles")]
MixedDoubles = 4,
[Display(Name = "Men's Group")]
MenGroup = 5,
[Display(Name = "Women's Group")]
WomenGroup = 6,
[Display(Name = "Mixed Group")]
MixedGroup = 7
}
The page looks like this (except each - is a radio button)
- Singles
- Men's Doubles
- Women's Doubles
- Mixed Doubles
- Men's Group
- Women's Group
- Mixed Group
Here is a helper method that I found on the internet to make a drop down list from an enum. You should be able to modify this code to create radio buttons instead of a drop down.
namespace Localicy.HtmlHelpers {
public static class HtmlHelperExtensions {
private static Type GetNonNullableModelType(ModelMetadata modelMetadata) {
Type realModelType = modelMetadata.ModelType;
Type underlyingType = Nullable.GetUnderlyingType(realModelType);
if (underlyingType != null)
realModelType = underlyingType;
return realModelType;
}
private static readonly SelectListItem[] SingleEmptyItem = new[] { new SelectListItem { Text = "", Value = "" } };
public static string GetEnumDescription<TEnum>(TEnum value) {
FieldInfo fi = value.GetType().GetField(value.ToString());
DescriptionAttribute[] attributes = (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false);
if ((attributes != null) && (attributes.Length > 0))
return attributes[0].Description;
else
return value.ToString();
}
public static MvcHtmlString EnumDropDownListFor<TModel, TEnum>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TEnum>> expression) {
return EnumDropDownListFor(htmlHelper, expression, null, null);
}
public static MvcHtmlString EnumDropDownListFor<TModel, TEnum>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TEnum>> expression, string defaultValueText) {
return EnumDropDownListFor(htmlHelper, expression, defaultValueText, null);
}
public static MvcHtmlString EnumDropDownListFor<TModel, TEnum>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TEnum>> expression, string defaultValueText, object htmlAttributes) {
ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
Type enumType = GetNonNullableModelType(metadata);
IEnumerable<TEnum> values = Enum.GetValues(enumType).Cast<TEnum>();
IEnumerable<SelectListItem> items = from value in values
select new SelectListItem {
Text = GetEnumDescription(value),
Value = value.ToString(),
Selected = value.Equals(metadata.Model)
};
// If the enum is nullable, add an 'empty' item to the collection
if (metadata.IsNullableValueType || defaultValueText != null)
if(defaultValueText != null) {
SelectListItem[] tempItem = new[] { new SelectListItem { Text = defaultValueText, Value = "" } };
items = tempItem.Concat(items);
}
else
items = SingleEmptyItem.Concat(items);
//items = (new ).Concat(items)
return htmlHelper.DropDownListFor(expression, items, htmlAttributes);
}
}
}

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)

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.

MVC 3: Enum dropdown template for use in EditorForModel

I know there is several threads on this topic. I see a lot of solutions but most of them demands the knowledge of which type the enum is. Since Enum by default renders uses the template for string, it's more difficult to find out the exact type.
Has someone got a complete example on how to solve this?
Edit. Based on Roys suggestion, here's my code.
HtmlHelpers.cs
public static MvcHtmlString EnumDropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression) where TModel : class
{
var type = htmlHelper.ViewData.ModelMetadata.ModelType;
var inputName = type.Name;
var value = htmlHelper.ViewData.Model == null ? default(TProperty) : expression.Compile()(htmlHelper.ViewData.Model);
var selected = value == null ? String.Empty : value.ToString();
return htmlHelper.DropDownList(inputName, ToSelectList(type, selected));
}
private static IEnumerable<SelectListItem> ToSelectList(Type enumType, string selectedItem)
{
var items = new List<SelectListItem>();
foreach (var item in Enum.GetValues(enumType))
{
var fi = enumType.GetField(item.ToString());
var attribute = fi.GetCustomAttributes(typeof(DescriptionAttribute), true).FirstOrDefault();
var title = attribute == null ? item.ToString() : ((DescriptionAttribute)attribute).Description;
var listItem = new SelectListItem
{
Value = ((int)item).ToString(),
Text = title,
Selected = selectedItem == ((int)item).ToString()
};
items.Add(listItem);
}
return new SelectList(items, "Value", "Text");
}
Viewmodel
public class AddTestForm
{
[UIHint("Enum")]
public EnumType Type { get; set; }
public string Description { get; set; }
public enum EnumType
{
One = 1,
Two = 2
}
EditorTemplates/Enum.cshtml
#model object
#Html.EnumDropDownListFor(x => x)
And in views...
#Html.EditorForModel()
The problem now is that I can't get the enum to fill after the form has been posted.
Your helper generates wrong names to the generated select elements:
<select id="Type_EnumType" name="Type.EnumType">
The DefaultModelBinder uses the name attribute to match the property names. Your helper generated name "Type.EnumType" doesn't match with the Model property name "Type". To make it work instead of the Type name you need to get the property name from the expression. It's quite easy with the ExpressionHelper class:
public static MvcHtmlString EnumDropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression) where TModel : class
{
...
return htmlHelper.DropDownList(ExpressionHelper.GetExpressionText(expression), ToSelectList(type, selected));
}

MVC3 - merging htmlAttributes set in View with ones set in Helper

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

Resources