Checkbox for nullable boolean - asp.net-mvc-3

My model has a boolean that has to be nullable
public bool? Foo
{
get;
set;
}
so in my Razor cshtml I have
#Html.CheckBoxFor(m => m.Foo)
except that doesn't work. Neither does casting it with (bool). If I do
#Html.CheckBoxFor(m => m.Foo.Value)
that doesn't create an error, but it doesn't bind to my model when posted and foo is set to null. Whats the best way to display Foo on the page and make it bind to my model on a post?

I got it to work with
#Html.EditorFor(model => model.Foo)
and then making a file at Views/Shared/EditorTemplates/Boolean.cshtml with the following:
#model bool?
#Html.CheckBox("", Model.GetValueOrDefault())

Found answer in similar question - Rendering Nullable Bool as CheckBox.
It's very straightforward and just works:
#Html.CheckBox("RFP.DatesFlexible", Model.RFP.DatesFlexible ?? false)
#Html.Label("RFP.DatesFlexible", "My Dates are Flexible")
It's like accepted answer from #afinkelstein except we don't need special 'editor template'

I have bool? IsDisabled { get; set; } in Model. Inserted if in View.
<div class="inputClass" id="disabled">
<div>
#if(Model.IsDisabled==null)
{
Model.IsDisabled = false;
}
#Html.CheckBoxFor(model => model.IsDisabled.Value)
</div>
</div>

My model has a boolean that has to be nullable
Why? This doesn't make sense. A checkbox has two states: checked/unchecked, or True/False if you will. There is no third state.
Or wait you are using your domain models in your views instead of view models? That's your problem. So the solution for me is to use a view model in which you will define a simple boolean property:
public class MyViewModel
{
public bool Foo { get; set; }
}
and now you will have your controller action pass this view model to the view and generate the proper checkbox.

