Custom MVC template for enum to drop down list - asp.net-mvc

I have created several MVC templates for the EditorFor and DisplayFor helper methods to style things the way I wanted using the Twitter Bootstrap framework. I now have a working solution for all the bits I need, but would like to generalize one part I set up to show a list of states. I have a State enum (with a list of all US states) that I display in a drop down for a users address. I used the [DataType] attribute to get MVC to use my State.cshtml template.
[Required]
[Display(Name = "State")]
[DataType("State")]
public State State { get; set; }
So it works nicely, but I would like to change it so that I can do something like DataType("Enum") or some other way to hit this template generically for all enums.
The template looks like this:
#using System
#using System.Linq
#using Beno.Web.Helpers
#using TC.Util
#model Beno.Model.Enums.State
<div class="control-group">
#Html.LabelFor(m => m, new {#class = "control-label{0}".ApplyFormat(ViewData.ModelMetadata.IsRequired ? " required" : "")})
<div class="controls">
<div class="input-append">
#Html.EnumDropDownListFor(m => m)
<span class="add-on">#(new MvcHtmlString("{0}".ApplyFormat(ViewData.ModelMetadata.IsRequired ? " <i class=\"icon-star\"></i>" : "")))</span>
</div>
#Html.ValidationMessageFor(m => m, null, new {#class = "help-inline"})
</div>
</div>
The EnumDropDownListFor is a helper method I posted about before and that works generically with any enum. What I don't know is how would I change this template to take a generic enum as the model object?
UPDATE: For completeness I include a listing of the EnumDropDownListFor method:
public static MvcHtmlString EnumDropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object htmlAttributes = null) where TProperty : struct, IConvertible
{
if (!typeof(TProperty).IsEnum)
throw new ArgumentException("TProperty must be an enumerated type");
var selectedValue = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData).Model.ToString();
var selectList = new SelectList(from value in EnumHelper.GetValues<TProperty>()
select new SelectListItem
{
Text = value.ToDescriptionString(),
Value = value.ToString()
}, "Value", "Text", selectedValue);
return htmlHelper.DropDownListFor(expression, selectList, htmlAttributes);
}
Changing the model type to Enum produces the following error on the line with the call to the helper method:
CS0453: The type 'System.Enum' must be a non-nullable value type in order to use it as parameter 'TProperty' in the generic type or method 'Beno.Web.Helpers.ControlHelper.EnumDropDownListFor<TModel,TProperty>(System.Web.Mvc.HtmlHelper<TModel>, System.Linq.Expressions.Expression<System.Func<TModel,TProperty>>, object)'
Then if I remove the check if TProperty is an enum and the struct where constraint, I get a compile error on the line where I am trying to get the enum values of:
System.ArgumentException: Type 'Enum' is not an enum
I wonder if it's just not possible to do what I am trying here.

You could just create an EditorTemplate Enum.cshtml
All you would have to do is change this line :
#model Beno.Model.Enums.State
For this :
#model System.Enum
You will then be able to use any Enum with it.
The catch: the engine can't infer the base class of an item thus, TestEnum won't be assigned the Enum template, so you would have to call it explicitly :
#Html.EditorFor(model => model.EnumValue, "Enum")

Not sure if I understand exactly what you mean, but try this:
#Html.DropDownListFor(model => model.EnumName, new SelectList(Enum.GetValues(typeof(Namespace.Models.EnumName))))
EnumName = State in your case.
I've used the above to get an enum into a drop down list using Twitter Bootstrap.

I too have been trying to achieve this.
Is the idea that you want to be able to use one template for all Enum types in all your models.
This way you have an Enum Template in the EditorTemplates folder that allow you to display them as drop down lists.
I have been following this article. http://blogs.msdn.com/b/stuartleeks/archive/2010/05/21/asp-net-mvc-creating-a-dropdownlist-helper-for-enums.aspx
The issue you have is that your template passes the type of System.Enum in the TModel and TProperty
Expression<Func<TModel, TProperty>> expression
Then when you perform the following below TProperty is of Type System.Enum not Beno.Model.Enums.State
EnumHelper.GetValues<TProperty>()
To get around this I do not bother looking at TProperty as it does not give me the right type.
Instead I look at the metadata.ModelType.
ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
This gives me the correct type but you can't use these in within the Covariance Derived class
EnumHelper.GetValue<metadata.ModelType> //This does not work.
So I rewrote the body to not use any generics.
public static MvcHtmlString EnumDropDownListFor<TModel, TEnum>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TEnum>> expression)
{
ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
var values = Enum.GetValues(metadata.ModelType);
List<SelectListItem> items = new List<SelectListItem>();
foreach (var v in values)
{
items.Add(new SelectListItem
{
Text = Regex.Replace(v.ToString(), "([A-Z][a-z])", " $1").Trim(),
Value = v.ToString(),
Selected = v.Equals(metadata.Model)
});
}
return htmlHelper.DropDownListFor(expression, items);
}
You may need to change the method signature to include your htmlattributes.

