RouteValueDictionary/object problem in mvc ActionLink - asp.net-mvc

so here it goes:
I have a html helper which renders ActionLinks with the optional parameters of the current url in it. this html helper also lets you add some more optional parameters as you please and merges them in 1 RouteValueDictionary.
public static string ActionLinkwParams(this HtmlHelper helper, string linktext, string action, string controller, object extraRVs, object htmlAttributes) {
//get current optional params from current URL
NameValueCollection c = helper.ViewContext.RequestContext.HttpContext.Request.QueryString;
//put those in a dict
RouteValueDictionary r = new RouteValueDictionary();
foreach (string s in c.AllKeys) {
r.Add(s, c[s]);
}
RouteValueDictionary htmlAtts = new RouteValueDictionary(htmlAttributes);
RouteValueDictionary extra = new RouteValueDictionary(extraRVs);
//merge them
RouteValueDictionary m = RouteValues.MergeRouteValues(r, extra);
return helper.ActionLink(linktext, action, controller, m, htmlAtts).ToHtmlString();
}
this works perfect, but I now added SecurityAware Actionlinks.
so
return helper.ActionLink(linktext, action, controller, m, htmlAtts).ToHtmlString();
becomes
return helper.SecurityTrimmedActionLink(linktext, action, controller, m, htmlAtts);
which then calls:
public static string SecurityTrimmedActionLink(this HtmlHelper htmlHelper, string linkText, string action, string controller, object extraRVs, object htmlAttributes) {
return SecurityTrimmedActionLink(htmlHelper, linkText, action, controller, extraRVs, htmlAttributes, false);
}
public static string SecurityTrimmedActionLink(this HtmlHelper htmlHelper, string linkText, string action, string controller, object extraRVs, object htmlAttributes, bool showDisabled) {
if (controller == null) {
RouteData routeData = htmlHelper.ViewContext.RouteData;
controller = routeData.GetRequiredString("controller");
}
if (IsAccessibleToUser(action, controller)) {
return htmlHelper.ActionLink(linkText, action, controller, extraRVs, htmlAttributes).ToHtmlString();
} else {
return showDisabled ? String.Format("<span>{0}</span>", linkText) : "";
}
}
Now this does NOT work. it compiles, but my URL looks not good.
<a count="3" keys="System.Collections.Generic.Dictionary`2+KeyCollection[System.String,System.Object]" values="System.Collections.Generic.Dictionary`2+ValueCollection[System.String,System.Object]" href="/2011-2012/Instelling?Count=3&Keys=System.Collections.Generic.Dictionary%602%2BKeyCollection%5BSystem.String%2CSystem.Object%5D&Values=System.Collections.Generic.Dictionary%602%2BValueCollection%5BSystem.String%2CSystem.Object%5D">Back to List</a>
as you see, what previously did work, now doesnt because it takes the RouteValueDictionarys as objects, which gives me not the result I want.
so I thought, What if i make them RouteValueDictionarys again.
if (IsAccessibleToUser(action, controller)) {
RouteValueDictionary parsedextraRVs = null;
if (extraRVs != null && extraRVs.GetType().Name == "RouteValueDictionary") {
parsedextraRVs = (RouteValueDictionary)extraRVs;
}
RouteValueDictionary parsedHtmlAttributes = null;
if (htmlAttributes != null && htmlAttributes.GetType().Name == "RouteValueDictionary") {
parsedHtmlAttributes = (RouteValueDictionary)htmlAttributes;
}
return htmlHelper.ActionLink(linkText, action, controller, parsedextraRVs == null ? extraRVs : parsedextraRVs, parsedHtmlAttributes == null ? htmlAttributes : parsedHtmlAttributes).ToHtmlString();
}
but this too gives me the url i just posted above.
Why did this work in my ActionLinkwParams method, but not when the ActionLinkwParams calls the SecurityTrimmedActionLink method? and how do I fix this?