Complicating a primitive with hidden fields to clarify whether False or Null is not recommended.
Checkbox isn't what you should be using -- it really only has one state: Checked. Otherwise, it could be anything.
When your database field is a nullable boolean (bool?), the UX should use 3-Radio Buttons, where the first button represents your "Checked", the second button represents "Not Checked" and the third button represents your null, whatever the semantics of null means. You could use a <select><option> drop down list to save real estate, but the user has to click twice and the choices aren't nearly as instantaneously clear.
1 0 null
True False Not Set
Yes No Undecided
Male Female Unknown
On Off Not Detected
The RadioButtonList, defined as an extension named RadioButtonForSelectList, builds the radio buttons for you, including the selected/checked value, and sets the <div class="RBxxxx"> so you can use css to make your radio buttons go horizontal (display: inline-block), vertical, or in a table fashion (display: inline-block; width:100px;)
In the model (I'm using string, string for the dictionary definition as a pedagogical example. You can use bool?, string)
public IEnumerable<SelectListItem> Sexsli { get; set; }
SexDict = new Dictionary<string, string>()
{
{ "M", "Male"},
{ "F", "Female" },
{ "U", "Undecided" },
};
//Convert the Dictionary Type into a SelectListItem Type
Sexsli = SexDict.Select(k =>
new SelectListItem
{
Selected = (k.Key == "U"),
Text = k.Value,
Value = k.Key.ToString()
});
<fieldset id="Gender">
<legend id="GenderLegend" title="Gender - Sex">I am a</legend>
#Html.RadioButtonForSelectList(m => m.Sexsli, Model.Sexsli, "Sex")
#Html.ValidationMessageFor(m => m.Sexsli)
</fieldset>
public static class HtmlExtensions
{
public static MvcHtmlString RadioButtonForSelectList<TModel, TProperty>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression,
IEnumerable<SelectListItem> listOfValues,
String rbClassName = "Horizontal")
{
var metaData = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
var sb = new StringBuilder();
if (listOfValues != null)
{
// Create a radio button for each item in the list
foreach (SelectListItem item in listOfValues)
{
// Generate an id to be given to the radio button field
var id = string.Format("{0}_{1}", metaData.PropertyName, item.Value);
// Create and populate a radio button using the existing html helpers
var label = htmlHelper.Label(id, HttpUtility.HtmlEncode(item.Text));
var radio = String.Empty;
if (item.Selected == true)
{
radio = htmlHelper.RadioButtonFor(expression, item.Value, new { id = id, #checked = "checked" }).ToHtmlString();
}
else
{
radio = htmlHelper.RadioButtonFor(expression, item.Value, new { id = id }).ToHtmlString();
}// Create the html string to return to client browser
// e.g. <input data-val="true" data-val-required="You must select an option" id="RB_1" name="RB" type="radio" value="1" /><label for="RB_1">Choice 1</label>
sb.AppendFormat("<div class=\"RB{2}\">{0}{1}</div>", radio, label, rbClassName);
}
}
return MvcHtmlString.Create(sb.ToString());
}
}

For me the solution was to change the view model. Consider you are searching for invoices. These invoices can be paid or not. So your search has three options: Paid, Unpaid, or "I don't Care".
I had this originally set as a bool? field:
public bool? PaidInvoices { get; set; }
This made me stumble onto this question. I ended up created an Enum type and I handled this as follows:
#Html.RadioButtonFor(m => m.PaidInvoices, PaidStatus.NotSpecified, new { #checked = true })
#Html.RadioButtonFor(m => m.PaidInvoices, PaidStatus.Yes)
#Html.RadioButtonFor(m => m.PaidInvoices, PaidStatus.No)
Of course I had them wrapped in labels and had text specified, I just mean here's another option to consider.

Checkbox only offer you 2 values (true, false). Nullable boolean has 3 values (true, false, null) so it's impossible to do it with a checkbox.
A good option is to use a drop down instead.
Model
public bool? myValue;
public List<SelectListItem> valueList;
Controller
model.valueList = new List<SelectListItem>();
model.valueList.Add(new SelectListItem() { Text = "", Value = "" });
model.valueList.Add(new SelectListItem() { Text = "Yes", Value = "true" });
model.valueList.Add(new SelectListItem() { Text = "No", Value = "false" });
View
#Html.DropDownListFor(m => m.myValue, valueList)

I would actually create a template for it and use that template with an EditorFor().
Here is how I did it:
Create My template, which is basically a partial view I created in the EditorTemplates directory, under Shared, under Views name it as (for example): CheckboxTemplate:
#using System.Globalization
#using System.Web.Mvc.Html
#model bool?
#{
bool? myModel = false;
if (Model.HasValue)
{
myModel = Model.Value;
}
}
<input type="checkbox" checked="#(myModel)" name="#ViewData.TemplateInfo.HtmlFieldPrefix" value="True" style="width:20px;" />
Use it like this (in any view):
#Html.EditorFor(x => x.MyNullableBooleanCheckboxToBe, "CheckboxTemplate")
Thats all.
Templates are so powerful in MVC, use them.. You can create an entire page as a template, which you would use with the #Html.EditorFor(); provided that you pass its view model in the lambda expression..

The cleanest approach I could come up with is to expand the extensions available to HtmlHelper while still reusing functionality provided by the framework.
public static MvcHtmlString CheckBoxFor<T>(this HtmlHelper<T> htmlHelper, Expression<Func<T, bool?>> expression, IDictionary<string, object> htmlAttributes) {
ModelMetadata modelMeta = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
bool? value = (modelMeta.Model as bool?);
string name = ExpressionHelper.GetExpressionText(expression);
return htmlHelper.CheckBox(name, value ?? false, htmlAttributes);
}
I experimented with 'shaping' the expression to allow a straight pass through to the native CheckBoxFor<Expression<Func<T, bool>>> but I don't think it's possible.

I had a similar issue in the past.
Create a Checkbox input in HTML, and set the attribute name="Foo" This should still post properly.
<input type="checkbox" name="Foo" checked="#model.Foo.Value" /> Foo Checkbox<br />

If checked = True and not checked = null
Model
public NotNullFoo
{
get { return this.Foo?? false; }
set { this.Foo= (value == false ? null : true as bool?); }
}
public bool? Foo
{
get;
set;
}
View
#Html.CheckBoxFor(x => Model.NotNullFoo })

Just check for the null value and return false to it:
#{ bool nullableValue = ((Model.nullableValue == null) || (Model.nullableValue == false)) ? false : true; }
#Html.CheckBoxFor(model => nullableValue)

I also faced the same issue. I tried the following approach to solve the issue
because i don't want to change the DB and again generate the EDMX.
#{
bool testVar = (Model.MYVar ? true : false);
}
<label>#Html.CheckBoxFor(m => testVar)testVar</label><br />