As others show, writing a custom helper is the way to go. This is exactly what was done in TwitterBootstrapMVC. Among other helpers it has a helper DropDownListFromEnumFor(...), which you'd use like so:
#Html.Bootstrap().DropDownListFromEnumFor(m => m.SomeEnum)
or
#Html.Bootstrap().DropDownListFromEnum("SomeEnum")
The cool thing about BMVC is that you can customize the dropdown with extension methods some of which are for regular html and others are Bootstrap specific. Below are some of them:
#(f.ControlGroup().DropDownListFromEnumFor(m => m.SomeEnum)
.Append("something")
.AppendIcon("glyphicon glyphicon-chevron-right")
.Class("cool-dd")
.OptionLabel("-- Select --")
.Tooltip("cool tooltip"))
Oh, and yeah, the example above will generate full control-group - input, label, and validation message.
Disclaimer: I'm the author of TwitterBootstrapMVC

Related

Is there a way to automatically display “*” required icon beside the [Required] fields

I have finished my asp.net MVC web application, and I have been using the data annotation [Required] to mention that the field is required. But currently the required fields does not have any indication that they are required, unless the user tried to submit the form. So is there way to force my Razor view to display a red “” beside any field that have [Required] defined on it? OR I need to manually add the “” icon ?
Thanks
After I got burned by the Bootstrap 2 to 3 upgrade, where they pretty much completely changed the HTML for form controls, I've been putting the entire form group in editor templates instead of just the field. Here's an example:
<div class="form-group">
#Html.Label("", new { #class = string.Format("control-label col-md-2{0}", ViewData.ModelMetadata.IsRequired ? string.Empty : " optional") })
<div class="col-md-6">
#Html.TextBox("", ViewData.TemplateInfo.FormattedModelValue.ToString(), new { type = "email", #class = "form-control" })
#Html.ValidationMessage("")
</div>
</div>
What's important here for you is the string.Format in Html.Label. I'm using the ViewData.ModelMetadata.IsRequired to add an "optional" class if it's false. Bootstrap makes the labels bold by default, so as a required indicator, I make optional field labels normal (non-bold). However, adding a * is a little more difficult. You could use the same thing I'm doing here to add an additional span tag:
#if (ViewData.ModelMetadata.IsRequired)
{
<span class="requiredIndicator>*</span>
}
#Html.Label(...)
...
The potential problem is that that won't actually be inside your <label> tag, so you might have to do some extra styling work to make it look right depending on the styles you apply to the labels.
An alternative is to create your own HtmlHelper to return a label with a required indicator. Here's some sample code for that:
public static MvcHtmlString RequiredIndicatorLabelFor<TModel, TValue>(
this HtmlHelper<TModel> html,
Expression<Func<TModel, TValue>> modelProperty,
object htmlAttributes)
{
var metadata = ModelMetadata.FromLambdaExpression(modelProperty, html.ViewData);
var labelText = metadata.IsRequired ? string.Format("* {0}", metadata.GetDisplayName()) : metadata.GetDisplayName();
return html.LabelFor(modelProperty, labelText, htmlAttributes);
}
You can also write a custom label helper for this purpose
public static MvcHtmlString CustomLabelFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression,
IDictionary<string, object> htmlAttributes = null )
{
var metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
var propertyName = ExpressionHelper.GetExpressionText(expression);
var builder = new TagBuilder("label");
builder.Attributes.Add("for", TagBuilder.CreateSanitizedId(htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(propertyName)));
var labelText = string.Format("{0}{1}", metadata.IsRequired ? "*" : string.Empty,
string.IsNullOrEmpty(metadata.DisplayName)
? metadata.PropertyName
: metadata.DisplayName);
builder.SetInnerText(labelText);
builder.MergeAttributes<string, object>(htmlAttributes, true);
return new MvcHtmlString(builder.ToString());
}
Now when used CustomLabelFor on a property with Required attribute, it will append * in fort of the label text.
#Html.CustomLabelFor(m => m.YourRequiredField)

Using #Html.DisplayNameFor() with PagedList

