ASP.Net MVC 3 - CheckBoxList - Need some suggestions - asp.net

I am pretty new to ASP.Net MVC (and razor) and I have a few questions.
1)
I created an HTML extension to create a check box list as below:
public static HtmlString CheckBoxList(this HtmlHelper htmlHelper, string name, List<InputItemInfo> ItemInfo)
{
if (String.IsNullOrEmpty(name))
throw new ArgumentException("The argument must have a value", "name");
if (ItemInfo == null)
throw new ArgumentNullException("ItemInfo");
if (ItemInfo.Count < 1)
throw new ArgumentException("The list must contain at least one value", "ItemInfo");
StringBuilder sb = new StringBuilder();
ItemInfo.Insert(0, new InputItemInfo("*", "Select All", ItemInfo.All(i => i.IsChecked)));
foreach (InputItemInfo info in ItemInfo)
{
TagBuilder builder = new TagBuilder("input");
if (info.IsChecked) builder.MergeAttribute("checked", "checked");
builder.MergeAttribute("type", "checkbox");
builder.MergeAttribute("value", info.Value);
builder.MergeAttribute("name", name);
builder.InnerHtml = info.DisplayText;
sb.Append(builder.ToString(TagRenderMode.Normal));
sb.Append("<br />");
}
return new HtmlString(sb.ToString());
}
I was able to use this in my views and also get the values in the controller as shown below:
#model List<AppTest.Models.InputExtensionsViewModel>
#{
ViewBag.Title = "Check";
}
<h2>Check</h2>
#using (Html.BeginForm())
{
<table border="0" style="border:0px;">
<tr>
<td valign="top">
#Html.Partial("CheckBoxList", Model[0])
</td>
</tr>
</table>
<br />
<input type="submit" value="Go" />
}
<div style="font-weight:bolder">
#ViewData["data"]
</div>
controller:
public ActionResult Check()
{
var model = new List<InputExtensionsViewModel>();
var model1 = new InputExtensionsViewModel
{
Title = "Facilities",
InputElementName = "facilities",
InputElements = // a list
};
model.Add(model1);
return View(model);
}
[HttpPost]
public ActionResult Check(string[] facilities)
{
...
}
The model is:
public class InputExtensionsViewModel
{
public string Title { get; set; }
public string InputElementName { get; set; }
public List<InputItemInfo> InputElements { get; set; }
public void SetSelected(string[] items)
{
if (items == null)
return;
this.InputElements.ForEach(delegate(InputItemInfo info)
{
if (items.Contains(info.Value))
info.IsChecked = true;
});
}
}
My question is, is there a way by which I could bind the array items to a property in the InputExtensionsViewModel model? If I just add a property called facilities to the view model, it's not bound automatically and I can understand why, as I am not binding that in my view. But I cannot seem to think of a way by which I could do that.
This check box list is a user control and I just wanted to avoid having too many string[] array for my action methods.
[EDIT] - Okay, I was able to do this when I tried now. Not sure why it didn't work before.
2) And, I was checking for alternatives and found out this answer in SO:
CheckboxList in MVC3.0
And I was able to replicate this but my question is, how do i bind a label to this checkbox? My labels are dynamic and part of the model and so cannot be hard-coded. I was trying to use a Html.LabelFor but that didn't work. In the editor template, if I just #Model.Text, it won't work and will be lost after a post-back as its not bound to a property
I googled and found suggestions to create HTML helpers which is what I did earlier (my 1st question is about that).
Please let me know if something is unclear. I could elaborate. Any input is appreciated!
Thanks in advance!