Modify the signature of the SecurityTrimmedActionLink method to this:
public static string SecurityTrimmedActionLink(
this HtmlHelper htmlHelper,
string linkText,
string action,
string controller,
RouteValueDictionary extraRVs,
RouteValueDictionary htmlAttributes
)
Notice the difference between this and this. In your case (the one that doesn't work) you are calling the second overload taking objects but in your case you are not passing anonymous objects but a RouteValueDictionary which is treated as if it was an anonymous object and its public properties (Count, Keys, Values) are serialized as attributes.
Remark: Your helper methods are not correct. They return strings. This is not how it is supposed to be. Helper methods should return MvcHtmlString.
So instead of
public static string ActionLinkwParams(...)
{
...
return helper.ActionLink(linktext, action, controller, m, htmlAtts).ToHtmlString();
}
it should be:
public static MvcHtmlString ActionLinkwParams(...)
{
...
return helper.ActionLink(linktext, action, controller, m, htmlAtts);
}

So what i did in the end (thanks Darin) is made some extra overloads to make this work.
public static MvcHtmlString SecurityTrimmedActionLink(this HtmlHelper htmlHelper, string linkText, string action) {
return SecurityTrimmedActionLink(htmlHelper, linkText, action, null, null);
}
public static MvcHtmlString SecurityTrimmedActionLink(this HtmlHelper htmlHelper, string linkText, string action, string controller) {
return SecurityTrimmedActionLink(htmlHelper, linkText, action, controller, null);
}
public static MvcHtmlString SecurityTrimmedActionLink(this HtmlHelper htmlHelper, string linkText, string action, object extraRVs) {
return SecurityTrimmedActionLink(htmlHelper, linkText, action, null, extraRVs, null);
}
public static MvcHtmlString SecurityTrimmedActionLink(this HtmlHelper htmlHelper, string linkText, string action, string controller, object extraRVs) {
return SecurityTrimmedActionLink(htmlHelper, linkText, action, controller, extraRVs, null);
}
public static MvcHtmlString SecurityTrimmedActionLink(this HtmlHelper htmlHelper, string linkText, string action, RouteValueDictionary extraRVs) {
return SecurityTrimmedActionLink(htmlHelper, linkText, action, null, extraRVs, null);
}
public static MvcHtmlString SecurityTrimmedActionLink(this HtmlHelper htmlHelper, string linkText, string action, string controller, RouteValueDictionary extraRVs) {
return SecurityTrimmedActionLink(htmlHelper, linkText, action, controller, extraRVs, null);
}
public static MvcHtmlString SecurityTrimmedActionLink(this HtmlHelper htmlHelper, string linkText, string action, string controller, object extraRVs, object htmlAttributes) {
RouteValueDictionary rv = new RouteValueDictionary(extraRVs);
RouteValueDictionary html = new RouteValueDictionary(htmlAttributes);
return SecurityTrimmedActionLink(htmlHelper, linkText, action, controller, extraRVs, html);
}
public static MvcHtmlString SecurityTrimmedActionLink(this HtmlHelper htmlHelper, string linkText, string action, string controller, RouteValueDictionary extraRVs, IDictionary<String, Object> htmlAttributes) {
if (controller == null) {
RouteData routeData = htmlHelper.ViewContext.RouteData;
controller = routeData.GetRequiredString("controller");
}
if (IsAccessibleToUser(action, controller)) {
return htmlHelper.ActionLink(linkText, action, controller, extraRVs, htmlAttributes);
} else {
return MvcHtmlString.Empty;
}
}

Related

Is there an existing MVC HtmlHelper like #Html.Label that generates a <span>?

I'm wondering if I need to create a custom HtmlHelper to simply display a value as a span.
Currently I'm using:
#model string
#{
var htmlAttributes = HtmlHelper.AnonymousObjectToHtmlAttributes(ViewData["htmlAttributes"]);
}
<span class="#htmlAttributes["class"]">#Model</string>
I'm just wondering if there already exists something like #Html.Label so I could do:
#model string
#Html.SomethingLikeSpan("", Model, #ViewData["htmlAttributes"])
Not yet, but you can extend the Helpers object with any tag combination you wish.
Add a class file named HtmlHelperExtensions.cs with the following code:
using System;
using System.Web;
using System.Web.Mvc;
namespace WebApplication1
{
public static class HtmlHelperExtensions
{
public static IHtmlString Span(this HtmlHelper Helper, string Content, string Class = "")
{
string classstring = Class == "" ? "" : string.Format(" class=\"{0}\" ", Class);
string htmlString = String.Format("<span{1}>{0}</span>", Content, classstring);
return new HtmlString(htmlString);
}
}
}
Then in your view, use the following to format anything with the new helper extension:
#using WebApplication1
#Html.Span("Test Content")
#Html.Span("Test with class", "btn btn-primary")
I know this post is quite old but I'm going to post what I used to accomplish a very basic version of the #Html.Span and #Html.SpanFor helpers. I'll post for both .netcore and MVC5. I hope this helps people out!
MVC 5 implementation
using System;
using System.Linq.Expressions;
using System.Web.Mvc;
public static class HMTLHelperExtensions
{
public static MvcHtmlString Span(this HtmlHelper Helper, string Name, string Content, object HtmlAttributes)
{
var htmlAttributes = HtmlHelper.AnonymousObjectToHtmlAttributes(HtmlAttributes);
TagBuilder tag = new TagBuilder("Span");
tag.MergeAttribute("name", TagBuilder.CreateSanitizedId(Name));
tag.GenerateId(Name);
tag.InnerHtml = Content;
foreach(var i in htmlAttributes )
{
tag.MergeAttribute(i.Key, i.Value.ToString());
}
return MvcHtmlString.Create(tag.ToString());
}
public static MvcHtmlString Span(this HtmlHelper Helper, string Name, string Content)
{
TagBuilder tag = new TagBuilder("Span");
tag.MergeAttribute("name", TagBuilder.CreateSanitizedId(Name));
tag.GenerateId(Name);
tag.InnerHtml = Content;
return MvcHtmlString.Create(tag.ToString());
}
public static MvcHtmlString SpanFor<TModel, TProperty>(this HtmlHelper<TModel> Helper, Expression<Func<TModel, TProperty>> expression, string Content, object HtmlAttributes)
{
var name = ExpressionHelper.GetExpressionText(expression);
var metaData = ModelMetadata.FromLambdaExpression(expression, Helper.ViewData);
return Span(Helper, name, metaData.Model as string, HtmlAttributes);
}
public static MvcHtmlString SpanFor<TModel, TProperty>(this HtmlHelper<TModel> Helper, Expression<Func<TModel, TProperty>> expression, string Content)
{
var name = ExpressionHelper.GetExpressionText(expression);
var metaData = ModelMetadata.FromLambdaExpression(expression, Helper.ViewData);
return Span(Helper, name, metaData.Model as string);
}
}
and here is the .netcore implementation
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
using System;
using System.Linq.Expressions;
public static class HtmlHelperExtensions
{
public static IHtmlContent Span(this IHtmlHelper htmlHelper, string name, string Content, object htmlAttributes)
{
TagBuilder tag = new TagBuilder("Span");
tag.MergeAttribute("name", TagBuilder.CreateSanitizedId(name,""));
tag.GenerateId(name, "");
tag.InnerHtml.Append(Content);
var attributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
foreach (var i in attributes)
{
tag.MergeAttribute(i.Key, i.Value.ToString());
}
return tag;
}
public static IHtmlContent Span(this IHtmlHelper htmlHelper, string name, string Content)
{
TagBuilder tag = new TagBuilder("Span");
tag.MergeAttribute("name", TagBuilder.CreateSanitizedId(name, ""));
tag.GenerateId(name, "");
tag.InnerHtml.Append(Content);
return tag;
}
public static IHtmlContent SpanFor<TModel, TProperty>(this IHtmlHelper<TModel> htmlHelper, Expression<Func<TModel,TProperty>> expression, string content, object htmlAttributes)
{
var modelExplorer = ExpressionMetadataProvider.FromLambdaExpression(expression, htmlHelper.ViewData, htmlHelper.MetadataProvider);
var name = ExpressionHelper.GetExpressionText(expression);
var metaData = modelExplorer.Metadata;
return Span(htmlHelper, name, modelExplorer.Model as string, htmlAttributes);
}
public static IHtmlContent SpanFor<TModel, TProperty>(this IHtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string content)
{
var modelExplorer = ExpressionMetadataProvider.FromLambdaExpression(expression, htmlHelper.ViewData, htmlHelper.MetadataProvider);
var name = ExpressionHelper.GetExpressionText(expression);
var metaData = modelExplorer.Metadata;
return Span(htmlHelper, name, modelExplorer.Model as string);
}
}
I want to point out, these will work with html attributes only if used in this format.
#Html.Span("Test", "Testing Content", new { #class = "test-class", style = "text-align:right" }
#Html.SpanFor(x=>x.ModelProperty, Model.ModelProperty, new { #class = "test-class", style = "text-align:right" }

Failover to an alternate View when Partial View is not found?

