Model
namespace Models
{
public class PartialTestModel
{
public int? NullableProp { get; set; }
}
}
This works fine
<div class="highlight1">
#Html.DropDownListFor(m => m.NullableProp, new SelectList(Enumerable.Range(1, 10), Model.NullableProp), "-- Select one --")
</div>
However if a partial view is created for this property
#model System.Int32?
<div class="highlight1">
Model's value is
#if (#Model.HasValue)
{
#Model.ToString()
}
else
{
<text>Null</text>
}
#Html.DropDownListFor(m => m, new SelectList(Enumerable.Range(1, 10), Model), "-- Select one --")
</div>
It throws an error
Value cannot be null or empty.
Parameter name: name
[ArgumentException: Value cannot be null or empty.
Parameter name: name]
System.Web.Mvc.Html.SelectExtensions.SelectInternal(HtmlHelper htmlHelper, String optionLabel, String name, IEnumerable`1 selectList, Boolean allowMultiple, IDictionary`2 htmlAttributes) +396232
System.Web.Mvc.Html.SelectExtensions.DropDownListFor(HtmlHelper`1 htmlHelper, Expression`1 expression, IEnumerable`1 selectList, String optionLabel, IDictionary`2 htmlAttributes) +35
System.Web.Mvc.Html.SelectExtensions.DropDownListFor(HtmlHelper`1 htmlHelper, Expression`1 expression, IEnumerable`1 selectList, String optionLabel) +14
The View renders the partial as
#Html.Partial("_PartialTest", Model.NullableProp, new ViewDataDictionary())
new ViewDataDictionary() is added as per the answer in asp.net mvc renderpartial with null model gets passed the wrong type. Without this incorrect model is seen in the partial view when the property value is null. When the property values are not null it can be used
#Html.Partial("_PartialTest", Model.NullableProp)
but still results in the same error as pasted above.
The problem is that .DropDownListFor(m => m, ... uses the lambda expression to create the input's name. However, m => m doesn't contain a property, so there's no name.
It might work if you instead use m => m.Value, because since this is a lambda expression, it might not actually be executed.
Related
I have a form begun with the Html.BeginForm() helper method:
#using (Html.BeginForm(
null,null, new { #id = "MaintenanceForm", #class = "datatable", #nonvalidate="nonvalidate" }
))
and the form is rendered as:
<form action="(controller's name)/(current action's name)/MaintenanceForm?class=datatable" method="post">
The attributes such as id, class, and nonvalidate aren't assigned. Also I do not want a default Http Method. What can I do?
Your current code is matching with the below overload of BeginForm method
public static MvcForm BeginForm(
this HtmlHelper htmlHelper,
string actionName,
string controllerName,
object routeValues
)
The third parameter here is an object for the route values. These will be added as the querystring key value(s) to your form's action attribute value. That is the reason you are seeing those big url as the action attribute value.
If you want to specify the html attributes( Id,class etc), Use this overload which has a fourth parameter which takes the html attributes. The third parameter is the FormMethod.
public static MvcForm BeginForm(
this HtmlHelper htmlHelper,
string actionName,
string controllerName,
FormMethod method,
object htmlAttributes
)
This should work.
#using (Html.BeginForm("Create", "Post",FormMethod.Post,
new { #id = "MaintenanceForm", #class = "datatable", #nonvalidate = "nonvalidate" }))
{
}
Replace Create and Post with your action method name and controller name.
When I use CheckBoxFor() HtmlHelper like this:
#Html.CheckBoxFor(x => Model.IsDeleted, new { myCustomAttribute = "myCustomAttribute" })
my checkbox have attribute, but hidden element not have.
<input myCustomAttribute="myCustomAttribute" id="IsDeleted" name="IsDeleted" type="checkbox" value="true">
<input name="IsDeleted" type="hidden" value="false">
How add atribute to hidden element? I do not want to write custom HTML.
In my opinion the easiest way to achieve such functionality is to create custom helper :) Below simple helper from http://20fingers2brains.blogspot.com/2013/05/custom-checkbox-html-helper-in-mvc3.html :
public static class CustomCheckBoxHelper
{
//This helper accepts name attribute. This method in turns calls our second overload.
public static MvcHtmlString Custom_Checkbox(this HtmlHelper helper, string name)
{
return Custom_Checkbox(helper, name, false);
}
//This helper accepts name and isChecked boolean attribute.
public static MvcHtmlString Custom_Checkbox(this HtmlHelper helper, string name,bool isChecked)
{
return Custom_Checkbox(helper, name, isChecked, null);
}
//This overload accepts name, isChecked and htmlAttributes as parameter.
public static MvcHtmlString Custom_Checkbox(this HtmlHelper helper, string name,bool isChecked,object htmlAttributes)
{
//Creating input control using TagBuilder class.
TagBuilder checkbox = new TagBuilder("input");
//Setting its type attribute to checkbox to render checkbox control.
checkbox.Attributes.Add("type", "checkbox");
//Setting the name and id attribute.
checkbox.Attributes.Add("name", name);
checkbox.Attributes.Add("id", name);
//Adding the checked attibute based on parameter received.
if (isChecked)
checkbox.Attributes.Add("checked", "checked");
//Merging the other attributes passed in the attribute.
checkbox.MergeAttributes(new RouteValueDictionary(htmlAttributes));
return MvcHtmlString.Create(checkbox.ToString(TagRenderMode.Normal));
}
}
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
I know others have asked this question, but I'm totally confused by this:
This displays the dropdown with no values selected:
<%= Html.DropDownList("items", new MultiSelectList(Model.AvailableItems,
"id", "name", Model.items), new { multiple = "multiple" })%>
This displays the dropdown with the values that I'm passing in (Model.items) selected properly like what I'd expect:
<%= Html.DropDownList("somethingelse", new MultiSelectList(Model.AvailableItems,
"id", "name", Model.items), new { multiple = "multiple" })%>
But the problem is that this item is now named "somethingelse" when i POST. I know I can hack around this but what's going?
Too little context provided in your question but I will try to show a full working example:
Model:
public class Item
{
public int Id { get; set; }
public string Name { get; set; }
}
public class MyModel
{
public IEnumerable<int> SelectedItemIds { get; set; }
public IEnumerable<Item> AvailableItems {
get
{
return new[]
{
new Item { Id = 1, Name = "Item 1" },
new Item { Id = 2, Name = "Item 2" },
new Item { Id = 3, Name = "Item 3" },
};
}
}
}
Controller:
[HandleError]
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new MyModel
{
SelectedItemIds = new[] { 2, 3 }
};
return View(model);
}
[HttpPost]
public ActionResult Index(IEnumerable<int> selectedItemIds)
{
var model = new MyModel
{
// Important: Don't ever try to modify the selectedItemIds here
// The Html helper will completely ignore it and use
// the POSTed values
SelectedItemIds = selectedItemIds
};
return View(model);
}
}
View:
<% using (Html.BeginForm()) { %>
<%= Html.ListBoxFor(x => x.SelectedItemIds,
new MultiSelectList(Model.AvailableItems, "Id", "Name")) %>
<input type="submit" value="GO" />
<% } %>
Notice that the Html.ListBoxFor is more adapted if you want to generate a multiple select. Obviously the AvailableItems property should be fetched from a repository.
The problem you have is using Model.Items as a parameter. The code
<%= Html.DropDownList("items", new MultiSelectList(Model.AvailableItems,
"id", "name", Model.items), new { multiple = "multiple" })%>
isn't actually working as you would expect. It's working because the name of the dropdown is "items". That's because there was a form param called "items" posted back to your action. That param gets stored in the action's ViewState (don't confuse with ViewData).
The Html.DropdownList() sees that there is a ViewState param named the same as you have named your dropdown and uses that ViewState param to work out the selected values. It completely ignores the Model.items that you passed in.
If anyone can explain the logic of not being able to override the default behavior then I'd love to hear it.
So, that's your first problem. To get around it all you have to do is to rename the dropdown to something else - exactly like you did in your second example. Now your second problem comes into play: the list of selected items must be a collection of simple objects (I think it actually needs to be an IEnumerable but I'm not 100% sure).
The DropDownList() method will try and match those selected values to the Value in your AvailableItems collection. If it can't do that it will try to match against the Text.
So, try this to see if it works
<%= Html.DropDownList("somethingelse", new MultiSelectList(Model.AvailableItems,
"id", "name", Model.items.Select(c=> c.name)), new { multiple = "multiple" })%>
Good luck
Actually, if you look at the MVC source code this behavior is baked into DropDownListFor by default (search for allowMultiple: false). The solution is to use ListBoxFor instead (you will see that as well in the MVC source code, allowMultiple: true), which makes a lot of sense as HTML wise, both render to
<select ...>
<option ...>
<option ...>
...
</select>
You don't have to use different properties on the model as suggested in the answers above this one, I got this working by simply switching to ListBoxFor instead (CSS takes it from there):
#Html.ListBoxFor(model => model.SelectedCategories,
new MultiSelectList(Model.Categories, Model.SelectedCategories),
new { multiple = "multiple" })
Works like a charm, even with POST and re-displaying the view on error.
I had the same problem, I used my own extention method to generate the html and problem solved
public static MvcHtmlString ListBoxMultiSelectFor<TModel, TProperty>(
this HtmlHelper<TModel> helper,
Expression<Func<TModel, TProperty>> expression,
IEnumerable<SelectListItem> selectList,
object htmlAttributes)
{
return ListBoxMultiSelectFor(helper, expression, selectList, new RouteValueDictionary(htmlAttributes));
}
public static MvcHtmlString ListBoxMultiSelectFor<TModel, TProperty>(
this HtmlHelper<TModel> helper,
Expression<Func<TModel, TProperty>> expression,
IEnumerable<SelectListItem> selectList,
IDictionary<string, object> htmlAttributes)
{
string name = ExpressionHelper.GetExpressionText(expression);
TagBuilder selectTag = new TagBuilder("select");
selectTag.MergeAttributes(htmlAttributes);
selectTag.MergeAttribute("id", name, true);
selectTag.MergeAttribute("name", name, true);
foreach (SelectListItem item in selectList)
{
TagBuilder optionTag = new TagBuilder("option");
optionTag.MergeAttribute("value", item.Value);
if (item.Selected) optionTag.MergeAttribute("selected", "selected");
optionTag.InnerHtml = item.Text;
selectTag.InnerHtml += optionTag.ToString();
}
return new MvcHtmlString(selectTag.ToString());
}
I had a similar problem, when using ListBoxFor and a MultiSelectList assigned as a ViewBag property. #jero's answer helped me figure out that if there's a naming collision between the ViewBag field and the model field, then the selected values don't appear properly.
If I did the following, the items did not show up as selected.
//Controller
ViewBag.SelectionIds = new MultiSelectView(possibleValues, "Value", "Name", selectedValues);
//View
#Html.ListBoxFor(model => model.SelectionIds, (MultiSelectList)ViewBag.SelectionIds, new { #class = "form-control" })
If I changed it to the following, then it was fine.
//Controller
//Changed ViewBag.SelectionIds to ViewBag.SelectionIdList
ViewBag.SelectionIdList = new MultiSelectView(possibleValues, "Value", "Name", selectedValues);
//View
#Html.ListBoxFor(model => model.SelectionIds, (MultiSelectList)ViewBag.SelectionIdList, new { #class = "form-control" })
You can go to the to the value of "items" with this
<HttpPost()> _
Function Edit(ByVal crm_cliente As crm_cliente, ByVal form As FormCollection) As ActionResult
If ModelState.IsValid Then
Dim items As String
crm_cliente.usuario_modifico = "ejmorales"
crm_cliente.fecha_modifico = Date.Now
items = form("items")
that will get you the selected items as a string separate with commas (,)
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