When making an EditorTemplate for a model which contains a nullable bool...
Split the nullable bool into 2 booleans:
// Foo is still a nullable boolean.
public bool? Foo
{
get
{
if (FooIsNull)
return null;
return FooCheckbox;
}
set
{
FooIsNull = (value == null);
FooCheckbox = (value ?? false);
}
}
// These contain the value of Foo. Public only so they are visible in Razor views.
public bool FooIsNull { get; set; }
public bool FooCheckbox { get; set; }
Within the editor template:
#Html.HiddenFor(m => m.FooIsNull)
#if (Model.FooIsNull)
{
// Null means "checkbox is hidden"
#Html.HiddenFor(m => m.FooCheckbox)
}
else
{
#Html.CheckBoxFor(m => m.FooCheckbox)
}
Do not postback the original property Foo, because that is now calculated from FooIsNull and FooCheckbox.

All the answers above came with it's own issues. Easiest/cleanest way IMO is to create a helper
MVC5 Razor
App_Code/Helpers.cshtml
#helper CheckBoxFor(WebViewPage page, string propertyName, bool? value, string htmlAttributes = null)
{
if (value == null)
{
<div class="checkbox-nullable">
<input type="checkbox" #page.Html.Raw(htmlAttributes)>
</div>
}
else if (value == true)
{
<input type="checkbox" value="true" #page.Html.Raw(htmlAttributes) checked>
}
else
{
<input type="checkbox" value="false" #page.Html.Raw(htmlAttributes)>
}
}
Usage
#Helpers.CheckBoxFor(this, "IsPaymentRecordOk", Model.IsPaymentRecordOk)
In my scenario, a nullable checkbox means that a staff member had not yet asked the question to the client, so it's wrapped in a .checkbox-nullable so that you may style appropriately and help the end-user identify that it is neither true nor false
CSS
.checkbox-nullable {
border: 1px solid red;
padding: 3px;
display: inline-block;
}

Extension methods:
public static MvcHtmlString CheckBoxFor<TModel>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, bool?>> expression)
{
return htmlHelper.CheckBoxFor<TModel>(expression, null);
}
public static MvcHtmlString CheckBoxFor<TModel>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, bool?>> expression, object htmlAttributes)
{
ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
bool? isChecked = null;
if (metadata.Model != null)
{
bool modelChecked;
if (Boolean.TryParse(metadata.Model.ToString(), out modelChecked))
{
isChecked = modelChecked;
}
}
return htmlHelper.CheckBox(ExpressionHelper.GetExpressionText(expression), isChecked??false , htmlAttributes);
}

Radio buttons are useful, but they don't allow you to deselect. If you want this behaviour, then consider using a drop-down/select. The following code will generate the SelectList and this binds successfully to a nullable boolean:
public static SelectList GetBoolTriState()
{
var items = new List<SelectListItem>
{
new SelectListItem {Value = "", Text = ""},
new SelectListItem {Value = "True", Text = "Yes"},
new SelectListItem {Value = "False", Text = "No"},
};
return new SelectList(items, "Value", "Text");
}

#{ bool testVar = ((bool)item.testVar ? true : false); }
#Html.DisplayFor(modelItem => testVar)

This is an old question, and the existing answers describe most of the alternatives. But there's one simple option, if you have bool? in your viewmodel, and you don't care about null in your UI:
#Html.CheckBoxFor(m => m.boolValue ?? false);

Related

Is there an overload for Html.DropDownList that will show a value from the model as the currently selected item?

