Custom Attributes for SelectlistItem in MVC - asp.net-mvc

I would like to create a custom htmlhelper(Extension Method) for dropdownlist to accept custom attributes in the Option tag of the selectlistitem.
I have a property in my model class, that I would like to include as an attribute in the option tag of the selectlist.
i.e <option value ="" modelproperty =""></option>
I have come across various examples but non quite specific to what I would want.

Try this:
public static MvcHtmlString CustomDropdown<TModel, TProperty>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression,
IEnumerable<SelectListItem> listOfValues,
string classPropName)
{
var model = htmlHelper.ViewData.Model;
var metaData = ModelMetadata
.FromLambdaExpression(expression, htmlHelper.ViewData);
var tb = new TagBuilder("select");
if (listOfValues != null)
{
tb.MergeAttribute("id", metaData.PropertyName);
var prop = model
.GetType()
.GetProperties()
.FirstOrDefault(x => x.Name == classPropName);
foreach (var item in listOfValues)
{
var option = new TagBuilder("option");
option.MergeAttribute("value", item.Value);
option.InnerHtml = item.Text;
if (prop != null)
{
// if the prop's value cannot be converted to string
// then this will throw a run-time exception
// so you better handle this, put inside a try-catch
option.MergeAttribute(classPropName,
(string)prop.GetValue(model));
}
tb.InnerHtml += option.ToString();
}
}
return MvcHtmlString.Create(tb.ToString());
}

Yeah you can create it by your own.
Create one Extension method which will accept a list of Object which contains all required properties of it. Use TagBuilder to create Tags and use MergeAttribute method of it to add your own attribute to it.
Cheers

Related

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 get selected text from a SelectList

I am creating a custom helper method for a Drop Down List with the following signature:
public static MvcHtmlString MyCustomDropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, string optionLabel, object htmlAttributes)
Within the method, I can do the following to get the selected value:
var Value = ((SelectList)selectList).SelectedValue);
In my helper method, I need to find out what the selected text is as well, and not just the value. How will I get that?
If you already have selected value you can just iterate through the IEnumerable trying to find the selected SelectListItem:
SelectListItem[] items = selectList.ToArray();
SelectListItem selectedItem = items.FirstOrDefault(i => i.Value == valueAsString)
?? items[0];
string selectedText = selectedItem.Text;
From my opinion, casting to SelectList is not the best option to find selected value, this casting could throw InvalidCastException when real type of selectList variable is other than SelectList.
In most cases your model contains a value that should be selected in the drop down list. You can get this value using ModelMetadata classes as follows:
//Get value from model or from ModelState
object modelValue = ModelMetadata
.FromLambdaExpression(expression, html.ViewData).Model;
SelectListItem[] items = selectList.ToArray();
string selectedValue = modelValue == null ? null : modelValue.ToString();
SelectListItem selectedItem = items.FirstOrDefault(i => i.Value == selectedValue)
?? items.FirstOrDefault();
string selectedText = selectedItem == null ? null : selectedItem.Text;
//Rest code goes here
public static string getText(SelectList selectList)
{
string text = selectList.Where(x => x.Selected).FirstOrDefault().Text;
return text;
}

Selected Property of SelectListItem and selectedValue parameter of SelectItem not working