Ah, I found the solutions!
1) As indicated in my edit - adding a property with a similar name to a model and using it in the [HttpPost] enabled action method works fine. Guess last time I missed the getter and setters.
2) For this, in the editor template for MyViewModel, we just need to add this (** and **, needless to say, remove **!):
#model AppName.Models.MyViewModel
#Html.HiddenFor(x => x.Id)
#Html.CheckBoxFor(x => x.IsChecked) **#Model.Text
#Html.HiddenFor(x => x.Text)**
EDIT:
I have changed this template to do more. Now there is a label control and it is associated to the checkboxes through jquery as shown below.
#model EncorPlusTest.Infrastructure.InputItemInfo
#Html.HiddenFor(model => model.Value)
#Html.CheckBoxFor(model => model.IsChecked) <label for="">#Model.Text</label>
#Html.HiddenFor(model => model.Text)
<br />
Then in jquery:
$('input:checkbox').each(function () {
var lbl = $(this).next('input:hidden').next('label');
var forID = $(this).attr('id');
$(lbl).attr('for', forID);
});
Hope its helpful for others!

To answer part 2, you can easily add your label text as a property such as:
public class MyViewModel
{
public int Id { get; set; }
public bool IsChecked { get; set; }
public string Text { get; set; }
}
Then your template would look similar to this:
#model AppName.Models.MyViewModel
#Html.HiddenFor(x => x.Id)
#Html.CheckBoxFor(x => x.IsChecked)
#Html.LabelFor(x => x.Text)
The only downside to the above is that the label wouldn't be linked directly to the checkbox. You can accomplish this by doing something such as: CheckboxList in MVC3
Depending on the chance for re-usability, you can always create your own HtmlHelper as you were doing in the first part of this and wrap in the suggestions from the URL I pasted above.

You don't jQuery to solve this problem. If you wrap the input with a label you get the same behavior.
By the way, another option, instead of a editor template, is a HTML Helper. Take a look at this:
public static class HtmlHelperExtensions
{
#region CheckBoxList
public static MvcHtmlString CheckBoxList(this HtmlHelper htmlHelper, string name, List<SelectListItem> listInfo)
{
return htmlHelper.CheckBoxList(name, listInfo, ((IDictionary<string, object>)null));
}
public static MvcHtmlString CheckBoxList(this HtmlHelper htmlHelper, string name, List<SelectListItem> listInfo, object htmlAttributes)
{
return htmlHelper.CheckBoxList(name, listInfo, ((IDictionary<string, object>)new RouteValueDictionary(htmlAttributes)));
}
public static MvcHtmlString CheckBoxList(this HtmlHelper htmlHelper, string name, List<SelectListItem> selectListItems, IDictionary<string, object> htmlAttributes)
{
// Verify arguments
if (String.IsNullOrEmpty(name))
throw new ArgumentNullException("name", "Name cannot be null");
if (selectListItems == null)
throw new ArgumentNullException("selectList", "Select list cannot be null");
if (selectListItems.Count() < 1)
throw new ArgumentException("Select list must contain at least one value", "selectList");
// Define items
StringBuilder items = new StringBuilder();
int index = 0;
// Loop through items)
foreach (SelectListItem i in selectListItems)
{
// hidden value
TagBuilder hiddenValue = new TagBuilder("input");
hiddenValue.MergeAttribute("type", "hidden");
hiddenValue.MergeAttribute("value", i.Value);
hiddenValue.MergeAttribute("id", string.Format("{0}_{1}__Value", name, index));
hiddenValue.MergeAttribute("name", string.Format("{0}[{1}].Value", name, index));
// check box
TagBuilder checkbox = new TagBuilder("input");
if (i.Selected)
checkbox.MergeAttribute("checked", "checked");
checkbox.MergeAttribute("id", string.Format("{0}_{1}__Selected", name, index));
checkbox.MergeAttribute("name", string.Format("{0}[{1}].Selected", name, index));
checkbox.MergeAttribute("type", "checkbox");
checkbox.MergeAttribute("value", "true");
// wrapper label
TagBuilder wrapperLabel = new TagBuilder("label");
wrapperLabel.InnerHtml = checkbox.ToString(TagRenderMode.SelfClosing);
wrapperLabel.InnerHtml += i.Text;
// hidden selected
TagBuilder hiddenSelected = new TagBuilder("input");
hiddenSelected.MergeAttribute("type", "hidden");
hiddenSelected.MergeAttribute("value", i.Selected.ToString().ToLower());
hiddenSelected.MergeAttribute("name", string.Format("{0}[{1}].Selected", name, index));
// label for checkbox
TagBuilder checkBoxLabel = new TagBuilder("label");
checkBoxLabel.MergeAttribute("for", checkbox.Attributes["id"]);
checkBoxLabel.MergeAttribute("id", string.Format("{0}_{1}__Text", name, index));
checkBoxLabel.MergeAttribute("name", string.Format("{0}[{1}].Text", name, index));
// hidden text
TagBuilder hiddenText = new TagBuilder("input");
hiddenText.MergeAttribute("type", "hidden");
hiddenText.MergeAttribute("value", i.Text);
hiddenText.MergeAttribute("id", string.Format("{0}_{1}__Text", name, index));
hiddenText.MergeAttribute("name", string.Format("{0}[{1}].Text", name, index));
// Add item
items.AppendLine(hiddenValue.ToString(TagRenderMode.SelfClosing));
items.AppendLine(wrapperLabel.ToString(TagRenderMode.Normal));
items.Append(hiddenSelected.ToString(TagRenderMode.SelfClosing));
items.AppendLine(hiddenText.ToString(TagRenderMode.SelfClosing));
items.AppendLine();
index++;
}
return MvcHtmlString.Create(items.ToString());
}
public static MvcHtmlString CheckBoxListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression)
{
var name = ExpressionHelper.GetExpressionText(expression);
var metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
return CheckBoxList(htmlHelper, name, metadata.Model as List<SelectListItem>);
}
#endregion
}