I have an MVC app that uses dynamic business objects that are inherited from a parent object type. For example the base class Client might have two sub classes called Vendor and ServiceProvider, and these are all handled by the same controller. I have a partial view that I load on the right side of the page when viewing the client's details called _Aside.cshtml. When I load the client I try to look for a specific Aside first and failing that I load a generic one. Below is what the code looks like.
#try
{
#Html.Partial("_" + Model.Type.TypeName + "Aside")
}
catch (InvalidOperationException ex)
{
#Html.Partial("_Aside")
}
The TypeName property would have the word "Vendor" or "ServiceProvider" in it.
Now this works fine but the problem is I only want it to fail over if the view is not found, It's also failing over when there is an actual InvalidOperationException thrown by the partial view (usually the result of a child action it might call). I've thought about checking against Exception.Message but that seems a bit hackish. Is there some other way I can get the desired result without having to check the Message property or is that my only option at this point?
ex.Message = "The partial view '_ServiceProviderAside' was not found or no view
engine supports the searched locations. The following locations were
searched: (... etc)"
UPDATE: This is the class with extension methods I have currently in my project based off of Jack's answer, and Chao's suggestions as well.
//For ASP.NET MVC
public static class ViewExtensionMethods
{
public static bool PartialExists(this HtmlHelper helper, string viewName)
{
if (string.IsNullOrEmpty(viewName)) throw new ArgumentNullException(viewName, "View name cannot be empty");
var view = ViewEngines.Engines.FindPartialView(helper.ViewContext, viewName);
return view.View != null;
}
public static bool PartialExists(this ControllerContext controllerContext, string viewName)
{
if (string.IsNullOrEmpty(viewName)) throw new ArgumentNullException(viewName, "View name cannot be empty");
var view = ViewEngines.Engines.FindPartialView(controllerContext, viewName);
return view.View != null;
}
public static MvcHtmlString OptionalPartial(this HtmlHelper helper, string viewName)
{
return PartialExists(helper, viewName) ? helper.Partial(viewName) : HtmlString.Empty;
}
public static MvcHtmlString OptionalPartial(this HtmlHelper helper, string viewName, string fallbackViewName)
{
return OptionalPartial(helper, viewName, fallbackViewName, null);
}
public static MvcHtmlString OptionalPartial(this HtmlHelper helper, string viewName, object model)
{
return PartialExists(helper, viewName) ? helper.Partial(viewName, model) : MvcHtmlString.Empty;
}
public static MvcHtmlString OptionalPartial(this HtmlHelper helper, string viewName, string fallbackViewName, object model)
{
return helper.Partial(PartialExists(helper, viewName) ? viewName : fallbackViewName, model);
}
public static void RenderOptionalPartial(this HtmlHelper helper, string viewName)
{
if (PartialExists(helper, viewName))
{
helper.RenderPartial(viewName);
}
}
public static void RenderOptionalPartial(this HtmlHelper helper, string viewName, string fallbackViewName)
{
helper.RenderPartial(PartialExists(helper, viewName) ? viewName : fallbackViewName);
}
}
UPDATE: If you happen to be using ASP.NET Core MVC, swap the PartialExists() methods for these three methods, and change all of the usages of HtmlHelper for IHtmlHelper in the other methods. Skip this if you're not using ASP.NET Core
//For ASP.NET Core MVC
public static class ViewExtensionMethods
{
public static bool PartialExists(this IHtmlHelper helper, string viewName)
{
var viewEngine = helper.ViewContext.HttpContext.RequestServices.GetService<ICompositeViewEngine>();
if (string.IsNullOrEmpty(viewName)) throw new ArgumentNullException(viewName, "View name cannot be empty");
var view = viewEngine.FindView(helper.ViewContext, viewName, false);
return view.View != null;
}
public static bool PartialExists(this ControllerContext controllerContext, string viewName)
{
var viewEngine = controllerContext.HttpContext.RequestServices.GetService<ICompositeViewEngine>();
if (string.IsNullOrEmpty(viewName)) throw new ArgumentNullException(viewName, "View name cannot be empty");
var view = viewEngine.FindView(controllerContext, viewName, false);
return view.View != null;
}
public static bool PartialExists(this ViewContext viewContext, string viewName)
{
var viewEngine = viewContext.HttpContext.RequestServices.GetService<ICompositeViewEngine>();
if (string.IsNullOrEmpty(viewName)) throw new ArgumentNullException(viewName, "View name cannot be empty");
var view = viewEngine.FindView(viewContext, viewName, false);
return view.View != null;
}
}
In my view...
#Html.OptionalPartial("_" + Model.Type.TypeName + "Aside", "_Aside")
//or
#Html.OptionalPartial("_" + Model.Type.TypeName + "Aside", "_Aside", Model.AsideViewModel)
Came across this answer while trying to solve the problem of nested sections as I wanted to include styles and scripts in an intermediate view. I ended up deciding the simplest approach was convention of templatename_scripts and templatename_styles.
So just to add to the various options here is what I'm using based on this.
public static class OptionalPartialExtensions
{
public static MvcHtmlString OptionalPartial(this HtmlHelper helper, string viewName)
{
return PartialExists(helper, viewName) ? helper.Partial(viewName) : MvcHtmlString.Empty;
}
public static MvcHtmlString OptionalPartial(this HtmlHelper helper, string viewName, string fallbackViewName)
{
return helper.Partial(PartialExists(helper, viewName) ? viewName : fallbackViewName);
}
public static void RenderOptionalPartial(this HtmlHelper helper, string viewName)
{
if (PartialExists(helper, viewName))
{
helper.RenderPartial(viewName);
}
}
public static void RenderOptionalPartial(this HtmlHelper helper, string viewName, string fallbackViewName)
{
helper.RenderPartial(PartialExists(helper, viewName) ? viewName : fallbackViewName);
}
public static bool PartialExists(this HtmlHelper helper, string viewName)
{
if (string.IsNullOrEmpty(viewName))
{
throw new ArgumentNullException(viewName, "View name cannot be empty");
}
var view = ViewEngines.Engines.FindPartialView(helper.ViewContext, viewName);
return view.View != null;
}
}
This brings the my most common use cases in to the extension methods helping keep the views that bit cleaner, the RenderPartials were added for completeness.
I had a similar requirement. I wanted to keep the view markup cleaner and also to avoid generating the dynamic view name twice. This is what I came up with (modified to match your example):
Helper extension:
public static string FindPartial(this HtmlHelper html, string typeName)
{
// If you wanted to keep it in the view, you could move this concatenation out:
string viewName = "_" + typeName + "Aside";
ViewEngineResult result = ViewEngines.Engines.FindPartialView(html.ViewContext, viewName);
if (result.View != null)
return viewName;
return "_Aside";
}
View:
#Html.Partial(Html.FindPartial(Model.Type.TypeName))
or with access to the Model within the partial :
#Html.Partial(Html.FindPartial(Model.Type.TypeName), Model)
You could try the FindPartialView method to check if the view exists. Something along these lines might work (untested):
public bool DoesViewExist(string name)
{
string viewName = "_" + Model.Type.TypeName + "Aside";
ViewEngineResult viewResult = ViewEngines.Engines.FindPartialView(ControllerContext, viewName , null);
return (viewResult.View != null);
}
Info on the FindPartialView method for ASP MVC 3
Bug fix to handle null viewName or null fallbackViewName (replace appropriate code in OP):
public static MvcHtmlString OptionalPartial(this HtmlHelper helper, string viewName, string fallbackViewName, object model)
{
string partialToRender = null;
if (viewName != null && PartialExists(helper, viewName))
{
partialToRender = viewName;
}
else if (fallbackViewName != null && PartialExists(helper, fallbackViewName))
{
partialToRender = fallbackViewName;
}
if (partialToRender != null)
{
return helper.Partial(partialToRender, model);
}
else
{
return MvcHtmlString.Empty;
}
}
I have edited the OP's code (which combines code from multiple answers), but my edit is pending peer review.

Stronglytyped html helper with different model for get and post