Is there an overload for Html.DropDownList that will show a value from the model as the currently selected item? In my view I insert a model using as using statement
#using SchoolIn.Models
Then I access the model like this:
if (Model.Enrollments != null)
{
#Html.DropDownList("searchString", Model.Enrollments.FirstOrDefault().weekDays.Select(s => new SelectListItem { Text = s.ToString(), Value = s.ToString() }))
}
Here’s the code from my model:
public virtual string classDays { get; set; }
public string[] weekDays = new string[6] { "Day", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday" };
public string[] WeekDays
{
get { return weekDays; }
When my view loads it presents a dropdown list that I can select from, I select a day and save the selection but when it loads again, I want the previously selected item to be the default selected in the list. How can I do this? I would really appreciate any help, thanks.
It looks like you should be able to set Selected on the appropriate item:
#{
var Items = Model.Enrollments.FirstOrDefault().weekDays.Select(s =>
new SelectListItem { Text = s.ToString(), Value = s.ToString()
Selected = s.ToString().Equals(PreviouslySelectedValue)
}
);
}
#Html.DropDownList("searchString", Items)
You could also use DropDownListFor, which should set the selected item to the value of the property.
#Html.DropDownListFor(model => PreviouslySelectedValue, Items)
You might need to use DropDownListFor rather than DropDownList. Here is a constructor definition from MSDN. There are also five more overloads.
public static MvcHtmlString DropDownListFor<TModel, TProperty>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression,
IEnumerable<SelectListItem> selectList
)
Try this Extension Method :
public static IEnumerable<SelectListItem> SetSelected(this IEnumerable<SelectListItem> selectList, object selectedValue)
{
selectList = selectList ?? new List<SelectListItem>();
if (selectedValue == null)
return selectList;
var vlaue = selectedValue.ToString();
return selectList.BuildList(m => m.Text, m => m.Value, null, m => String.Equals(m.Value, vlaue, StringComparison.CurrentCultureIgnoreCase));
}
Then you may call it in your view like this:
#Html.DropDownListFor(model => model.CategoryId, ((IEnumerable<SelectListItem>)ViewData["Categories"]).SetSelected(model.CategoryId))

MultiSelectList does not highlight items in the selectedValues list

In a ASP.NET MVC (Razor) project, I'm using a ListBox with Multi Select option in a Edit View, there was a problem in highlighting the previously selected items by using selectedValues in MultiSelectList, so I asked a question on SO previously. According to the answers given for that question I decided to use a ViewModel (with AutoMapper) for passing the data to the View, without using the ViewBag, but still I have the same problem.. It does not select the items given in the selectedValues list
this is my new code
MODELS
public class Post
{
public int Id { get; set; }
...
public string Tags { get; set; }
}
public class PostEditViewModel
{
private DocLibraryContext db = new DocLibraryContext();
public int Id { get; set; }
..
public MultiSelectList TagsList { get; set; }
}
Controller
public ActionResult Edit(int id)
{
Post post = db.Posts.Find(id);
PostEditViewModel postEditViewModel = Mapper.Map<Post, PostEditViewModel>(post);
var tagsQuery = from d in db.Tags
orderby d.Name
select d;
postEditViewModel.TagsList = new MultiSelectList(tagsQuery, "Id", "Name", post.Tags.Split(','));
return View(postEditViewModel);
}
VIEW
<div class="editor-field">
#Html.ListBoxFor(model => model.Tags, Model.TagsList as MultiSelectList)
</div>
What am I doing wrong here? Please help....
UPDATE 1 :
changed controller to
public ActionResult Edit(int id)
{
Post post = db.Posts.Find(id);
PostEditViewModel postEditViewModel = Mapper.Map<Post, PostEditViewModel>(post);
var tagsQuery = from d in db.Tags
orderby d.Name
select d;
var selectedIds = post.Tags.Split(',').Select(n => tagsQuery.First(t => t.Name == n));
postEditViewModel.TagsList = new MultiSelectList(tagsQuery, "Id", "Name", selectedIds);
return View(postEditViewModel);
}
but I get the same results.
UPDATE 2:
I tried changing code (as in this tutorial), which worked, But I need to use previous method..
MODELS
public Post Post { get; set; }
public MultiSelectList TagsList { get; set; }
public PostEditViewModel(Post post)
{
Post = post;
var tagsQuery = from d in db.Tags
orderby d.Name
select d;
TagsList = new MultiSelectList(tagsQuery, "Name", "Name", post.Tags.Split(','));
}
Controller
public ActionResult Edit(int id)
{
Post post = db.Posts.Find(id);
return View(new PostEditViewModel(post));
}
VIEW
<div class="editor-field">
#Html.ListBox("Tags", Model.TagsList as MultiSelectList)
</div>
What makes the difference...??
The problem is with the construction of your MultiSelectList:
new MultiSelectList(tagsQuery, "Id", "Name", post.Tags.Split(','));
You are specifying that the values for the elements will be taken from each tag's Id property, but then for the actual selected values you are passing in an array of strings which presumably corresponds to the Names of the tags. It doesn't matter that you also specify Name to be the property from which the display text will be determined; the selectedValues parameter matches against values, not display text.
To fix this, project each tag name into its corresponding Id:
var selectedIds = post.Tags.Split(',').Select(n => tagsQuery.First(t => t.Name == n).Id);
new MultiSelectList(tagsQuery, "Id", "Name", selectedIds);
Update:
Oops, there was a mistake in the code above.
I edited the answer to add a required .Id at the end of the selectedIds initialization -- the previous version was selecting tags, not ids (and of course they were comparing unequal, apples and oranges).
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());
}