Related

Convert IQueryable object to a List and add a new Item

I have the following IQueryable object:
var user = from d in _context.Users
join userRole in _context.UserRoles on d.Id equals userRole.UserId
join role in _context.Roles on userRole.RoleId equals role.Id
where role.Name == "Liquidador"
select d;
Which then is send as a ViewBag to the View:
ViewBag.UserID = new SelectList(user.AsNoTracking(), "UserName", "Name", selectedUser);
The Problem:
I need to add a new Item to the result of the IQueryable. So I've proceeded like this:
var UserNameList = user.Select(s => new { s.Name, s.UserName }).ToList();
However, I'm missing something when I'm trying to add the new item:
UserNameList.Insert(0, new *NewWhat?* { Name = "Liquidador", UserName = "--Select--"} );
Usually I declare a new element of a specific model but in this case I don't know which model to declare for this IQueryable. Any recomendations?
Thanks
EDIT:
The IQueryable object goes to the Get Method of the View as part of a function:
public async Task<IActionResult> Management()
{
PopulateUserDropDownList();
var Tiendas = await _context.Stores.ToListAsync();
StoreEmployee model = new StoreEmployee
{
Stores = Tiendas
};
return View(model);
}
This list is then presented in a dropdownlist, inside a table:
<td class="col-md-2">
<div class="form-group" form="#(String.Format("{0}{1}","form",item.StoreID))">
<div>
<select asp-for="#item.Usuario" class="form-control" asp-items="ViewBag.UserId" form="#(String.Format("{0}{1}","form",item.StoreID))"></select>
<span asp-validation-for="#item.Usuario" class="text-danger"></span>
</div>
</div>
</td>
It seems it would be cleaner if you define a class UserDTO (or any other name that likes you more)
public class UserDTO
{
public string Name { get; set; }
public string UserName { get; set; }
}
and then you do
var UserNameList = user
.Select(s => new UserDTO { Name = s.Name, UserName = s.UserName })
.ToList();
UserNameList.Insert(0, new UserDTO { Name = "Liquidador", UserName = "--Select--"} );
OTOH... smells a little to add the empty element as part of the data array, my recommendation is to handle that on razor view and just send UserNameList with real data. Dropdown razor methods contains overloads to specify the empty element text.
If you show your HTML, we may help you to implement a better solution.

Validate Modified Model Using Annotations in EntityFramwork and ASPNET