If a Get Action returns a View with a "Car" model. The view displays info from the object and takes input to post within a form to another action that takes an object of type "Payment"
The Model on the view is of type Car and gives me stronglytyped html support and some other features like displaytext. But for posting I there is no Htmlhelper support like TextBox(x => x.amount I need to make it like #Html.TextBox("Amount"...
Its possible, but is this the only option?
You can do this:
#{
var paymentHtml = Html.HtmlHelperFor<Payment>();
}
#paymentHtml.EditorFor(p => p.Amount)
with this extension method:
public static class HtmlHelperFactoryExtensions {
public static HtmlHelper<TModel> HtmlHelperFor<TModel>(this HtmlHelper htmlHelper) {
return HtmlHelperFor(htmlHelper, default(TModel));
}
public static HtmlHelper<TModel> HtmlHelperFor<TModel>(this HtmlHelper htmlHelper, TModel model) {
return HtmlHelperFor(htmlHelper, model, null);
}
public static HtmlHelper<TModel> HtmlHelperFor<TModel>(this HtmlHelper htmlHelper, TModel model, string htmlFieldPrefix) {
var viewDataContainer = CreateViewDataContainer(htmlHelper.ViewData, model);
TemplateInfo templateInfo = viewDataContainer.ViewData.TemplateInfo;
if (!String.IsNullOrEmpty(htmlFieldPrefix))
templateInfo.HtmlFieldPrefix = templateInfo.GetFullHtmlFieldName(htmlFieldPrefix);
ViewContext viewContext = htmlHelper.ViewContext;
ViewContext newViewContext = new ViewContext(viewContext.Controller.ControllerContext, viewContext.View, viewDataContainer.ViewData, viewContext.TempData, viewContext.Writer);
return new HtmlHelper<TModel>(newViewContext, viewDataContainer, htmlHelper.RouteCollection);
}
static IViewDataContainer CreateViewDataContainer(ViewDataDictionary viewData, object model) {
var newViewData = new ViewDataDictionary(viewData) {
Model = model
};
newViewData.TemplateInfo = new TemplateInfo {
HtmlFieldPrefix = newViewData.TemplateInfo.HtmlFieldPrefix
};
return new ViewDataContainer {
ViewData = newViewData
};
}
class ViewDataContainer : IViewDataContainer {
public ViewDataDictionary ViewData { get; set; }
}
}
If I understand your question correctly, here's some code I just wrote for one of my projects to do something similar. It doesn't require anything special like what was suggested by Max Toro.
#{
var teamHelper = new HtmlHelper<Team>(ViewContext, this);
}
#using (teamHelper.BeginForm())
{
#teamHelper.LabelFor(p => p.Name)
#teamHelper.EditorFor(p => p.Name)
}
Adding to the implementation by Max Toro, here are a couple more for when you have a non-null model but don't have static type information (these two methods need to be embedded into the implementation Max provides).
These methods work well when you have dynamically retrieved property names for a model and need to call the non-generic HtmlHelper methods that take a name instead of an expression:
#Html.TextBox(propertyName)
for example.
public static HtmlHelper HtmlHelperFor( this HtmlHelper htmlHelper, object model )
{
return HtmlHelperFor( htmlHelper, model, null );
}
public static HtmlHelper HtmlHelperFor( this HtmlHelper htmlHelper, object model, string htmlFieldPrefix )
{
var t = model.GetType();
var viewDataContainer = CreateViewDataContainer( htmlHelper.ViewData, model );
TemplateInfo templateInfo = viewDataContainer.ViewData.TemplateInfo;
if( !String.IsNullOrEmpty( htmlFieldPrefix ) )
templateInfo.HtmlFieldPrefix = templateInfo.GetFullHtmlFieldName( htmlFieldPrefix );
ViewContext viewContext = htmlHelper.ViewContext;
ViewContext newViewContext = new ViewContext( viewContext.Controller.ControllerContext, viewContext.View, viewDataContainer.ViewData, viewContext.TempData, viewContext.Writer );
var gt = typeof( HtmlHelper<> ).MakeGenericType( t );
return Activator.CreateInstance( gt, newViewContext, viewDataContainer, htmlHelper.RouteCollection ) as HtmlHelper;
}
For ASP.NET Core 2
public static class HtmlHelperFactoryExtensions
{
public static IHtmlHelper<TModel> HtmlHelperFor<TModel>(this IHtmlHelper htmlHelper)
{
return HtmlHelperFor(htmlHelper, default(TModel));
}
public static IHtmlHelper<TModel> HtmlHelperFor<TModel>(this IHtmlHelper htmlHelper, TModel model)
{
return HtmlHelperFor(htmlHelper, model, null);
}
public static IHtmlHelper<TModel> HtmlHelperFor<TModel>(this IHtmlHelper htmlHelper, TModel model, string htmlFieldPrefix)
{
ViewDataDictionary<TModel> newViewData;
var runtimeType = htmlHelper.ViewData.ModelMetadata.ModelType;
if (runtimeType != null && typeof(TModel) != runtimeType && typeof(TModel).IsAssignableFrom(runtimeType))
{
newViewData = new ViewDataDictionary<TModel>(htmlHelper.ViewData, model);
}
else
{
newViewData = new ViewDataDictionary<TModel>(htmlHelper.MetadataProvider, new ModelStateDictionary())
{
Model = model
};
}
if (!String.IsNullOrEmpty(htmlFieldPrefix))
newViewData.TemplateInfo.HtmlFieldPrefix = newViewData.TemplateInfo.GetFullHtmlFieldName(htmlFieldPrefix);
ViewContext newViewContext = new ViewContext(htmlHelper.ViewContext, htmlHelper.ViewContext.View, newViewData, htmlHelper.ViewContext.Writer);
var newHtmlHelper = htmlHelper.ViewContext.HttpContext.RequestServices.GetRequiredService<IHtmlHelper<TModel>>();
((HtmlHelper<TModel>)newHtmlHelper).Contextualize(newViewContext);
return newHtmlHelper;
}
}
If I understand your problem correctly, try:
#Html.EditorFor(x => x.Amount)
You could also create an editor template for Payment. See this page for details on doing this.
If I'm misunderstanding, some sample code might help.

Can I add a CSS class definition to the Html.LabelFor in MVC3?

I hope someone out there has some ideas. I would like to tidy up my code and so I already used the Html.LabelFor. However now I want to assign a CSS class to the labels.
Html.LabelFor(model => model.Customer.Description ????)
Does anyone out there know if this is possible in MVC3. Note it's MVC3 I am using. Already I saw a post that talked about MVC2 and there being no simple solution.
Here you go buddy-o:
namespace System.Web.Mvc.Html
{
using System;
using Collections.Generic;
using Linq;
using Linq.Expressions;
using Mvc;
public static class LabelExtensions
{
public static MvcHtmlString LabelFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, object htmlAttributes)
{
return html.LabelFor(expression, null, htmlAttributes);
}
public static MvcHtmlString LabelFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string labelText, object htmlAttributes)
{
return html.LabelHelper(
ModelMetadata.FromLambdaExpression(expression, html.ViewData),
ExpressionHelper.GetExpressionText(expression),
HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes),
labelText);
}
private static MvcHtmlString LabelHelper(this HtmlHelper html, ModelMetadata metadata, string htmlFieldName, IDictionary<string, object> htmlAttributes, string labelText = null)
{
var str = labelText
?? (metadata.DisplayName
?? (metadata.PropertyName
?? htmlFieldName.Split(new[] { '.' }).Last()));
if (string.IsNullOrEmpty(str))
return MvcHtmlString.Empty;
var tagBuilder = new TagBuilder("label");
tagBuilder.MergeAttributes(htmlAttributes);
tagBuilder.Attributes.Add("for", TagBuilder.CreateSanitizedId(html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(htmlFieldName)));
tagBuilder.SetInnerText(str);
return tagBuilder.ToMvcHtmlString(TagRenderMode.Normal);
}
private static MvcHtmlString ToMvcHtmlString(this TagBuilder tagBuilder, TagRenderMode renderMode)
{
return new MvcHtmlString(tagBuilder.ToString(renderMode));
}
}
}
There is no built in way to do this in MVC 3. You will have to write your helper that does this. Take a look at the LabelExtensions class to see how it is done.