I am trying to develop an HtmlHelper extension method: EnumDropDownListFor. No matter what I did I was unable to show the selected value. I tried setting Selected=true property of SelectListItem and setting selectedValue of SelectList constructor. While debugging (at return line) I can see Selected=true for the SelectLİstItem which is supposed to be Selected, for both cases. But when I "View Source" none of the options have selected="selected" attribute.
Where am I going wrong?
Note: Toolkit is my utility class and ToByte is an extension method for Enum
public static MvcHtmlString EnumDropDownListFor<TModel, TProperty>(
this HtmlHelper<TModel> helper, Expression<Func<TModel, TProperty>> expression, string optionLabel = null,
object htmlAttributes = null) where TModel : class
{
var selectedValue = helper.ViewData.Model == null
? default(TProperty)
: expression.Compile()(helper.ViewData.Model);
var enumVals = Toolkit.GetEnumValues(typeof(TProperty));
//var selectList = from enumVal in enumVals.OfType<Enum>()
// select new SelectListItem
// {
// Text = enumVal.GetName(),
// Value = enumVal.ToByte().ToString(),
// Selected = Equals(enumVal, Toolkit.To<Enum>(selectedValue))
// };
// helper.ViewData[(expression.Body as MemberExpression).Member.Name] = Toolkit.To<Enum>(selectedValue).ToByte().ToString();
var selectList = new SelectList(from enumVal in enumVals.OfType<Enum>()
select new
{
TextField = enumVal.GetName(),
ValueField = enumVal.ToByte().ToString()
}, "ValueField", "TextField", Toolkit.To<Enum>(selectedValue).ToByte().ToString());
return helper.DropDownListFor(expression, selectList, optionLabel, htmlAttributes);
}
I solved it (:
That was beacuse I am calling "helper.DropDownListFor" with same expression which returns an Enum type and I was trying to set values of options by "Byte" casted values. So it seems that Expression's return value overrides the given selected value, makes a lot of sense.

How to customize Html.ValidationMessageFor in ASP MVC

Is it possible to customize the Html.ValidationMessageFor method so that it produces different HTML?
I want to do something similar to:
<div class="field-error-box">
<div class="top"></div>
<div class="mid"><p>This field is required.</p></div>
</div>
I am not sure if it's possible to use paragraph instead of default span, as it may make impossible for validation plugin to place error messages. But for div -s, thats easy - you could write custom html helper.
Something along these lines (may need further testing/coding). You will need to include the namespace of this static extension method in your view, or put this into System.Web.Mvc.Html directly.
public static class Validator
{
public static MvcHtmlString MyValidationMessageFor<TModel, TProperty>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TProperty>> expression)
{
TagBuilder containerDivBuilder = new TagBuilder("div");
containerDivBuilder.AddCssClass("field-error-box");
TagBuilder topDivBuilder = new TagBuilder("div");
topDivBuilder.AddCssClass("top");
TagBuilder midDivBuilder = new TagBuilder("div");
midDivBuilder.AddCssClass("mid");
midDivBuilder.InnerHtml = helper.ValidationMessageFor(expression).ToString();
containerDivBuilder.InnerHtml += topDivBuilder.ToString(TagRenderMode.Normal);
containerDivBuilder.InnerHtml += midDivBuilder.ToString(TagRenderMode.Normal);
return MvcHtmlString.Create(containerDivBuilder.ToString(TagRenderMode.Normal));
}
}
As you see, this uses default ValidationMessageFor method, to not interfere with validation-plugin error message processing.
And you use this simply, as default validation message helper
#Html.MyValidationMessageFor(model => model.SomeRequiredField)
I used another way:
public static MvcHtmlString DivValidationMessageFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression)
{
return MvcHtmlString.Create(htmlHelper.ValidationMessageFor(expression).ToString().Replace("span", "div"));
}
This way you can use the built in way, but replace the span with a div.
If you need any other overloads of the function, just duplicate as necessary.
You can implement your own ValidationMessageFor helper to emit your desired output or use some javascript to add/modify the rendered HTML code but the custom ValidationMessageFor implementation is the cleaner approach IMHO.
To implement your own ValidationMessageFor helper take a look at the ValidationExtensions.ValidationMessageFor and ValidationMessageHelper methods in the ASP.NET MVC source code.
Implementation Hints
Since GetFormContextForClientValidation is internal you have to work around that implementation by duplicating the internal functionality in your code:
FormContext formContext = htmlHelper.ViewContext.ClientValidationEnabled ? htmlHelper.ViewContext.FormContext : null;
Some other methods are private in ValidationExtensions like GetUserErrorMessageOrDefault you would need to duplicate that code too. What you can do to avoid duplicating code is to let ValidationExtentensions.ValidationMessageFor render the validation message string that is wrapped in a span and afterwards change the rendered string according to your requirements. Keep in mind that "null" is returned in case no error was found and that you'll need the data- HTML attributes in case you have unobtrusive JavaScript enabled.
You can download the ASP.NET MVC 3 source code from here
The only need for change of the default tag generation was in my case, that spans behavior results in anoying margin setups.
I resolved this by using 'display: block'
Maybe this helps some people..
Maybe you can put that code
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;
}
on top of the answered function, so that the div doesn't appear on the form load.
I created ValidationMessageAsStringFor which just returns the error message as string. It is basically a simplified version of ValidationMessageFor:
public static MvcHtmlString ValidationMessageAsStringFor<TModel, TProperty>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TProperty>> expression)
{
var field = ExpressionHelper.GetExpressionText(expression);
string modelName = helper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(field);
if (!helper.ViewData.ModelState.ContainsKey(modelName))
{
return null;
}
var modelState = helper.ViewData.ModelState[modelName];
var modelErrors = (modelState == null) ? null : modelState.Errors;
var modelError = ((modelErrors == null) || (modelErrors.Count == 0)) ? null : modelErrors.FirstOrDefault(m => !String.IsNullOrEmpty(m.ErrorMessage)) ?? modelErrors[0];
if (modelError == null)
{
return null;
}
var errorMessage = GetUserErrorMessageOrDefault(helper.ViewContext.HttpContext, modelError, modelState);
return MvcHtmlString.Create(errorMessage);
}
private static string GetUserErrorMessageOrDefault(HttpContextBase httpContext, ModelError error, ModelState modelState)
{
if (!string.IsNullOrEmpty(error.ErrorMessage))
{
return error.ErrorMessage;
}
if (modelState == null)
{
return null;
}
return modelState.Value?.AttemptedValue;
}
With this in place and after importing the namespace containing the new helper, just create the HTML code you need:
<div class="field-error-box">
<div class="top"></div>
<div class="mid"><p>#Html.ValidationMessageAsStringFor(m => m.FieldName)</p></div>
</div>
Yes, just use a metamodel for the field:
[MetadataType(typeof(YourMetaData))]
public partial class YOURCLASS
{
[Bind(Exclude = "objID")]
public class YourMetaData
{
[Required(AllowEmptyStrings = false, ErrorMessage = "Please enter a name")]
public object Name { get; set; }
}
}
Change your message at the ErrorMessage field :)
Hope this help :)