I have this class as a part of EF Model:
class Person {
public int Id { get; set; }
[MaxLength(100, ErrorMessage="Name cannot be more than 100 characters")]
public string Name { get; set; }
}
And I have this method in my controller:
public IActionResult ChangeName(int id, string name) {
var person = db.Persons.Find(id);
if(person == null) return NotFound();
person.Name = name;
db.SaveChanges();
return Json(new {result = "Saved Successfully"});
}
Is there any way to validate person after changing the Name property using the annotation MaxLength rather than manually check for it. Becuase sometimes I might have more than one validation and I don't want to examine each one of them. Also, I might change these parameters in the future (e.g. make the max length 200), and that means I have to change it everywhere else.
So is it possible?
Your method works as long as there is one validation error per property. Also, it's quite elaborate. You can use db.GetValidationErrors() to get the same result. One difference is that errors are collected in a collection per property name:
var errors = db.GetValidationErrors()
.SelectMany(devr => devr.ValidationErrors)
.GroupBy(ve => ve.PropertyName)
.ToDictionary(ve => ve.Key, ve => ve.Select(v => v.ErrorMessage));
Okay, I found a solution to my problem, I created a method that takes the model and checks for errors:
private IDictionary<string, string> ValidateModel(Person model)
{
var errors = new Dictionary<string, string>();
foreach (var property in model.GetType().GetProperties())
{
foreach (var attribute in property.GetCustomAttributes())
{
var validationAttribute = attribute as ValidationAttribute;
if(validationAttribute == null) continue;
var value = property.GetValue(model);
if (!validationAttribute.IsValid(value))
{
errors.Add(property.Name, validationAttribute.ErrorMessage);
}
}
}
return errors;
}
UPDATE:
As stated by #Gert Arnold, the method above returns only one validation per property. Below is the fixed version which returns a list of errors for each property
public static IDictionary<string, IList<string>> ValidateModel(Person model)
{
var errors = new Dictionary<string, IList<string>>();
foreach (var property in model.GetType().GetProperties())
{
foreach (var attribute in property.GetCustomAttributes())
{
var validationAttribute = attribute as ValidationAttribute;
if (validationAttribute == null) continue;
var value = property.GetValue(model);
if (validationAttribute.IsValid(value)) continue;
if (!errors.ContainsKey(property.Name))
errors[property.Name] = new List<string>();
errors[property.Name].Add(validationAttribute.ErrorMessage);
}
}
return errors;
}

asp.net mvc validation annotation for dollar currency

I am validation for Html.TextBoxFor. Here is my code on the view
#Html.TextBoxFor(m => m.Amount, new {#class = "form-control", Value = String.Format("{0:C}", Model.Amount) })
This code takes the double value from the database like 5000.00 and displays on the UI as $5,000.00. However when the user hits the submit button, a validation error is displayed that
The value '$5,000.00' is not valid for Amount.
My validation annotation on the Model is
[Range(0, double.MaxValue, ErrorMessage = "Please enter valid dollar amount")]
To get it to submit, I had to retype as 5000.00. How can I fix this? Thanks.
When you do the value = string.Format("{0:C}", Model.Amount) in the htmlAttributes, razor will execute the C# code and return the value,"$125.67", (Assuming the value of your Amount property is 125.67M) which is a string. So the markup generated by your view will be
<input value="$125.67" class="form-control" id="Amount" name="Amount" type="text">
Now since $125.67 is not not a valide decimal value, but a string. it cannot map the value of this textbox to the Amount property of your view model which is of type decimal/doube.
What you can do is, create a new property in your view model of type string to store this formatted string value and when user submits the form, try to parse it back to a decimal variable and use it.
So add a new property to your view model
public class CreateOrderVm
{
public int Id { set;get;}
public string AmountFormatted { set;get;} // New property
public decimal Amount { set;get;}
}
And in your view, which is strongly typed to CreateOrderVm
#model CreateOrderVm
#using(Html.BeginForm())
{
#Html.TextBoxFor(m => m.AmountFormatted, new { #class = "form-control",
Value = String.Format("{0:C}", Model.Amount) })
<input type="submit" />
}
And in your HttpPost action
[HttpPost]
public ActionResult Create(CreateOrderVm model)
{
decimal amountVal;
if (Decimal.TryParse(vm.AmountFormatted, NumberStyles.Currency,
CultureInfo.CurrentCulture, out amountVal))
{
vm.Amount = amountVal;
}
else
{
//add a Model state error and return the model to view,
}
//vm.Amount has the correct decimal value now. Use it to save
// to do :Return something
}
You can create you own Binder object to handle this. First create this object:
public class DoubleModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
ValueProviderResult valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
ModelState modelState = new ModelState { Value = valueResult };
object actualValue = null;
try
{
if (!string.IsNullOrEmpty(valueResult.AttemptedValue))
actualValue = Convert.ToDouble(valueResult.AttemptedValue.Replace("$", ""), System.Globalization.CultureInfo.CurrentCulture);
}
catch (FormatException e)
{
modelState.Errors.Add(e);
}
if (bindingContext.ModelState.ContainsKey(bindingContext.ModelName))
bindingContext.ModelState[bindingContext.ModelName] = modelState;
else
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return actualValue;
}
}
Then in your Global.asax.cs file in the Application_Start function, add this:
ModelBinders.Binders.Add(typeof(double?), new DoubleModelBinder());