Support for optgroup in dropdownlist .NET MVC?

Continuing on from this question programmatically creating a drop down list I would like my list to have several optgroup lists too. Is this currently possible?
I know I need to pass a selectList to a dropDownList but do not know how to add text, value, optgroup to the selectList.
I want the end result to produce:
<option value="">Please select</option>
<optgroup label="Option A">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
</optgroup>
<optgroup label="Option B">
<option value="a">A</option>
<option value="b">B</option>
<option value="c">C</option>
</optgroup>
</option>
My extension is a bit more complicated, but it has all overloads as original DropDownList has.
Actually I've created it specially to be able to produce DropDownList with groups and to be transformed in to two joined selects with help of jDoubleSelect
using System;using System.Collections;using System.Collections.Generic;using System.Globalization; using System.Linq;using System.Linq.Expressions;using System.Text; using System.Web;using System.Web.Mvc;using System.Web.Routing;
public class GroupedSelectListItem : SelectListItem
{
public string GroupKey { get; set; }
public string GroupName { get; set; }
}
public static class HtmlHelpers
{
public static MvcHtmlString DropDownGroupList(this HtmlHelper htmlHelper, string name)
{
return DropDownListHelper(htmlHelper, name, null, null, null);
}
public static MvcHtmlString DropDownGroupList(this HtmlHelper htmlHelper, string name, IEnumerable<GroupedSelectListItem> selectList)
{
return DropDownListHelper(htmlHelper, name, selectList, null, null);
}
public static MvcHtmlString DropDownGroupList(this HtmlHelper htmlHelper, string name, string optionLabel)
{
return DropDownListHelper(htmlHelper, name, null, optionLabel, null);
}
public static MvcHtmlString DropDownGroupList(this HtmlHelper htmlHelper, string name, IEnumerable<GroupedSelectListItem> selectList, IDictionary<string, object> htmlAttributes)
{
return DropDownListHelper(htmlHelper, name, selectList, null, htmlAttributes);
}
public static MvcHtmlString DropDownGroupList(this HtmlHelper htmlHelper, string name, IEnumerable<GroupedSelectListItem> selectList, object htmlAttributes)
{
return DropDownListHelper(htmlHelper, name, selectList, null, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
}
public static MvcHtmlString DropDownGroupList(this HtmlHelper htmlHelper, string name, IEnumerable<GroupedSelectListItem> selectList, string optionLabel)
{
return DropDownListHelper(htmlHelper, name, selectList, optionLabel, null);
}
public static MvcHtmlString DropDownGroupList(this HtmlHelper htmlHelper, string name, IEnumerable<GroupedSelectListItem> selectList, string optionLabel, IDictionary<string, object> htmlAttributes)
{
return DropDownListHelper(htmlHelper, name, selectList, optionLabel, htmlAttributes);
}
public static MvcHtmlString DropDownGroupList(this HtmlHelper htmlHelper, string name, IEnumerable<GroupedSelectListItem> selectList, string optionLabel, object htmlAttributes)
{
return DropDownListHelper(htmlHelper, name, selectList, optionLabel, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
}
public static MvcHtmlString DropDownGroupListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<GroupedSelectListItem> selectList)
{
return DropDownGroupListFor(htmlHelper, expression, selectList, null /* optionLabel */, null /* htmlAttributes */);
}
public static MvcHtmlString DropDownGroupListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<GroupedSelectListItem> selectList, object htmlAttributes)
{
return DropDownGroupListFor(htmlHelper, expression, selectList, null /* optionLabel */, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
}
public static MvcHtmlString DropDownGroupListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<GroupedSelectListItem> selectList, IDictionary<string, object> htmlAttributes)
{
return DropDownGroupListFor(htmlHelper, expression, selectList, null /* optionLabel */, htmlAttributes);
}
public static MvcHtmlString DropDownGroupListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<GroupedSelectListItem> selectList, string optionLabel)
{
return DropDownGroupListFor(htmlHelper, expression, selectList, optionLabel, null /* htmlAttributes */);
}
public static MvcHtmlString DropDownGroupListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<GroupedSelectListItem> selectList, string optionLabel, object htmlAttributes)
{
return DropDownGroupListFor(htmlHelper, expression, selectList, optionLabel, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
}
public static MvcHtmlString DropDownGroupListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<GroupedSelectListItem> selectList, string optionLabel, IDictionary<string, object> htmlAttributes)
{
if (expression == null)
{
throw new ArgumentNullException("expression");
}
return DropDownListHelper(htmlHelper, ExpressionHelper.GetExpressionText(expression), selectList, optionLabel, htmlAttributes);
}
private static MvcHtmlString DropDownListHelper(HtmlHelper htmlHelper, string expression, IEnumerable<GroupedSelectListItem> selectList, string optionLabel, IDictionary<string, object> htmlAttributes)
{
return SelectInternal(htmlHelper, optionLabel, expression, selectList, false /* allowMultiple */, htmlAttributes);
}
// Helper methods
private static IEnumerable<GroupedSelectListItem> GetSelectData(this HtmlHelper htmlHelper, string name)
{
object o = null;
if (htmlHelper.ViewData != null)
{
o = htmlHelper.ViewData.Eval(name);
}
if (o == null)
{
throw new InvalidOperationException(
String.Format(
CultureInfo.CurrentCulture,
"Missing Select Data"));
}
var selectList = o as IEnumerable<GroupedSelectListItem>;
if (selectList == null)
{
throw new InvalidOperationException(
String.Format(
CultureInfo.CurrentCulture,
"Wrong Select DataType"));
}
return selectList;
}
internal static string ListItemToOption(GroupedSelectListItem item)
{
var builder = new TagBuilder("option")
{
InnerHtml = HttpUtility.HtmlEncode(item.Text)
};
if (item.Value != null)
{
builder.Attributes["value"] = item.Value;
}
if (item.Selected)
{
builder.Attributes["selected"] = "selected";
}
return builder.ToString(TagRenderMode.Normal);
}
private static MvcHtmlString SelectInternal(this HtmlHelper htmlHelper, string optionLabel, string name, IEnumerable<GroupedSelectListItem> selectList, bool allowMultiple, IDictionary<string, object> htmlAttributes)
{
name = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);
if (String.IsNullOrEmpty(name))
{
throw new ArgumentException("Null Or Empty", "name");
}
bool usedViewData = false;
// If we got a null selectList, try to use ViewData to get the list of items.
if (selectList == null)
{
selectList = htmlHelper.GetSelectData(name);
usedViewData = true;
}
object defaultValue = (allowMultiple) ? htmlHelper.GetModelStateValue(name, typeof(string[])) : htmlHelper.GetModelStateValue(name, typeof(string));
// If we haven't already used ViewData to get the entire list of items then we need to
// use the ViewData-supplied value before using the parameter-supplied value.
if (!usedViewData)
{
if (defaultValue == null)
{
defaultValue = htmlHelper.ViewData.Eval(name);
}
}
if (defaultValue != null)
{
var defaultValues = (allowMultiple) ? defaultValue as IEnumerable : new[] { defaultValue };
var values = from object value in defaultValues select Convert.ToString(value, CultureInfo.CurrentCulture);
var selectedValues = new HashSet<string>(values, StringComparer.OrdinalIgnoreCase);
var newSelectList = new List<GroupedSelectListItem>();
foreach (var item in selectList)
{
item.Selected = (item.Value != null) ? selectedValues.Contains(item.Value) : selectedValues.Contains(item.Text);
newSelectList.Add(item);
}
selectList = newSelectList;
}
// Convert each ListItem to an <option> tag
var listItemBuilder = new StringBuilder();
// Make optionLabel the first item that gets rendered.
if (optionLabel != null)
{
listItemBuilder.AppendLine(ListItemToOption(new GroupedSelectListItem { Text = optionLabel, Value = String.Empty, Selected = false }));
}
foreach (var group in selectList.GroupBy(i => i.GroupKey))
{
string groupName = selectList.Where(i => i.GroupKey == group.Key).Select(it => it.GroupName).FirstOrDefault();
listItemBuilder.AppendLine(string.Format("<optgroup label=\"{0}\" value=\"{1}\">", groupName, group.Key));
foreach (GroupedSelectListItem item in group)
{
listItemBuilder.AppendLine(ListItemToOption(item));
}
listItemBuilder.AppendLine("</optgroup>");
}
var tagBuilder = new TagBuilder("select")
{
InnerHtml = listItemBuilder.ToString()
};
tagBuilder.MergeAttributes(htmlAttributes);
tagBuilder.MergeAttribute("name", name, true /* replaceExisting */);
tagBuilder.GenerateId(name);
if (allowMultiple)
{
tagBuilder.MergeAttribute("multiple", "multiple");
}
// If there are any errors for a named field, we add the css attribute.
ModelState modelState;
if (htmlHelper.ViewData.ModelState.TryGetValue(name, out modelState))
{
if (modelState.Errors.Count > 0)
{
tagBuilder.AddCssClass(HtmlHelper.ValidationInputCssClassName);
}
}
return MvcHtmlString.Create(tagBuilder.ToString());
}
internal static object GetModelStateValue(this HtmlHelper helper, string key, Type destinationType)
{
ModelState modelState;
if (helper.ViewData.ModelState.TryGetValue(key, out modelState))
{
if (modelState.Value != null)
{
return modelState.Value.ConvertTo(destinationType, null /* culture */);
}
}
return null;
}
}
This was added to ASP.Net MVC at version 5.2 and is now built-in.
The Group property on SelectListItem allows you to specify a group for each item:
New SelectList constructors also allow you to provide the name of the field that contains the group title on the supplied list of items.
The HtmlHelper DropDownList and DropDownListFor methods now generate optgroup elements based on the groups included on the list of items.
Easy!
I just write a extensions for do this, see it:
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Routing;
namespace System.Web.Mvc.Html
{
public static class GroupDropListExtensions
{
public static string GroupDropList(this HtmlHelper helper, string name, IEnumerable<GroupDropListItem> data, string SelectedValue, object htmlAttributes)
{
if (data == null && helper.ViewData != null)
data = helper.ViewData.Eval(name) as IEnumerable<GroupDropListItem>;
if (data == null) return string.Empty;
var select = new TagBuilder("select");
if (htmlAttributes != null)
select.MergeAttributes(new RouteValueDictionary(htmlAttributes));
select.GenerateId(name);
var optgroupHtml = new StringBuilder();
var groups = data.ToList();
foreach (var group in data)
{
var groupTag = new TagBuilder("optgroup");
groupTag.Attributes.Add("label", helper.Encode( group.Name));
var optHtml = new StringBuilder();
foreach (var item in group.Items)
{
var option = new TagBuilder("option");
option.Attributes.Add("value", helper.Encode(item.Value));
if (SelectedValue != null && item.Value == SelectedValue)
option.Attributes.Add("selected", "selected");
option.InnerHtml = helper.Encode(item.Text);
optHtml.AppendLine(option.ToString(TagRenderMode.Normal));
}
groupTag.InnerHtml = optHtml.ToString();
optgroupHtml.AppendLine(groupTag.ToString(TagRenderMode.Normal));
}
select.InnerHtml = optgroupHtml.ToString();
return select.ToString(TagRenderMode.Normal);
}
}
public class GroupDropListItem
{
public string Name { get; set; }
public List<OptionItem> Items { get; set; }
}
public class OptionItem
{
public string Text { get; set; }
public string Value { get; set; }
}
}
Looking through the code on www.codeplex.com/aspnet, it doesn't appear that neither the SelectList nor the DropDownList extension method supports the use of OptGroup in a select. Looks like you'll need to write your own extension method and extend SelectListItem to contain the grouping or generate the select manually in markup.
Data annotations for client validations missing?
In answer to Chrno Love's question above, adding to Serge Zab's solution - Milimetric's fix for MVC3 clientside validation attributes when using a collection of dropdowns in the same view:
public static MvcHtmlString DropDownGroupListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>>
expression,
IEnumerable<GroupedSelectListItem>
selectList, string optionLabel,
IDictionary<string, object> htmlAttributes)
{
if (expression == null)
{
throw new ArgumentNullException("expression");
}
// fixing clientside validation attributes
// http://stackoverflow.com/questions/4799958/asp-net-mvc-3-unobtrusive-client-validation-does-not-work-with-drop-down-lists/8102022#8102022
var metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
var mergedAttributes =
htmlHelper.GetUnobtrusiveValidationAttributes(ExpressionHelper.GetExpressionText(expression), metadata);
if (htmlAttributes != null)
{
foreach (PropertyDescriptor descriptor in TypeDescriptor.GetProperties(htmlAttributes))
{
object value = descriptor.GetValue(htmlAttributes);
mergedAttributes.Add(descriptor.Name, value);
}
}
//return DropDownListHelper(htmlHelper, ExpressionHelper.GetExpressionText(expression), selectList, optionLabel, htmlAttributes);
return DropDownListHelper(htmlHelper, ExpressionHelper.GetExpressionText(expression), selectList, optionLabel, mergedAttributes);
}
Serge Zab's answer was exactly what I was looking for. Being a die hard VB programmer I ported it to this VB module:
'based on Serge Zab's answer on http://stackoverflow.com/questions/607188/support-for-optgroup-in-dropdownlist-net-mvc
Imports System.Collections
Imports System.Collections.Generic
Imports System.Globalization
Imports System.Linq
Imports System.Linq.Expressions
Imports System.Text
Imports System.Web
Imports System.Web.Mvc
Imports System.Web.Routing
Public Class GroupedSelectListItem
Inherits SelectListItem
Public Property GroupKey() As String
Get
Return m_GroupKey
End Get
Set(value As String)
m_GroupKey = Value
End Set
End Property
Private m_GroupKey As String
Public Property GroupName() As String
Get
Return m_GroupName
End Get
Set(value As String)
m_GroupName = Value
End Set
End Property
Private m_GroupName As String
End Class
Public Module HtmlHelpers
<System.Runtime.CompilerServices.Extension> _
Public Function DropDownGroupList(htmlHelper As HtmlHelper, name As String) As MvcHtmlString
Return DropDownListHelper(htmlHelper, name, Nothing, Nothing, Nothing)
End Function
<System.Runtime.CompilerServices.Extension> _
Public Function DropDownGroupList(htmlHelper As HtmlHelper, name As String, selectList As IEnumerable(Of GroupedSelectListItem)) As MvcHtmlString
Return DropDownListHelper(htmlHelper, name, selectList, Nothing, Nothing)
End Function
<System.Runtime.CompilerServices.Extension> _
Public Function DropDownGroupList(htmlHelper As HtmlHelper, name As String, optionLabel As String) As MvcHtmlString
Return DropDownListHelper(htmlHelper, name, Nothing, optionLabel, Nothing)
End Function
<System.Runtime.CompilerServices.Extension> _
Public Function DropDownGroupList(htmlHelper As HtmlHelper, name As String, selectList As IEnumerable(Of GroupedSelectListItem), htmlAttributes As IDictionary(Of String, Object)) As MvcHtmlString
Return DropDownListHelper(htmlHelper, name, selectList, Nothing, htmlAttributes)
End Function
<System.Runtime.CompilerServices.Extension> _
Public Function DropDownGroupList(htmlHelper As HtmlHelper, name As String, selectList As IEnumerable(Of GroupedSelectListItem), htmlAttributes As Object) As MvcHtmlString
Return DropDownListHelper(htmlHelper, name, selectList, Nothing, New RouteValueDictionary(htmlAttributes))
End Function
<System.Runtime.CompilerServices.Extension> _
Public Function DropDownGroupList(htmlHelper As HtmlHelper, name As String, selectList As IEnumerable(Of GroupedSelectListItem), optionLabel As String) As MvcHtmlString
Return DropDownListHelper(htmlHelper, name, selectList, optionLabel, Nothing)
End Function
<System.Runtime.CompilerServices.Extension> _
Public Function DropDownGroupList(htmlHelper As HtmlHelper, name As String, selectList As IEnumerable(Of GroupedSelectListItem), optionLabel As String, htmlAttributes As IDictionary(Of String, Object)) As MvcHtmlString
Return DropDownListHelper(htmlHelper, name, selectList, optionLabel, htmlAttributes)
End Function
<System.Runtime.CompilerServices.Extension> _
Public Function DropDownGroupList(htmlHelper As HtmlHelper, name As String, selectList As IEnumerable(Of GroupedSelectListItem), optionLabel As String, htmlAttributes As Object) As MvcHtmlString
Return DropDownListHelper(htmlHelper, name, selectList, optionLabel, New RouteValueDictionary(htmlAttributes))
End Function
<System.Runtime.CompilerServices.Extension> _
Public Function DropDownGroupListFor(Of TModel, TProperty)(htmlHelper As HtmlHelper(Of TModel), expression As Expression(Of Func(Of TModel, TProperty)), selectList As IEnumerable(Of GroupedSelectListItem)) As MvcHtmlString
' optionLabel
' htmlAttributes
Return DropDownGroupListFor(htmlHelper, expression, selectList, Nothing, Nothing)
End Function
<System.Runtime.CompilerServices.Extension> _
Public Function DropDownGroupListFor(Of TModel, TProperty)(htmlHelper As HtmlHelper(Of TModel), expression As Expression(Of Func(Of TModel, TProperty)), selectList As IEnumerable(Of GroupedSelectListItem), htmlAttributes As Object) As MvcHtmlString
' optionLabel
Return DropDownGroupListFor(htmlHelper, expression, selectList, Nothing, New RouteValueDictionary(htmlAttributes))
End Function
<System.Runtime.CompilerServices.Extension> _
Public Function DropDownGroupListFor(Of TModel, TProperty)(htmlHelper As HtmlHelper(Of TModel), expression As Expression(Of Func(Of TModel, TProperty)), selectList As IEnumerable(Of GroupedSelectListItem), htmlAttributes As IDictionary(Of String, Object)) As MvcHtmlString
' optionLabel
Return DropDownGroupListFor(htmlHelper, expression, selectList, Nothing, htmlAttributes)
End Function
<System.Runtime.CompilerServices.Extension> _
Public Function DropDownGroupListFor(Of TModel, TProperty)(htmlHelper As HtmlHelper(Of TModel), expression As Expression(Of Func(Of TModel, TProperty)), selectList As IEnumerable(Of GroupedSelectListItem), optionLabel As String) As MvcHtmlString
' htmlAttributes
Return DropDownGroupListFor(htmlHelper, expression, selectList, optionLabel, Nothing)
End Function
<System.Runtime.CompilerServices.Extension> _
Public Function DropDownGroupListFor(Of TModel, TProperty)(htmlHelper As HtmlHelper(Of TModel), expression As Expression(Of Func(Of TModel, TProperty)), selectList As IEnumerable(Of GroupedSelectListItem), optionLabel As String, htmlAttributes As Object) As MvcHtmlString
Return DropDownGroupListFor(htmlHelper, expression, selectList, optionLabel, New RouteValueDictionary(htmlAttributes))
End Function
<System.Runtime.CompilerServices.Extension> _
Public Function DropDownGroupListFor(Of TModel, TProperty)(htmlHelper As HtmlHelper(Of TModel), expression As Expression(Of Func(Of TModel, TProperty)), selectList As IEnumerable(Of GroupedSelectListItem), optionLabel As String, htmlAttributes As IDictionary(Of String, Object)) As MvcHtmlString
If expression Is Nothing Then
Throw New ArgumentNullException("expression")
End If
Return DropDownListHelper(htmlHelper, ExpressionHelper.GetExpressionText(expression), selectList, optionLabel, htmlAttributes)
End Function
Private Function DropDownListHelper(htmlHelper As HtmlHelper, expression As String, selectList As IEnumerable(Of GroupedSelectListItem), optionLabel As String, htmlAttributes As IDictionary(Of String, Object)) As MvcHtmlString
' allowMultiple
Return SelectInternal(htmlHelper, optionLabel, expression, selectList, False, htmlAttributes)
End Function
' Helper methods
<System.Runtime.CompilerServices.Extension> _
Private Function GetSelectData(htmlHelper As HtmlHelper, name As String) As IEnumerable(Of GroupedSelectListItem)
Dim o As Object = Nothing
If htmlHelper.ViewData IsNot Nothing Then
o = htmlHelper.ViewData.Eval(name)
End If
If o Is Nothing Then
Throw New InvalidOperationException([String].Format(CultureInfo.CurrentCulture, "Missing Select Data", name, "IEnumerable<GroupedSelectListItem>"))
End If
Dim selectList As IEnumerable(Of GroupedSelectListItem) = TryCast(o, IEnumerable(Of GroupedSelectListItem))
If selectList Is Nothing Then
Throw New InvalidOperationException([String].Format(CultureInfo.CurrentCulture, "Wrong Select DataType", name, o.[GetType]().FullName, "IEnumerable<GroupedSelectListItem>"))
End If
Return selectList
End Function
Friend Function ListItemToOption(item As GroupedSelectListItem) As String
Dim builder As New TagBuilder("option") With { _
.InnerHtml = HttpUtility.HtmlEncode(item.Text) _
}
If item.Value IsNot Nothing Then
builder.Attributes("value") = item.Value
End If
If item.Selected Then
builder.Attributes("selected") = "selected"
End If
Return builder.ToString(TagRenderMode.Normal)
End Function
<System.Runtime.CompilerServices.Extension> _
Private Function SelectInternal(htmlHelper__1 As HtmlHelper, optionLabel As String, name As String, selectList As IEnumerable(Of GroupedSelectListItem), allowMultiple As Boolean, htmlAttributes As IDictionary(Of String, Object)) As MvcHtmlString
name = htmlHelper__1.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name)
If [String].IsNullOrEmpty(name) Then
Throw New ArgumentException("Null Or Empty", "name")
End If
Dim usedViewData As Boolean = False
' If we got a null selectList, try to use ViewData to get the list of items.
If selectList Is Nothing Then
selectList = htmlHelper__1.GetSelectData(name)
usedViewData = True
End If
Dim defaultValue As Object = If((allowMultiple), htmlHelper__1.GetModelStateValue(name, GetType(String())), htmlHelper__1.GetModelStateValue(name, GetType(String)))
' If we haven't already used ViewData to get the entire list of items then we need to
' use the ViewData-supplied value before using the parameter-supplied value.
If Not usedViewData Then
If defaultValue Is Nothing Then
defaultValue = htmlHelper__1.ViewData.Eval(name)
End If
End If
If defaultValue IsNot Nothing Then
Dim defaultValues As IEnumerable = If((allowMultiple), TryCast(defaultValue, IEnumerable), New String() {defaultValue})
Dim values As IEnumerable(Of String) = From value In defaultValues Select (Convert.ToString(value, CultureInfo.CurrentCulture))
Dim selectedValues As New HashSet(Of String)(values, StringComparer.OrdinalIgnoreCase)
Dim newSelectList As New List(Of GroupedSelectListItem)()
For Each item As GroupedSelectListItem In selectList
item.Selected = If((item.Value IsNot Nothing), selectedValues.Contains(item.Value), selectedValues.Contains(item.Text))
newSelectList.Add(item)
Next
selectList = newSelectList
End If
' Convert each ListItem to an <option> tag
Dim listItemBuilder As New StringBuilder()
' Make optionLabel the first item that gets rendered.
If optionLabel IsNot Nothing Then
listItemBuilder.AppendLine(ListItemToOption(New GroupedSelectListItem() With { _
.Text = optionLabel, _
.Value = [String].Empty, _
.Selected = False _
}))
End If
For Each group As Object In selectList.GroupBy(Function(i) i.GroupKey)
Dim groupName As String = selectList.Where(Function(i) i.GroupKey = group.Key).[Select](Function(it) it.GroupName).FirstOrDefault()
listItemBuilder.AppendLine(String.Format("<optgroup label=""{0}"" value=""{1}"">", groupName, group.Key))
For Each item As GroupedSelectListItem In group
listItemBuilder.AppendLine(ListItemToOption(item))
Next
listItemBuilder.AppendLine("</optgroup>")
Next
Dim tagBuilder As New TagBuilder("select") With { _
.InnerHtml = listItemBuilder.ToString() _
}
TagBuilder.MergeAttributes(htmlAttributes)
' replaceExisting
TagBuilder.MergeAttribute("name", name, True)
TagBuilder.GenerateId(name)
If allowMultiple Then
TagBuilder.MergeAttribute("multiple", "multiple")
End If
' If there are any errors for a named field, we add the css attribute.
Dim modelState As ModelState = Nothing
If htmlHelper__1.ViewData.ModelState.TryGetValue(name, modelState) Then
If modelState.Errors.Count > 0 Then
TagBuilder.AddCssClass(HtmlHelper.ValidationInputCssClassName)
End If
End If
Return MvcHtmlString.Create(TagBuilder.ToString())
End Function
<System.Runtime.CompilerServices.Extension> _
Friend Function GetModelStateValue(helper As HtmlHelper, key As String, destinationType As Type) As Object
Dim modelState As ModelState = Nothing
If helper.ViewData.ModelState.TryGetValue(key, modelState) Then
If modelState.Value IsNot Nothing Then
' culture
Return modelState.Value.ConvertTo(destinationType, Nothing)
End If
End If
Return Nothing
End Function
End Module
I have been trying #Serge Zab solution out which works well but was having some trouble with the unobtrusive validation, after some inspection I found the problem.
There seems to be some required attributes missing off the select element if you want the Jquery validation to fire, just after you create your TagBuilder
TagBuilder tagBuilder = new TagBuilder("select");
Add these attributes
tagBuilder.MergeAttribute("data-val", "true",true);
tagBuilder.MergeAttribute("data-val-required", "your validation message", true)
and the unobtrusive validation should fire.
In Serge Zab answer, data attributes, like data_valuename, dont marging in correct form data-valuename.
I replaced code tagBuilder.MergeAttributes(htmlAttributes); in SelectInternal method to this
foreach (var htmlAttribute in htmlAttributes)
{
tagBuilder.MergeAttribute(
htmlAttribute.Key.Replace('_', '-'),
(string)htmlAttribute.Value
);
}
Just a note on using Serge's extension to create more than one select list on the same form. I was having problems getting the second select list to apply the groups, and when I examined the html being generated I realized both had been given the same id. To fix this, go into the SelectInternal function in serge's extension and comment out/delete the following two lines:
tagBuilder.MergeAttribute("name", name, true /* replaceExisting */);
tagBuilder.GenerateId(name);
Alternatively you can just pass unique id's to each (though the DropDownGroupListFor doesn't take the 'string name' parameter so you'll have to add an overload that does)
I needed a solution to handle a multiple selection with optgroup, and I used Serge Zab solution. I've just two comments about it (too long for a comment).
Despite his claims, his solution does not support all existing overloads of DropDownListFor, since it doesn't support MultiSelectList or SelectList, oftenly used in models. But that's not hard to add these.
His solution was not working for me for Multiple selection to initialize selected/not selected items from model : the initial values were not affected.
I've just modified the following method :
private static MvcHtmlString DropDownListHelper(...)
{
return SelectInternal(htmlHelper, optionLabel, expression, selectList, false /* allowMultiple */, htmlAttributes);
}
To this :
private static MvcHtmlString DropDownListHelper(HtmlHelper htmlHelper, string expression, IEnumerable<GroupedSelectListItem> selectList, string optionLabel, IDictionary<string, object> htmlAttributes)
{
bool allowMultiple = htmlAttributes.ContainsKey("multiple");
return SelectInternal(htmlHelper, optionLabel, expression, selectList, allowMultiple, htmlAttributes);
}
And it worked as expected.
Of course the multiple attribute must be defined :
#Html.DropDownGroupListFor(m => m.Selected, Model.Values, new { multiple = "multiple" })
Thanks to Serge for his answer.

Resources