ASP.Net MVC Html.Label with Attribute List?

How do I create an ASP.Net MVC Helper for an Html.Label which takes in attributes?
Currently when I define an Html.TextBox I am able to pass in a list of attributes. Sort of like below:
new {disabled="disabled", #class="pcTextBoxWithoutPaddingDisabled"})%>
However, it does not look as though the Html.Label has this feature. As a result, I have to define my labels using the label tag. Sort of like below:
<label class="pcLabelBlackWithoutPadding">
I would like to be consistent I how my Html element get created.
So, how do I create an Html.Label that will take in a list of attributes?
Thanks for your help.
This is updated version for MVC3:
public static MvcHtmlString Label(this HtmlHelper helper, String htmlFieldName, String labelText, Object htmlAttributes)
{
ModelMetadata metadata = ModelMetadata.FromStringExpression(htmlFieldName, helper.ViewData);
String innerText = labelText ?? (metadata.DisplayName ?? (metadata.PropertyName ?? htmlFieldName.Split('.').Last()));
if (String.IsNullOrEmpty(innerText))
{
return MvcHtmlString.Empty;
}
TagBuilder tagBuilder = new TagBuilder("label");
tagBuilder.Attributes.Add("for", TagBuilder.CreateSanitizedId(helper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(htmlFieldName)));
tagBuilder.MergeAttributes(new RouteValueDictionary(htmlAttributes));
tagBuilder.SetInnerText(innerText);
return new MvcHtmlString(tagBuilder.ToString(TagRenderMode.Normal));
}
I have modified Alexandr code a bit with lambda expression, in case anyone needed the lambda expression.
usage:
#Html.LabelFor(model => model.Property , new { #class = "bigFont" })
code:
public static MvcHtmlString LabelFor<TModel, TProperty>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression, Object htmlAttributes)
{
var metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
var innerText = metadata.DisplayName ?? metadata.PropertyName;
if (String.IsNullOrEmpty(innerText))
{
return MvcHtmlString.Empty;
}
var tagBuilder = new TagBuilder("label");
tagBuilder.Attributes.Add("for", TagBuilder.CreateSanitizedId(htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(metadata.PropertyName)));
tagBuilder.MergeAttributes(new RouteValueDictionary(htmlAttributes));
tagBuilder.SetInnerText(innerText);
return new MvcHtmlString(tagBuilder.ToString(TagRenderMode.Normal));
}
I'd suggest creating your own HtmlHelper extension method and using a TagBuilder to create the label.
public static HtmlHelperExtensions
{
public static Label( this HtmlHelper helper, string labelText, object properties )
{
var builder = new TagBuilder("label");
builder.MergeAttributes( new RouteValueDictionary( properties ) );
builder.SetInnerText( labelText );
return builder.ToString( TagRenderMode.Normal );
}
}
See the MVC source code for ideas on how to create a strongly-typed label helper. Note that you'll need to add the namespace containing your extensions either to the page or the web.config to be able to use it.

Resources