MVC 3 - validating values on multiple rows - asp.net-mvc-3

I have an MVC project similar to this...
Model
Public Class ItemDetails
Public Property SerialNo As Integer
Public Property Description As String
Public Property GroupNo As Integer
Public Property Price As String
Public Property Quantity As Integer
End Class
Controller
Function ListItems() As ActionResult
' GetItems retrieves the items from the database
Dim i As List(Of ItemDetails) = ItemsRepository.GetItems
Return View(i)
End Function
View
#ModelType List(Of MyApp.ItemDetails)
#Using Html.BeginForm()
Dim RowNo As Integer
For i As Integer = 0 To Model.Count - 1
RowNo = i
#Html.HiddenFor(Function(model) model(RowNo).SerialNo)
#Html.LabelFor(Function(model) model(RowNo).Description)
#Html.HiddenFor(Function(model) model(RowNo).GroupNo)
#Html.LabelFor(Function(model) model(RowNo).Price)
#Html.TextBoxFor(Function(model) model(RowNo).Quantity)
Next
End Using
Note: This is done from memory so may not be completely accurate.
As you can see this displays a list of items. The items are retrieved from a database. Each item has a description and a Group No. The user can enter how many of each item they would like to order.
Two or more items can be in the same group. For examle there might be: Item 1 in Group 1, Item 2 in Group 1, Item 3 in Group 2 and Item 4 in Group 3.
When the user clicks submit I would like to validate that for each Group No the combined Quantity does not exceed 10. So in the example above if I enter a Quantity of 7 for Item 1 then quantity for Item 2 must be 3 or less.
I can validate this easily client side.
What is the best way to go about validating this server side (where and how)?
I need to total the combined value of Quantity does not exceed 10 for each Group No and if it does display an error.

Personally I use FluentValidation.NET for this kind of tasks. It allows you to define your complex validation rules for a given view model into a separate layer, express them in a fluent manner, it has an absolutely marvelous and seamless integration with ASP.NET MVC and allows you to unit test your validation logic in isolation.
If you don't want to use third party libraries you could always write a custom validation attribute and decorate your view model property with it. You could introduce a view model that will contain the collection of ItemDetails as a property and then decorate this property with your custom validator.
public class ItemDetails
{
public int SerialNo { get; set; }
public string Description { get; set; }
public int GroupNo { get; set; }
// Please take a note that I have allowed myself
// to use the decimal datatype for a property called
// Price as I was a bit shocked to see String in your code
public decimal Price { get; set; }
public int Quantity { get; set; }
}
public class MyViewModel
{
[EnsureMaxGroupItems(10, ErrorMessage = "You cannot have more than 10 items in each group")]
public IList<ItemDetails> Items { get; set; }
}
and the validation attribute itself:
[AttributeUsage(AttributeTargets.Property)]
public class EnsureMaxGroupItemsAttribute : ValidationAttribute
{
public int MaxItems { get; private set; }
public EnsureMaxGroupItemsAttribute(int maxItems)
{
MaxItems = maxItems;
}
public override bool IsValid(object value)
{
var items = value as IEnumerable<ItemDetails>;
if (items == null)
{
return true;
}
return items
.GroupBy(x => x.GroupNo)
.Select(g => g.Sum(x => x.Quantity))
.All(quantity => quantity <= MaxItems);
}
}
and finally your controller actions will work with the view model:
public ActionResult ListItems()
{
var model = new MyViewModel
{
Items = ItemsRepository.GetItems()
};
return View(model);
}
[HttpPost]
public ActionResult ListItems(MyViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
...
}
and next the corresponding strongly typed view:
#model MyViewModel
#Html.ValidationSummary()
#using (Html.BeginForm())
{
#Html.EditorFor(x => x.Items)
<button type="submit">Go go go</button>
}
and the last bit is the corresponding editor template that will automatically be rendered for each element of the Items collection so that you don't even need to write for loops (~/Views/Shared/EditorTemplates/ItemDetails.cshtml):
#model ItemDetails
#Html.HiddenFor(x => x.SerialNo)
#Html.LabelFor(x => x.Description)
#Html.HiddenFor(x => x.GroupNo)
#Html.LabelFor(x => x.Price)
#Html.TextBoxFor(x => x.Quantity)

Related

How to update hierarchical ViewModel?