MVC3 Validation for controls in a loop

In my View, I'm making an entire html table editable. Each field in the table has validation. Here is some sample code...
<tbody>
foreach (Item item in Model.Items)
{
<tr>
<td>
#Html.TextBoxFor(x => em.Value)
#Html.ValidationMessageFor(x => em.Value)
</td>
</tr>
}
</tbody>
This is creating two problems on the browser.
1) Since the name attribute for all the controls are the same, when one field is invalid, the error messages are displayed on all fields of the same name.
2) Probably related to the names as well, when validating the form, if the control in the first row is invalid, the form is invalid. But controls in all other rows will not invalidate the form (even though the error messages are displayed).
if (!$(form).valid()) {
return false;
}
Any thoughts would be helpful.
UPDATE: THE SOLUTION
Per bobek suggestion, I created custom extensions for TextBoxInLoopFor and ValidationMessageInLoop. Below is the solution:
public static MvcHtmlString TextBoxInLoopFor<TModel, IItem, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, IItem item, object htmlAttributes)
{
var model = item as IInLoopForModel;
if (model == null)
return MvcHtmlString.Empty;
var metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
var fieldName = ExpressionHelper.GetExpressionText(expression);
var name = model.Name + fieldName;
var tag = new TagBuilder("input");
tag.MergeAttributes(new RouteValueDictionary(htmlAttributes));
tag.Attributes.Add("id", name);
tag.Attributes.Add("for", name);
tag.Attributes.Add("name", name);
tag.Attributes.Add("value", metadata.Model.ToString());
// Add the validation attributes
ModelState modelState;
if (html.ViewData.ModelState.TryGetValue(name, out modelState))
{
if (modelState.Errors.Count > 0)
tag.AddCssClass(HtmlHelper.ValidationInputCssClassName);
}
tag.MergeAttributes(html.GetUnobtrusiveValidationAttributes(name, metadata));
return MvcHtmlString.Create(tag.ToString(TagRenderMode.Normal));
}
public static MvcHtmlString ValidationMessageInLoopFor<TModel, IItem, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, IItem item)
{
var model = item as IInLoopForModel;
if (model == null)
return MvcHtmlString.Empty;
var fieldName = ExpressionHelper.GetExpressionText(expression);
var name = model.Name + fieldName;
var tag = new TagBuilder("span");
tag.Attributes.Add("class", "field-validation-valid");
tag.Attributes.Add("data-valmsg-replace", "true");
tag.Attributes.Add("data-valmsg-for", name);
return MvcHtmlString.Create(tag.ToString(TagRenderMode.Normal));
}
This is not valid at all. MVC3 helpers for input are adding an ID for each one so here you will have multiple textboxes with same ID - it's not valid in HTML.
You should manually create a TextBoxFor and ValidationFor your each property on the model, or if it's possible try:
foreach (Item item in Model.Items)
{
<tr>
<td>
#Html.TextBoxFor(x => item.Value)
#Html.ValidationMessageFor(x => item.Value)
</td>
</tr>
}
I would suggest using metadata in your domain.model . Here is a small example for you to understand the process :
namespace yourDomain.Domain.Model
{
[MetadataType(typeof(fooEntityMeta))]
public partial class fooEntity
{
}
and pick the value field from your entity; make it required
public class fooEntityMeta
{
[Required(ErrorMessage = "Value is required")]
public string Value;
...
}
here's a link that will help you through the process : http://msdn.microsoft.com/en-us/magazine/ee336030.aspx

Cascading dropdown lists MVC3

Relatively new to MVC and trying to get a cascading dropdown list working for train times.
After looking at a lot of posts, people say that you should stay away from ViewBag/ViewData and instead focus on ViewModels, but I just can't seem to get my head round it, and it's driving me up the wall. Any tutorial seems to be either to complex or too easy and the whole viewModel idea just hasn't clicked with me yet.
So here is my scenario: I have an admin system where staff can add individual train journeys. For each train time, I have an input form where the user can choose a Company, and from there, I'd like the dropdownlist underneath to populate with a list of journey numbers, which indicate routes. Once they have chosen a number, they can carry on with the rest of the form, which is quite large, including time of travel, facilities on the train etc.
I've created a viewmodel like so:
public class JourneyNumbersViewModel
{
private List<SelectListItem> _operators = new List<SelectListItem>();
private List<SelectListItem> _journeys= new List<SelectListItem>();
[Required(ErrorMessage = "Please select an operator")]
public string SelectedOperator { get; set; }
[Required(ErrorMessage = "Please select a journey")]
public string SelectedJourney { get; set; }
public List<SelectListItem> Journeys
{
get { return _journeys; }
}
public List<SelectListItem> Operators
{
get
{
foreach(Operator a in Planner.Repository.OperatorRepository.GetOperatorList())
{
_operators.Add(new SelectListItem() { Text = a.OperatorName, Value = a.OperatorID.ToString() });
}
return _operators;
}
}
}
In my controller, I have this for the Create view:
public ActionResult Create()
{
return View(new JourneyNumbersViewModel());
}
And this is where it isn't really working for me - if I change my model at the top of the Create view to: #model Planner.ViewModels.JourneyNumbersViewModel, then the rest of my form throws errors as the model is no longer correct for the rest of the form. Is this the way it is supposed to work - what if you need to reference multiple view models with a single view?
I know this is a simple thing and as soon as it clicks I'll wonder how on earth I could have struggled with it in the first place, but if anyone can point out where I'm going wrong, I'd be very grateful.
I have done something similar. Here is some of the code (apologies upfront for this being quite long, but I wanted to make sure you could re-create this on your side):
View looks like this:
using Cascading.Models
#model CascadingModel
#{
ViewBag.Title = "Index";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>Cascading Forms</h2>
<table>
#using(Html.BeginForm("Index", "Home"))
{
<tr>
<td>#Html.LabelFor(m=>m.CategoryId)</td>
<td>#Html.DropDownListFor(m => m.CategoryId, new SelectList(Model.Categories, "Id", "Name"), string.Empty)</td>
</tr>
<tr>
<td>#Html.LabelFor(m=>m.ProductId)</td>
<td>#Html.CascadingDropDownListFor(m => m.ProductId, new SelectList(Model.Products, "Id", "Name"), string.Empty, null, "CategoryId", "Home/CategorySelected")</td>
</tr>
<tr>
<td> </td>
<td><input type="submit" value="Go"/></td>
</tr>
}
</table>
the Model looks as follows:
public class CascadingModel
{
public int CategoryId { get; set; }
public List<Category> Categories { get; set; }
public int ProductId { get; set; }
public List<Product> Products { get; set; }
}
the real "clever" part of the system is the Html.CascadingDropDownListFor which looks as follows:
public static class MvcHtmlExtensions
{
public static MvcHtmlString CascadingDropDownListFor<TModel, TProperty>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression,
IEnumerable<SelectListItem> selectList,
string optionLabel,
IDictionary<string, Object> htmlAttributes,
string parentControlName,
string childListUrl
)
{
var memberName = GetMemberInfo(expression).Member.Name;
MvcHtmlString returnHtml = Html.SelectExtensions.DropDownListFor(htmlHelper, expression, selectList, optionLabel, htmlAttributes);
var returnString = MvcHtmlString.Create(returnHtml.ToString() +
#"<script type=""text/javascript"">
$(document).ready(function () {
$(""#<<parentControlName>>"").change(function () {
var postData = { <<parentControlName>>: $(""#<<parentControlName>>"").val() };
$.post('<<childListUrl>>', postData, function (data) {
var options = """";
$.each(data, function (index) {
options += ""<option value='"" + data[index].Id + ""'>"" + data[index].Name + ""</option>"";
});
$(""#<<memberName>>"").html(options);
})
.error(function (jqXHR, textStatus, errorThrown) { alert(jqXHR.responseText); });
});
});
</script>"
.Replace("<<parentControlName>>", parentControlName)
.Replace("<<childListUrl>>", childListUrl)
.Replace("<<memberName>>", memberName));
return returnString;
}
private static MemberExpression GetMemberInfo(Expression method)
{
LambdaExpression lambda = method as LambdaExpression;
if (lambda == null)
throw new ArgumentNullException("method");
MemberExpression memberExpr = null;
if (lambda.Body.NodeType == ExpressionType.Convert)
{
memberExpr = ((UnaryExpression)lambda.Body).Operand as MemberExpression;
}
else if (lambda.Body.NodeType == ExpressionType.MemberAccess)
{
memberExpr = lambda.Body as MemberExpression;
}
if (memberExpr == null)
throw new ArgumentException("method");
return memberExpr;
}
}
Controller Logic for those looking for it:
public ActionResult CategoriesAndProducts()
{
var viewModel = new CategoriesAndProductsViewModel();
viewModel.Categories = FetchCategoriesFromDataBase();
viewModel.Products = FetchProductsFromDataBase();
viewModel.CategoryId = viewModel.Categories[0].CategoryId;
viewModel.ProductId = viewModel.Products.Where(p => p.CategoryId).FirstOrDefault().ProductId;
return View(viewModel);
}

Best way to sort a DropDownList in MVC3 / Razor using helper method

Hi so I'm pretty new to MVC3 and Razor and I've been trying to get my head around it the past few days. I've been given a task by my project architect to create a helper method that sorts a drop down list in an MVC View. I have a View that retrieves various data from a Controller and I'm returning some values that I want to appear in a drop down list. I've been told not to sort it in the Controller and also to pass the field that we want to sort by into the helper method. I could do it like below but the architect wants to keep the view free of c sharp code:
#Html.DropDownListFor(model => model.StudyName, new SelectList(ViewBag.StudyTypes, "Value", "Text").OrderBy(l => l.Text))
So I've created some sample code and some extension methods to try and get it to work. My idea is to replicate the existing Html.DropDownList method and allow the passing of 'object htmlAttributes' so I can set the style as part of the method call.
Here's my code so far. I'm returning the data for the drop down in ViewBag.StudyTypes in the Edit Controller method:
public ActionResult Edit(int id)
{
IEnumerable<SelectListItem> mySelectList = new List<SelectListItem>();
IList<SelectListItem> myList = new List<SelectListItem>();
for (int i = 0; i < 5; i++)
{
myList.Add(new SelectListItem()
{ Value = i.ToString(), Text = "My Item " + i.ToString(), Selected = i == 2 }
);
}
mySelectList = myList;
ViewBag.StudyTypes = mySelectList;
StudyDefinition studydefinition = db.StudyDefinitions.Find(id);
return View(studydefinition);
}
Here's my View code:
#model MyStudyWeb.Models.StudyDefinition
#using MyStudyWeb.Helpers
#{
ViewBag.Mode = "Edit";
}
<div>
#Html.DropDownListSorted(new SelectList(ViewBag.StudyTypes, "Value", "Text"))<br />
#Html.DropDownListSorted("MyList", new SelectList(ViewBag.StudyTypes, "Value", "Text"))<br />
</div>
Finally below are the extension methods I'm trying to get to work. The first extension method does nothing, I just get a blank space at that point in the View. The second method kind of works but it's ugly. For the 3rd method I don't know how to specify an 'order by' parameter as the OrderBy on an IEnumerable expects a Linq expression.
namespace StudyDefinition.Helpers
{
public static class HtmlHelperExtensions
{
// 1st sort method: sort the passed in list and return a new sorted list
public static SelectList DropDownListSorted(this HtmlHelper helper, IEnumerable<SelectListItem> selectList)
{
var x = new SelectList(selectList.ToList()).OrderBy(l => l.Text);
return x as SelectList;
}
// 2nd sort method: return IHtml string and create <select> list manually
public static IHtmlString DropDownListSorted(this HtmlHelper helper, string name, SelectList selectList)
{
StringBuilder output = new StringBuilder();
(selectList).OrderBy(l => l.Text);
output.Append("<select id=" + name + " name=" + name + ">");
foreach (var item in selectList)
{
output.Append("<option value=" + item.Value.ToString() + ">" + item.Text + "</option>");
}
output.Append("</select>");
return MvcHtmlString.Create(output.ToString());
}
// 3rd sort method: pass in order by parameter - how do I use this?
public static IHtmlString DropDownListSorted(this HtmlHelper helper, string name, SelectList selectList, string orderBy)
{
StringBuilder output = new StringBuilder();
//How do I use the orderBy parameter?
(selectList).OrderBy(l => l.Text);
output.Append("<select id=" + name + " name=" + name + ">");
foreach (var item in selectList)
{
output.Append("<option value=" + item.Value.ToString() + ">" + item.Text + "</option>");
}
output.Append("</select>");
return MvcHtmlString.Create(output.ToString());
}
}
}
I really don't know the best approach to take, there may be a much simpler way that I'm totally missing and I might be at the point where I can't see the wood for the trees anymore. Some questions
Should I return a SelectList or an MvcHtmlString, or something else entirely?
For the first extension method how do I get the returned SelectList to render in the View?
How to I pass in a parameter to my extension methods that specifies the sort order?
How do I pass an 'object htmlAttributes' parameter, and how do I apply this object / parameter to the SelectList?
If anyone has some ideas or suggestions then I'd appreciate some feedback :)
The first and most important part of your code would be to get rid of any ViewBag/ViewData (which I personally consider as cancer for MVC applications) and use view models and strongly typed views.
So let's start by defining a view model which would represent the data our view will be working with (a dropdownlistg in this example):
public class MyViewModel
{
public string SelectedItem { get; set; }
public IEnumerable<SelectListItem> Items { get; set; }
}
then we could have a controller:
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new MyViewModel
{
// I am explicitly putting some items out of order
Items = new[]
{
new SelectListItem { Value = "5", Text = "Item 5" },
new SelectListItem { Value = "1", Text = "Item 1" },
new SelectListItem { Value = "3", Text = "Item 3" },
new SelectListItem { Value = "4", Text = "Item 4" },
}
};
return View(model);
}
}
and a view:
#model MyViewModel
#Html.DropDownListForSorted(
x => x.SelectedItem,
Model.Items,
new { #class = "foo" }
)
and finally the last piece is the helper method which will sort the dropdown by value (you could adapt it to sort by text):
public static class HtmlExtensions
{
public static IHtmlString DropDownListForSorted<TModel, TProperty>(
this HtmlHelper<TModel> helper,
Expression<Func<TModel, TProperty>> expression,
IEnumerable<SelectListItem> items,
object htmlAttributes
)
{
var model = helper.ViewData.Model;
var orderedItems = items.OrderBy(x => x.Value);
return helper.DropDownListFor(
expression,
new SelectList(orderedItems, "Value", "Text"),
htmlAttributes
);
}
}
Just add in the sorting before you return the items to the dropdown list.
Do this:
Models: StudyViewModel.cs
public class StudyViewModel {
public string StudyName { get; set; }
public string StudyTypes { get; set; }
}
Controller: StudyController.cs
using System.Web.Mvc;
public class StudyController
{
public List<SelectListItem> studyTypes()
{
List<SelectListItem> itemList = new List<SelectListItem>();
for (var i=0; i<5; i++)
{
itemList.Add = new SelectListItem({
Value = i.ToString();
Text = "My Item";
});
}
// You can sort here....
List<SelectListItem> sortedList = itemList.OrderBy(x=>x.Text);
return sortedList;
}
public ActionResult Edit(int id)
{
//You won't need this because you get it using your
//controller's routine, instead
//ViewBag.StudyTypes = studySlots.OrderBy(e => e.Value);
//-- unless you need to add these values to the model for
// some reason (outside of filling the ddl), in which case....
// StudyViewModel svm = new StudyViewModel();
// svm.StudyTypes = studySlots.OrderBy(e => e.Value);
// svm.StudyName = "My Item";
// return View(svm);
// Otherwise, just....
return View();
}
}
View: Edit.cshtml
#Html.DropDownListFor(model => model.StudyName)
.OptionLabel('Select...')
.DataTextField('Text')
.DataValueField('Value')
.Datasource(source =>
{
// This is where you populate your data from the controller
source.Read(read =>
{
read.Action("studyTypes", "Study");
});
})
.Value(Model.StudyName != null ? Model.StudyName.ToString() : "")
)
This way will avoid ViewBags and just use a function to fill in the values, directly.
If you are using a database you can use a query to define the sort element
using (BDMMContext dataContext = new BDMMContext())
{
foreach (Arquiteto arq in dataContext.Arquitetos.SqlQuery("SELECT * FROM Arquitetos ORDER BY Nome"))
{
SelectListItem selectItem = new SelectListItem { Text = arq.Nome, Value = arq.Arquiteto_Id.ToString() };
//
list.Add(selectItem);
}
}

Resources