I've been trying out the PagedList package to get paging for my index views. Everything was going well, and at the controller level everything is working fine, it only displays 5 records per page, and displays the appropriate page based on the querystring.
My problem is in the view. I changed the #Model to PagedList.IPagedList so I could access the Model.HasNextPage and other properties, but now the #Html.DisplayNameFor(model => model.ItemName) are no longer working. I get this error:
PagedList.IPagedList<Dossier.Models.Item>' does not contain a definition for 'ItemName' and no extension method 'ItemName' accepting a first argument of type 'PagedList.IPagedList<Dossier.Models.Item>' could be found (are you missing a using directive or an assembly reference?)
Here are the relevant parts of the view:
#model PagedList.IPagedList<Dossier.Models.Item>
#using Dossier.Models.Item
...
<th>
#Html.DisplayNameFor(model => model.ItemName)
</th>
It seems IPagedList is not compatible with DisplayNameFor(). Any idea why this is happening, and how I could fix it? I know I could just manually enter the column names, but I'd like for that information to stay (and be changeable) in the model later.
You can try this
#Html.DisplayNameFor(model => model.FirstOrDefault().ItemName)
As an alternate solution to the accepted answer, remember that IPagedList inherits from IEnumerable. That means that you could write:
#model IEnumerable<Dossier.Models.Item>
At the beginning of the page, and just cast the model to IPagedList when needed:
#Html.PagedListPager((IPagedList)Model, page => Url.Action("Index", new { page = page }))
You can even declare the casted variable in the header, in order to use it multiple times within the page:
#{
ViewBag.Title = "My page title";
var pagedlist = (IPagedList)Model;
}
This would allow you to use the DisplayNameFor helper method, and access all PagedList methods/properties, without the need for dummy elements nor calling .FirstOrDefault() for each field.
I solved the problem by creating an overload of DisplayNameFor that accepts a IPagedList<TModel>.
namespace PagedList.Mvc
{
public static class Extensions
{
[SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
public static MvcHtmlString DisplayNameFor<TModel, TValue>(this HtmlHelper<IPagedList<TModel>> html, Expression<Func<TModel, TValue>> expression)
{
return DisplayNameForInternal(html, expression);
}
[SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Justification = "This is an extension method")]
internal static MvcHtmlString DisplayNameForInternal<TModel, TValue>(this HtmlHelper<IPagedList<TModel>> html, Expression<Func<TModel, TValue>> expression)
{
return DisplayNameHelper(ModelMetadata.FromLambdaExpression(expression, new ViewDataDictionary<TModel>()),
ExpressionHelper.GetExpressionText(expression));
}
internal static MvcHtmlString DisplayNameHelper(ModelMetadata metadata, string htmlFieldName)
{
string resolvedDisplayName = metadata.DisplayName ?? metadata.PropertyName ?? htmlFieldName.Split('.').Last();
return new MvcHtmlString(HttpUtility.HtmlEncode(resolvedDisplayName));
}
}
}
I'll be sending a pull request to PageList project to include it into the project for everyone.
You do not need to change #Html.DisplayNameFor. Declare model in the view as:
#model IEnumerable<Dossier.Models.Item>
Just move your pager to partial view (lets name it "_Pager"):
#model IPagedList
...
#Html.PagedListPager(Model,
page => Url.Action("Index", new { page, pageSize = Model.PageSize }))
...
Render the pager in your view:
#Html.Partial("_Pager", Model)
Thats it.
P.S. You can create Html helper instead of partial view...
As an alternate solution you could try:
#Html.DisplayNameFor(x => x.GetEnumerator().Current.ItemName)
It will work even if the list is empty!

Is it possible to create a generic #helper method with Razor?