I am stuck with this problem.
I have a model AssessmentModel defined like this:
public class AssessmentModel
{
public Respondent Respondent { get; set; }
public List<CompetencyModel> Competencies { get; set; }
}
public class CompetencyModel
{
public int Id { get; set; }
public string Name { get; set; }
public List<ResultModel> Results { get; set; }
}
public class ResultModel
{
public int Id { get; set; }
public int Score { get; set; }
}
All I need is to set value to the Score property of ResultModel.
Score is the only editable property here.
And I have just 1 View only, this view has a #model List, it displays a list of CompetencyModel items with Edit button for each one.
When I click the Edit button, the Id of CompetencyModel is passed to the same View, and the View draws an Edit form for ResultModel items that belong to the selected CompetencyModel.
However the form for ResultModel items exists on the same View, and the model of the View is still #model List.
How can I get to the Score property by using bindable Html.EditorFor(m=>m.Score) helper for each ResultModel item?
The View is defined like this:
#model List<CompetencyModel>
#foreach(var comp in Model)
{
<p>#comp.Name</p>
Edit
}
In the controller I set ViewBag.CurrentId = comp.Id, and at the bottom of the View:
if(ViewBag.CurrentId != null) //draw a form for ResultModel items
{
// What should I do now?
// how cant I use Html.EditorFor(m=>...) if the Model is still List<CompetencyModel>
}
I need to get to a single ResultModel entity to set a value to a Score property.
Thank you.
You should be able to get this done using Linq. Consider having the following code segment in the your last if statement
var result = Model.Results.FirstOrDefault(r => r.Id == ViewBag.CurrentId);
I dont have a IDE with me, so watchout for syntext errors

Single property not getting bound on HttpPost