How to set the .Selected property of an item in ASP.NET MVC SelectList using LINQ?

I am trying to populate a SelectList in an action method for a dropdown list in the view. The dropdown gets displayed on the view just fine but the selected attribute doesn't show up using the following code:
public ActionResult Edit(int ID)
{
var ctx = new NorthwindEntities();
var product = ctx.Products.Where(p => p.ProductID == ID).SingleOrDefault();
var selectList = new SelectList(ctx.Categories, "CategoryID", "CategoryName");
selectList.Where(s => s.Value == product.CategoryID.ToString()).SingleOrDefault().Selected = true;
ViewData["CategoryID"] = selectList;
return View(product);
}
However, passing the selectedValue parameter to SelectList constructor does the job:
var selectList = new SelectList(ctx.Categories, "CategoryID", "CategoryName", product.CategoryID.ToString());
My guess is that Either the LINQ expession is the problem or SelectedItem can only be specified in the SelectList constructor. Any ideas?
This question is answered here
Set selected value in SelectList after instantiation
If you still think it is wrong then break your linq query up and check to see if you can find an object in the list, and then do an if( obj != null) { selected = true;}
But from what I have read the selected property can only be set in the constructor.
in that link is a wrapper/helper method
public static string DropDownListEx(this HtmlHelper helper, string name, SelectList selectList, object selectedValue)
{
return helper.DropDownList(name, new SelectList(selectList.Items, selectList.DataValueField, selectList.DataTextField, selectedValue));
}
That migth help you

How to use a radio button to select a list item in MVC3