I am trying to write a helper in Razor that looks like the following:
#helper DoSomething<T, U>(Expression<Func<T, U>> expr) where T : class
Unfortunately, the parser thinks that <T is the beginning of an HTML element and I end up with a syntax error. Is it possible to create a helper with Razor that is a generic method? If so, what is the syntax?
This is possible to achieve inside a helper file with the #functions syntax but if you want the razor-style readability you are referring to you will also need to call a regular helper to do the HTML fit and finish.
Note that functions in a Helper file are static so you would still need to pass in the HtmlHelper instance from the page if you were intending to use its methods.
e.g.
Views\MyView.cshtml:
#MyHelper.DoSomething(Html, m=>m.Property1)
#MyHelper.DoSomething(Html, m=>m.Property2)
#MyHelper.DoSomething(Html, m=>m.Property3)
App_Code\MyHelper.cshtml:
#using System.Web.Mvc;
#using System.Web.Mvc.Html;
#using System.Linq.Expressions;
#functions
{
public static HelperResult DoSomething<TModel, TItem>(HtmlHelper<TModel> html, Expression<Func<TModel, TItem>> expr)
{
return TheThingToDo(html.LabelFor(expr), html.EditorFor(expr), html.ValidationMessageFor(expr));
}
}
#helper TheThingToDo(MvcHtmlString label, MvcHtmlString textbox, MvcHtmlString validationMessage)
{
<p>
#label
<br />
#textbox
#validationMessage
</p>
}
...
No, this is not currently possible. You could write a normal HTML helper instead.
public static MvcHtmlString DoSomething<T, U>(
this HtmlHelper htmlHelper,
Expression<Func<T, U>> expr
) where T : class
{
...
}
and then:
#(Html.DoSomething<SomeModel, string>(x => x.SomeProperty))
or if you are targeting the model as first generic argument:
public static MvcHtmlString DoSomething<TModel, TProperty>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expr
) where TModel : class
{
...
}
which will allow you to invoke it like this (assuming of course that your view is strongly typed, but that's a safe assumption because all views should be strongly typed anyways :-)):
#Html.DoSomething(x => x.SomeProperty)
In all cases the TModel will be the same (the model declared for the view), and in my case, the TValue was going to be the same, so I was able to declare the Expression argument type:
#helper FormRow(Expression<Func<MyViewModel, MyClass>> expression) {
<div class="form-group">
#(Html.LabelFor(expression, new { #class = "control-label col-sm-6 text-right" }))
<div class="col-sm-6">
#Html.EnumDropDownListFor(expression, new { #class = "form-control" })
</div>
#Html.ValidationMessageFor(expression)
</div>
}
If your model fields are all string, then you can replace MyClass with string.
It might not be bad to define two or three helpers with the TValue defined, but if you have any more that would generate some ugly code, I didn't really find a good solution. I tried wrapping the #helper from a function I put inside the #functions {} block, but I never got it to work down that path.
if your main problem is to get name attribute value for binding using lambda expression seems like the #Html.TextBoxFor(x => x.MyPoperty), and if your component having very complex html tags and should be implemented on razor helper, then why don't just create an extension method of HtmlHelper<TModel> to resolve the binding name:
namespace System.Web.Mvc
{
public static class MyHelpers
{
public static string GetNameForBinding<TModel, TProperty>
(this HtmlHelper<TModel> model,
Expression<Func<TModel, TProperty>> property)
{
return ExpressionHelper.GetExpressionText(property);
}
}
}
your razor helper should be like usual:
#helper MyComponent(string name)
{
<input name="#name" type="text"/>
}
then here you can use it
#TheHelper.MyComponent(Html.GetNameForBinding(x => x.MyProperty))

ASP.Net MVC, ViewPage<Dynamic> and EditorFor/LabelFor

I'm playing with MVC3 using the Razer syntax, though I believe the problem to be more general.
In the controller, I have something like:
ViewModel.User = New User(); // The model I want to display/edit
ViewModel.SomeOtherProperty = someOtherValue; // Hense why need dynamic
Return View();
My View inherits from System.Web.Mvc.ViewPage
But if I try to do something like:
<p>
#Html.LabelFor(x => x.User.Name
#Html.EditorFor(x => x.User.Name
</p>
I get the error: "An expression tree may not contain a dynamic operation"
However, the use of ViewPage seems quite common, as are EditorFor/LabelFor. Therefore I'd be surprised if there's not a way to do this - appreciate any pointers.
Don't use ViewPage<Dynamic>. I would recommend you using a view model and strongly type your view to this view model:
var model = new MyViewModel
{
User = new User
{
Name = "foo"
},
SomeOtherProperty = "bar"
};
return View(model);
and then strongly type your view to ViewPage<MyViewModel> and:
#Html.LabelFor(x => x.User.Name)
#Html.EditorFor(x => x.User.Name)
<div>#Model.SomeOtherProperty</div>
It seems the expression trees => http://msdn.microsoft.com/en-us/library/bb397951.aspx must not contain any dynamic variables.
Unfortunately this is the case for TModel when you use dynamics in it.
public static MvcHtmlString TextBoxFor<TModel, TProperty>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func> expression
)

How to unpack htmlAttributes in a span tag in ASP.NET MVC

I have htmlAttributes generated - originally for the purpose of creating a link with the attribute using Html.ActionLink.
Now, based on some condition, I would like to create a <span> tag instead and put the attributes in there. Is there anyway it can be done easily ?
Eg: something like:
<span <%= htmlAttributes.Unpack() %> > Some txt </span>
OR
<%= Html.SpanTag("Some txt", htmlAttributes) %>
OR anything similar without wresling too much with the already generated htmlAttribues?
Thanks
You could easily build an Html.Span Extension something like :
public static MvcHtmlString DatePicker(this HtmlHelper htmlHelper, string name, string text, object htmlAttributes)
{
RouteValueDictionary attributes =
htmlAttributes == null ? new RouteValueDictionary()
: new RouteValueDictionary(htmlAttributes);
TagBuilder tagBuilder = new TagBuilder("span");
tagBuilder.MergeAttributes(attributes);
tagBuilder.MergeAttribute("name", name, true);
tagBuilder.GenerateId(name);
tagBuilder.SetInnerText(text);
return MvcHtmlString.Create(tagBuilder.ToString());
}
I didn't test this but should be working

Resources