I'm working on the first MVC3 project at our company, and I've hit a block. No one can seem to figure out what's going on.
I have a complex Model that I'm using on the page:
public class SpaceModels : List<SpaceModel> {
public bool HideValidation { get; set; }
[Required(ErrorMessage=Utilities.EffectiveDate + Utilities.NotBlank)]
public DateTime EffectiveDate { get; set; }
public bool DisplayEffectiveDate { get; set; }
}
In the Controller, I create a SpaceModels object with blank SpaceModels for when Spaces get combined (this would be the destination Space).
// Need a list of the models for the View.
SpaceModels models = new SpaceModels();
models.EffectiveDate = DateTime.Now.Date;
models.DisplayEffectiveDate = true;
models.Add(new SpaceModel { StoreID = storeID, SiteID = siteID, IsActive = true });
return View("CombineSpaces", models);
Then in the View, I am using that SpaceModels object as the Model, and in the form making a TextBox for the Effective Date:
#model Data.SpaceModels
#using (Html.BeginForm("CombineSpaces", "Space")) {
<div class="EditLine">
<span class="EditLabel LongText">
New Space Open Date
</span>
#Html.TextBoxFor(m => m.EffectiveDate, new {
size = "20",
#class = "datecontrol",
// Make this as a nullable DateTime for Display purposes so we don't start the Calendar at 1/1/0000.
#Value = Utilities.ToStringOrDefault(Model.EffectiveDate == DateTime.MinValue ? null : (DateTime?)Model.EffectiveDate, "MM/dd/yyyy", string.Empty)
})
#Html.ValidationMessageFor(m => m.EffectiveDate)
</div>
<hr />
Html.RenderPartial("_SpaceEntry", Model);
}
The Partial View that gets rendered iterates through all SpaceModels, and creates a containing the Edit fields for the individual SpaceModel objects. (I'm using the List to use the same Views for when the Spaces get Subdivided as well.)
Then on the HttpPost, the EffectiveDate is still back at it's DateTime.MinValue default:
[HttpPost]
public ActionResult CombineSpaces(SpaceModels model, long siteID, long storeID, DateTime? effectiveDate) {
// processing code
}
I added that DateTime? effectiveDate parameter to prove that the value when it gets changed does in fact come back. I even tried moving the rendering of the TextBox into the _SpaceEntry Partial View, but nothing worked there either.
I did also try using the #Html.EditorFor(m => m.EffectiveDate) in place of the #Html.TextBoxFor(), but that still returned DateTime.MinValue. (My boss doesn't like giving up the control of rendering using the #Html.EditorForModel by the way.)
There has to be something simple that I'm missing. Please let me know if you need anything else.
Looking at the source code for DefaultModelBinder, specifically BindComplexModel(), if it detects a collection type it will bind the individual elements but will not attempt to bind properties of the list object itself.
What model binding does is attempt to match the names of things or elements in the view to properties in your model or parameters in your action method. You do not have to pass all of those parameters, all you have to do is add them to your view model, then call TryUpdateModel in your action method. I am not sure what you are trying to do with SpaceModel or List but I do not see the need to inherit from the List. Im sure you have a good reason for doing it. Here is how I would do it.
The view model
public class SpacesViewModel
{
public DateTime? EffectiveDate { get; set; }
public bool DisplayEffectiveDate { get; set; }
public List<SpaceModel> SpaceModels { get; set; }
}
The GET action method
[ActionName("_SpaceEntry")]
public PartialViewResult SpaceEntry()
{
var spaceModels = new List<SpaceModel>();
spaceModels.Add(
new SpaceModel { StoreID = storeID, SiteID = siteID, IsActive = true });
var spacesVm = new SpacesViewModel
{
EffectiveDate = DateTime.Now,
DisplayEffectiveDate = true,
SpaceModels = spaceModels
};
return PartialView("_SpaceEntry", spacesVm);
}
The POST action method
[HttpPost]
public ActionResult CombineSpaces()
{
var spacesVm = new SpacesViewModel();
// this forces model binding and calls ModelState.IsValid
// and returns true if the model is Valid
if (TryUpdateModel(spacesVm))
{
// process your data here
}
return RedirectToAction("Index", "Home");
}
And the view
<label>Effective date: </label>
#Html.TextBox("EffectiveDate", Model.EffectiveDate.HasValue ?
Model.EffectiveDate.Value.ToString("MM/dd/yyyy") : string.empty,
new { #class = "datecontrol" })
Sometimes you need to explicitly bind form data using hidden fields such as
#Html.HiddenField("EffectiveDate", Model.EfectiveDate.)
In order to bind the properties of the SpaceModel object you can add individual properties such as SiteID to the view model or add a SpaceModel property for a single SpaceModel. If you want to successfully bind a complex model, add it as a Dictionary populated with key-value pairs rather than a List. You should then add the dictionary to the view model. You can even add a dictionary of dictionaries for hierarchical data.
I hope this helps :)

implementing dropdownlist in asp.net mvc 3

I am teaching myself asp .net mvc3. I have researched a lot but the more I read the more confused I become. I want to create a page where users can register their property for sale or rent.
I have created a database which looks like this:
public class Property
{
public int PropertyId { get; set; }
public int PropertyType { get; set; }
ยทยทยท
public int Furnished { get; set; }
...
}
Now, I want dropdownlistfor = PropertyType and Furnished.
Property type would be
1 Flat
2 House
3 Detached House
...
Furnished would be:
1 Furnished
2 UnFurnished
3 PartFurnished
...
Now, I am really not sure where to keep this information in my code. Should I have 2 tables in my database which store this lookup? Or should I have 1 table which has all lookups? Or should I just keep this information in the model?
How will the model bind to PropertyType and Furnished in the Property entity?
Thanks!
By storing property types and furnished types in the database, you could enforce data integrity with a foreign key, rather than just storing an integer id, so I would definitely recommend this.
It also means it is future proofed for if you want to add new types. I know the values don't change often/will never change but if you wanted to add bungalow/maisonette in the future you don't have to rebuild and deploy your project, you can simply add a new row in the database.
In terms of how this would work, I'd recommend using a ViewModel that gets passed to the view, rather than passing the database model directly. That way you separate your database model from the view, and the view only sees what it needs to. It also means your drop down lists etc are strongly typed and are directly in your view model rather than just thrown into the ViewBag. Your view model could look like:
public class PropertyViewModel
{
public int PropertyId { get; set; }
public int PropertyType { get; set; }
public IEnumerable<SelectListItem> PropertyTypes { get; set; }
public int Furnished { get; set; }
public IEnumerable<SelectListItem> FurnishedTypes { get; set; }
}
So then your controller action would look like:
public class PropertiesController : Controller
{
[HttpGet]
public ViewResult Edit(int id)
{
Property property = db.Properties.Single(p => p.Id == id);
PropertyViewModel viewModel = new PropertyViewModel
{
PropertyId = property.Id,
PropertyType = property.PropertyType,
PropertyTypes = from p in db.PropertyTypes
orderby p.TypeName
select new SelectListItem
{
Text = p.TypeName,
Value = g.PropertyTypeId.ToString()
}
Furnished = property.Furnished,
FurnishedTypes = from p in db.FurnishedTypes
orderby p.TypeName
select new SelectListItem
{
Text = p.TypeName,
Value = g.FurnishedTypeId.ToString()
}
};
return View();
}
[HttpGet]
public ViewResult Edit(int id, PropertyViewModel propertyViewModel)
{
if(ModelState.IsValid)
{
// TODO: Store stuff in the database here
}
// TODO: Repopulate the view model drop lists here e.g.:
propertyViewModel.FurnishedTypes = from p in db.FurnishedTypes
orderby p.TypeName
select new SelectListItem
{
Text = p.TypeName,
Value = g.FurnishedTypeId.ToString()
};
return View(propertyViewModel);
}
}
And your view would have things like:
#Html.LabelFor(m => m.PropertyType)
#Html.DropDownListFor(m => m.PropertyType, Model.PropertyTypes)
I usually handle this sort of situation by using an enumeration in code:
public enum PropertyType {
Flat = 1,
House = 2,
Detached House = 3
}
Then in your view:
<select>
#foreach(var val in Enum.GetNames(typeof(PropertyType)){
<option>val</option>
}
</select>
You can set the id of the option equal to the value of each item in the enum, and pass it to the controller.
EDIT: To directly answer your questions:
You can store them as lookups in the db, but for small unlikely to change things, I usually just use an enum, and save a round trip.
Also look at this approach, as it looks better than mine:
Converting HTML.EditorFor into a drop down (html.dropdownfor?)