I need to present my user with a list of package options, from which they select one. I don't want to use a radio button list, as I need fairly complex templating for each list item. Forgive me, but I just can't seem to figure out how to link a column of radio buttons to a selection property in my view model.
My view model has a SelectedPackageId (int), and a list of MemberPackageListItem view models that represent the individual packages. MemberPackageListItem has a PackageId (int), so I need to couple the PackageId of the selected item to the SelectedPackageId of the root view model.
It is hard for me to post code, as inheritance etc. obscures much of what you would want to see, so I'm hoping my outline of a fairly common scenario, and some general guidelines on using radio buttons in this scenario will suffice to help me continue.
I would suggest using an HtmlHelper to render your radio button list, as follows:
[SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
public static MvcHtmlString RadioButtonListFor<TModel, TList, TSelectedItem>(
this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TList>> expression,
Expression<Func<TModel, TSelectedItem>> selectedItem)
{
return RadioButtonListFor(htmlHelper, expression, selectedItem, null /* htmlAttributes */);
}
[SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
public static MvcHtmlString RadioButtonListFor<TModel, TList, TSelectedItem>(
this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TList>> expression,
Expression<Func<TModel, TSelectedItem>> selectedItem, object htmlAttributes)
{
return RadioButtonListFor(htmlHelper, expression, selectedItem, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
}
[SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
public static MvcHtmlString RadioButtonListFor<TModel, TList, TSelectedItem>(
this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TList>> expression,
Expression<Func<TModel, TSelectedItem>> selectedItem, IDictionary<string, object> htmlAttributes)
{
if (expression == null)
{
throw new ArgumentNullException("expression");
}
ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
IEnumerable<SelectListItem> items = null;
if (metadata.Model != null)
{
IEnumerable<SelectListItem> modelItems = (IEnumerable<SelectListItem>)metadata.Model;
if (modelItems != null)
{
items = modelItems;
}
}
ModelMetadata selectedItemMetadata = ModelMetadata.FromLambdaExpression(selectedItem, htmlHelper.ViewData);
return RadioButtonListHelper(htmlHelper, metadata, selectedItemMetadata, ExpressionHelper.GetExpressionText(selectedItem), items, htmlAttributes);
}
private static MvcHtmlString RadioButtonListHelper(HtmlHelper htmlHelper, ModelMetadata metadata,
ModelMetadata selectedItemMetadata, string name, IEnumerable<SelectListItem> selectList, IDictionary<string, object> htmlAttributes)
{
// Verify arguments
if (String.IsNullOrEmpty(name)) throw new ArgumentNullException("name", "Name cannot be null");
if (selectList == null) throw new ArgumentNullException("selectList", "Select list cannot be null");
if (selectList.Count() < 1) throw new ArgumentException("Select list must contain at least one value", "selectList");
string fullName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);
string fullId = htmlHelper.ViewData.TemplateInfo.HtmlFieldPrefix + "_" + name;
IDictionary<string, object> validationAttributes = htmlHelper
.GetUnobtrusiveValidationAttributes(ExpressionHelper.GetExpressionText(name), selectedItemMetadata);
// Define items
StringBuilder items = new StringBuilder();
// Loop through items
Int32 index = 0;
foreach (SelectListItem i in selectList)
{
// Define check box input
TagBuilder input = new TagBuilder("input");
input.MergeAttribute("type", "radio");
input.MergeAttribute("name", fullName, true);
if (i.Selected)
input.MergeAttribute("checked", "checked");
input.MergeAttribute("value", i.Value);
if (index == 0)
input.MergeAttributes(validationAttributes);
input.MergeAttributes(htmlAttributes);
// Define label
TagBuilder label = new TagBuilder("label");
label.MergeAttribute("for", fullId + "[" + index.ToString() + "].Selected");
label.InnerHtml = i.Text;
// Add item
items.AppendFormat("\r\t<div>\r\t\t{0}\r\t\t{1}\r\t</div>",
input.ToString(TagRenderMode.Normal),
label.ToString(TagRenderMode.Normal));
index++;
}
// Return list
return new MvcHtmlString(items.ToString() + "\r");
}
Please note that MemberPackageListItem must be of type IEnumerable<SelectListItem>. Usage is as follows (Razor syntax):
#Html.RadioButtonListFor(m => m.MemberPackageListItem, m => m.SelectedPackageId)
counsellorben
While I appreciate the technical comprehensiveness of #counsellorben's answer, and will keep it around for future use, today I arrived at a more immediate and not altogether clumsy jQuery solution. The selectors could be more specific, but I have no need now. My solution is below. Radio type inputs are grouped by their name attribute, which is given a different index for each row. Therefore:
$(function () {
// Back up current names of package radio buttons, then make all their names the same for grouping.
$("#packageForm :radio[name$='.IsSelected']").each(function () {
$(this).attr("oldname", $(this).attr("name"));
});
$(":radio[name$='.IsSelected']").attr("name", "Package.IsSelected");
// Hook the 'submit' click to restore original radio button names.
$("#packageForm :submit").click(function () {
$(":radio[name='Package.IsSelected']").each(function () {
$(this).attr("oldname", $(this).attr("name"));
});
});
});
IsSelected is my property per row that tells me if that row is selected, it isn't a jQuery or DOM property.

Resources