select the value to populate html.dropdownlist

I have two classes as follows
public class ODCTE_Major
{
public int ODCTE_MajorId { get; set; }
public string OfficialMajorName { get; set; }
public string MajorCode { get; set; }
... More unrelated code ....
}
AND
public class CareerMajor
{
...lots of unrealted code to this question left out
public int ODCTE_MajorId { get; set; }
public virtual ODCTE_Major ODCTE_Major { get; set; }
}
I added a controller with CRUD methods and in the create.cshtml there is this line
<div class="editor-field">
#Html.DropDownList("ODCTE_MajorId", String.Empty)
#Html.ValidationMessageFor(model => model.ODCTE_MajorId)
</div>
The select list populates it with the OfficialMajorName from ODCTE_Major. I need the select list to populate with the MajorCode or a value that looks like MajorCode - OfficialMajorName.
Could someone provide assistance for how this is done, please?
Thanks.
Add this to ODCTE_Major:
public string MajorDisplayName
{
get { return string.Format("{0} - {1}", MajorCode, OfficialMajorName); }
}
This is just a read only property used to create the display text in the format you want the menu to use.
Then in CareerMajor, add:
public IEnumerable<ODCTE_Major> Majors{ set; get; } // Thank you Shyju!
This will give you a place in your view model to pass the list of Majors you want in your menu to the view.
Then in your action method when you're creating a CareerMajor view model to send to the view, populate the new IEnumberable with the ODCTE_Major entities you'd like displayed in your menu.
On the view page:
#Html.DropDownListFor(m => m.ODCTE_MajorId, new SelectList(Model.Majors, "ODCTE_MajorId", "MajorDisplayName", Model.ODCTE_MajorId), "Select One")
This creates a SelectList to populate the drop down with. The SelectList constructor is saying use ODCTE_MajorId as the value for a SelectListItem in the menu, and to use MajorDisplayName as the text to actually display in the menu. It sets the selected value, if there is one, and adds a null item with the text "Select One" to the top of the menu. Feel free to take that final argument out if you don't want the null text.
Have your ViewModel hold a Collection property to represent all available Majors (for poulating the Dropdown)
public class CareerMajor
{
//other proiperties
public int ODCTE_MajorId { get; set; }
public IEnumerable<ODCTE_Major> Majors{ set; get; }
}
And in your GET Action, fill it and send it to your strongly typed view
pubilc ACtionResult Create()
{
var viewModel=new CareerMajor();
viewModel.Majors=db.GetllAllMajors(); // getting a list of ODCTE_Major objects
return View(viewModel);
}
and in the View, use the DropDownListFor HTML Helper method.
#model CareerMajor
#Html.BeginForm())
{
#Html.DropDownListFor(m=>m.ODCTE_MajorId,
new SelectList(Model.Majors,"ODCTE_MajorId ","MajorCode"),"select one..")
//other elements
}
In your controller action:
ViewBag.ODCTE_MajorId = new SelectList(availableMajors, "ODCTE_MajorId", "MajorCode");
*second and third parameters are the names of the value and text fields respectively
Then in your view:
#Html.DropDownList("ODCTE_MajorId", String.Empty)
where availableMajors is an IEnumerable that contains the majors you want to list.

MVC 3 Unobtrusive validation of a list

Question
I have created a server-side property level validation attribute. But instead of applying it to an individual field I've applied it to a List. This allows me to validate the model as a whole.
I now need to know how to convert this to work using the unobtrusive client-side validation built into MVC 3.
My current code is below to illustrate my issue...
Scenario
The basic scenario was the ability total up all the Quantity values for every row in a List grouped by the GroupNo field. If the sum of any of the groups was more than 10 then an error should be displayed.
I was kindly given an answer in a previous post to make this work server-side using a validation attribute against a List...
The model:
public class ItemDetails
{
public int SerialNo { get; set; }
public string Description { get; set; }
public int GroupNo { get; set; }
public decimal Price { get; set; }
public int Quantity { get; set; }
}
public class MyViewModel
{
[EnsureMaxGroupItems(10, ErrorMessage = "You cannot have more than 10 items in each group")]
public IList<ItemDetails> Items { get; set; }
}
and the validation attribute itself:
[AttributeUsage(AttributeTargets.Property)]
public class EnsureMaxGroupItemsAttribute : ValidationAttribute
{
public int MaxItems { get; private set; }
public EnsureMaxGroupItemsAttribute(int maxItems)
{
MaxItems = maxItems;
}
public override bool IsValid(object value)
{
var items = value as IEnumerable<ItemDetails>;
if (items == null)
{
return true;
}
return items
.GroupBy(x => x.GroupNo)
.Select(g => g.Sum(x => x.Quantity))
.All(quantity => quantity <= MaxItems);
}
}
and finally your controller actions will work with the view model:
public ActionResult ListItems()
{
var model = new MyViewModel
{
Items = ItemsRepository.GetItems()
};
return View(model);
}
[HttpPost]
public ActionResult ListItems(MyViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
...
}
and next the corresponding strongly typed view:
#model MyViewModel
#Html.ValidationSummary()
#using (Html.BeginForm())
{
#Html.EditorFor(x => x.Items)
<button type="submit">Go go go</button>
}
and the last bit is the corresponding editor template that will automatically be rendered for each element of the Items collection so that you don't even need to write for loops (~/Views/Shared/EditorTemplates/ItemDetails.cshtml):
#model ItemDetails
#Html.HiddenFor(x => x.SerialNo)
#Html.LabelFor(x => x.Description)
#Html.HiddenFor(x => x.GroupNo)
#Html.LabelFor(x => x.Price)
#Html.TextBoxFor(x => x.Quantity)
Client-side unobtrusive validation possible?
I would like it all to validate using unobtrusive MVC validation. But I cannot figure out how to unobtrusively validate the EnsureMaxGroupItemsAttribute attribute against the list as a whole.
I've implemented IClientValidatable in this way:
Public Function GetClientValidationRules(metadata As System.Web.Mvc.ModelMetadata, context As System.Web.Mvc.ControllerContext) As System.Collections.Generic.IEnumerable(Of System.Web.Mvc.ModelClientValidationRule) Implements System.Web.Mvc.IClientValidatable.GetClientValidationRules
Dim result = New List(Of ModelClientValidationRule)
Dim rule = New ModelClientValidationRule() With { _
.ErrorMessage = "You cannot have more than 10 items in each group", _
.ValidationType = "itemscheck"}
result.Add(rule)
Return result
End Function
Note: the mix of VB and C# is only because the previous question I asked was answered in C#. The project is in VB but I don't mind an answer in C#.
I've created the adaptor in my JS file:
jQuery.validator.unobtrusive.adapters.addBool("itemscheck");
... and ...
jQuery.validator.addMethod("itemscheck", function (value, element, params) {
// The check has been omitted for the sake of saving space.
// However this method never gets called
return false;
});
Is there a way to hook this up to work unobtrusively?
This is not possible because your custom attribute is placed in the collection property and there are no HTML5 data-* attributes emitted at all. It is not a supported scenario by the unobtrusive client validation framework. You could write directly a custom jquery validate rule to handle this scenario if you need client validation for it.

Resources