ASP.Net MVC 4.0 - Validation Issues With array based properties on ViewModel - asp.net-mvc

ASP.Net MVC 4.0 - Validation Issues With array based properties on ViewModel .
Scenario :
When a ViewModel has a string array as a property type,the default Scaffolding template for say, Edit, does not render the that property in the markup.
Say, I have ViewModel setup like this :
Employee.cs
public class Employee
{
[Required]
public int EmpID
{
get;
set;
}
[Required]
public string FirstName
{
get;
set;
}
[Required]
public string LastName
{
get;
set;
}
[Required]
public string[] Skills
{
get;
set;
}
}
}
The (strongly typed) Edit View generated by the scaffolding template, as shown below, typically skips the portion relevant to field Skills.
**Employee.cshtml**
#model StringArray.Models.Employee
#{
ViewBag.Title = "EditEmployee";
}
<h2>EditEmployee</h2>
#using (Html.BeginForm()) {
#Html.ValidationSummary(true)
<fieldset>
<legend>Employee</legend>
<div class="editor-label">
#Html.LabelFor(model => model.EmpID)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.EmpID)
#Html.ValidationMessageFor(model => model.EmpID)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.FirstName)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.FirstName)
#Html.ValidationMessageFor(model => model.FirstName)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.LastName)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.LastName)
#Html.ValidationMessageFor(model => model.LastName)
</div>
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
}
The corresponding Controller code is
..
[HttpGet]
public ActionResult EditEmployee()
{
Employee E = new Employee()
{
EmpID = 1,
FirstName = "Sandy",
LastName = "Peterson",
Skills = new string[] { "Technology", "Management", "Sports" }
};
return View(E);
}
[HttpPost]
public ActionResult EditEmployee(Employee E)
{
return View(E);
}
To get the missing section for the Skills field, I added
Snippet to the View
<div class="editor-label">
#Html.LabelFor(model => model.Skills)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Skills)
#Html.ValidationMessageFor(model => model.Skills)
</div>
Corresponding UIHint to the ViewModel
[UIHint("String[]")]
public string[] Skills ...
EditorTemplates inside relevant folder as
~\View\shared\EditorTemplates\String[].cshtml
and
~\View\shared\EditorTemplates\mystring.cshtml
string[].cshtml
#model System.String[]
#if(Model != null && Model.Any())
{
for (int i = 0; i < Model.Length; i++)
{
#Html.EditorFor(model => model[i], "mystring")
//Html.ValidationMessageFor(model => model[i])
}
}
mystring.cshtml
#model System.String
#{
//if(Model != null)
{
//To resolve issue/bug with extra dot getting rendered in the name - like
//Skills.[0], Skills.[1], etc.
//ViewData.TemplateInfo.HtmlFieldPrefix=ViewData.TemplateInfo.HtmlFieldPrefix.Replace(".[", "[");
#Html.TextBoxFor(model => model)
}
}
But despite this all, the Validations for the Skills section [with 3 fields/elements - refer the EditEmployee method in Controller above.]
are entirely skipped, on postback.
I tried below changes inside the mystring.cshtml EditorTemplate :
//to correct the rendered names in the browser from Skills.[0] to Skills for all the 3 items in the
//Skills (string array), so that model binding works correctly.
string x = ViewData.TemplateInfo.HtmlFieldPrefix;
x = x.Substring(0, x.LastIndexOf("."));
#Html.TextBoxFor(model =>model, new { Name = x })
Postback WORKS But Validations DON'T, since the "data-valmsg-for" still points to <span class="field-validation-valid" data-valmsg-for="Skills" data-valmsg-replace="true"></span>
and thus doesn't apply at granular level - string element level.
Lastly, I tried removing #Html.ValidationMessageFor(model => model.Skills) from the Employee.cshtml and correspondingly adding the
same to string[].cshtml as #Html.ValidationMessageFor(model => model[i]).
But this led to data-valmsg-for getting rendered for each granular string element like
data-valmsg-for="Skills.[0]" ,
data-valmsg-for="Skills.[1]" and data-valmsg-for="Skills.[2]", respectively.
Note: Validations work for other fields - EmpID, FirstName LastName, BUT NOT for Skills.
Question
How do I set the data-valmsg-for="Skills" for each of the above three granular elements related to Skills property.
I am stuck on this for quite some time now. It would be nice if some one can point out the issue, at the earliest.
Thanks, Sandesh L

This is where you like to change
[Required]
public string[] Skills
{
get;
set;
}
You are giving validation on the array.
you might want to have a new string class call Skill
[Required]
public string Skill
{
get;
set;
}
And you can change to you model with
[Required]
public List<Skill> Skills
{
get;
set;
}
I prefer using List instead of array. Then, you can change you skill view according to the model updated
you template view can be something like
#model IEnumerable<Skill>
<div class="editor-label">
<h3>#Html.LabelFor(model=> model.Skills)
</h3>
</div>
<div class="editor-field">
#foreach (var item in Model)
{ #Html.Label(model => item)
#Html.TextBoxFor(model => item) <br/>
}
#Html.ValidationMessageFor(model => item)

Related

Passing values from the view to the controller in MVC4

I'm trying to pass these textbox values to the controller:
#model DTOs.QuestionDTO
#{
ViewBag.Title = "AddQuestion";
}
<h2>AddQuestion</h2>
#using (Html.BeginForm("AddQuestionDB", "Exam"))
{
<fieldset>
<legend>Add Question</legend>
<div class="editor-label">
#Html.LabelFor(model => model.QuestionDes)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.QuestionDes , new { #id="question" , #name="question"})
#Html.ValidationMessageFor(model => model.QuestionDes)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.Answer1)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Answer1 , new { #id="a1" , #name="a1"})
#Html.ValidationMessageFor(model => model.Answer1)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.Answer2)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Answer2 , new { #id="a2" , #name="a2"})
#Html.ValidationMessageFor(model => model.Answer2)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.Answer3)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Answer3 , new { #id="a3" , #name="a3"})
#Html.ValidationMessageFor(model => model.Answer3)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.Answer4)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Answer4 , new { #id="a4" , #name="a4"})
#Html.ValidationMessageFor(model => model.Answer4)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.Correct)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Correct , new { #id="correct" , #name="correct"})
#Html.ValidationMessageFor(model => model.Correct)
</div>
<p>
<input type="submit" value="Add" />
</p>
</fieldset>
}
<div>
#Html.ActionLink("Back to Login", "Login")
</div>
#section Scripts {
#Scripts.Render("~/bundles/jqueryval")
}
this is how my controller method looks like:
public ActionResult AddQuestionDB(string question, string a1, string a2, string a3, string a4, string correct)
{
ViewBag.a1 = a1;
ViewBag.a2 = a2;
ViewBag.a3 = a3;
ViewBag.a4 = a4;
ViewBag.correct = correct;
return View();
}
And i have created a View to display this Viewbag variables... but these variables wont come up.
it seem to be that these variables are null..
#{
ViewBag.Title = "AddQuestionDB";
}
<h2>AddQuestionDB</h2>
<p>#Session["q"]</p>
<p>#ViewBag.question</p>
<p>#ViewBag.a1</p>
<p>#ViewBag.a2</p>
<p>#ViewBag.a3</p>
<p>#ViewBag.a4</p>
<p>#ViewBag.correct</p>
this is how my DTO looks like:
namespace DTOs
{
public class QuestionDTO
{
public int ID { get; set; }
public string QuestionDes { get; set; }
public string Answer1 { get; set; }
public string Answer2 { get; set; }
public string Answer3 { get; set; }
public string Answer4 { get; set; }
public int Correct { get; set; }
}
}
Can you please explain, how i should do this??
thank you!!!
Instead of -
public ActionResult AddQuestionDB(string question, string a1, string a2, string a3, string a4, string correct)
{
// Your custom code
return View();
}
have this code -
public ActionResult AddQuestionDB(QuestionDTO question)
{
// Use 'question' object here to get posted values.
return View();
}
I replicated your scenario in a small sample project on my local, here is the outcome when I debugged the code with breakpoint -
Html.EditorFor does not support setting attributes, which is why your original code wasn't working. The name attribute didn't match the parameter names in your controller action, so they were assigned null.
You can either use #ramiramilu's answer, or you could use TextBoxFor instead:
#Html.TextBoxFor(model => model.Correct , new { id="correct" , Name="correct"})
(Note that Name must be capitalized in order for this to work).
Example: https://dotnetfiddle.net/ZfzCaZ

ASP.NET MVC bool value returned from the view is 1 or 0

I am using ASP.NET MVC, and I have some problems using a CheckBoxFor. Here is my problem:
I have the following code in the view:
#Html.CheckBoxFor(model => model.stade, new { #id = "stade" })
model.stade is of type bool. In my controller, I have:
//Editar
[HttpPost]
public ActionResult InvoiceType(int Id, string Name, string Code, string Stade)
{
clsInvoiceTypea Model = new clsInvoiceType();
Model.Id = Id;
Model.Name = Name;
Model.Code = Code;
Model.Stade = stade== "1" ? true : false;
return PartialView(Model);
}
I get an error, because when Model.Stade is submitted to the view the value is 1 or 0, and I get an error saying "Can not recognize the string as a valid Boolean", but if Model.stade is boolean why the model is submitted to the view like 0 or 1? How I can I resolve this?
Here goes my solution -
Let your Model be -
public class clsInvoiceTypea
{
public int Id { get; set; }
public string Name { get; set; }
public string Code { get; set; }
public bool stade { get; set; }
}
Let your HttpGet Action be -
public ActionResult GetInvoice()
{
clsInvoiceTypea type = new clsInvoiceTypea();
return View(type);
}
And the corresponding view -
#model YourValidNameSpace.clsInvoiceTypea
#{
ViewBag.Title = "GetInvoice";
}
<h2>GetInvoice</h2>
#using (Html.BeginForm("SubmitData","Home",FormMethod.Post)) {
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<fieldset>
<legend>clsInvoiceTypea</legend>
<div class="editor-label">
#Html.LabelFor(model => model.Name)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Name)
#Html.ValidationMessageFor(model => model.Name)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.Code)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Code)
#Html.ValidationMessageFor(model => model.Code)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.stade)
</div>
<div class="editor-field">
#Html.CheckBoxFor(model => model.stade)
#Html.ValidationMessageFor(model => model.stade)
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
Let the following be your HttpPost Action -
[HttpPost]
public ActionResult SubmitData(clsInvoiceTypea model)
{
return View();
}
When you run the code, you will get following view -
When you select the checkbox and hit the create button, if you put a breakpoint on POST method and check the value, you will get true.

InvalidOperationException in Razor View with MVC4

I'm trying to build a flashcard application to learn MVC4. A Set contains Cards, and a Card contains Sides (to facilitate one or many-sided cards).
I have the following viewModel for Card Creation:
public class CreateCardViewModel
{
[HiddenInput(DisplayValue = false)]
public int SetId { get; set; }
[Required]
public ICollection<Side> Sides { get; set; }
[Required]
[DataType(DataType.Date)]
public DateTime DateCreated { get; set; }
[Required]
public bool IsReady { get; set; }
}
And the following actions defined for Create:
[HttpGet]
public ActionResult Create(int setId)
{
var model = new CreateCardViewModel();
// attach card to current set
model.SetId = setId;
// create a new Side
var side = new Side() {Content = "Blank Side"};
// Add this to the model's Collection
model.Sides = new Collection<Side> { side };
return View(model);
}
[HttpPost]
public ActionResult Create(CreateCardViewModel viewModel)
{
if (ModelState.IsValid)
{
var set = _db.Sets.Single(s => s.SetId == viewModel.SetId);
var card = new Card {Sides = viewModel.Sides};
set.Cards.Add(card);
_db.Save();
}
return View(viewModel);
}
In the view, I'm trying to start by displaying the Side that I created in the Controller, and allowing the user to edit it. I get "InvalidOperationException: Templates can be used only with field access, property access, single-dimension array index, or single-parameter custom indexer expressions." when I try to run with the following markup in the view:
<h2>Create</h2>
#using (Html.BeginForm()) {
#Html.ValidationSummary(true)
<fieldset>
<legend>CreateCardViewModel</legend>
<div class="editor-label">
#Html.LabelFor(model => model.SetId)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.SetId)
#Html.ValidationMessageFor(model => model.SetId)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.DateCreated)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.DateCreated)
#Html.ValidationMessageFor(model => model.DateCreated)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.IsReady)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.IsReady)
#Html.ValidationMessageFor(model => model.IsReady)
</div>
// OFFENDING CODE
#foreach (var side in Model.Sides)
{
#Html.EditorFor(model => model.Sides.ElementAt(side.SideId))
}
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
How can I let the user edit the existing Side when creating a card?
Instead of using ElementAt(), just use the normal [] index operator:
#Html.EditorFor(model => model.Sides[side.SideId])

ICollection from ViewModel showing up as null, modelstate is not valid in MVC4

I have the following viewModel:
public class CreateCardViewModel
{
[HiddenInput(DisplayValue = false)]
public int SetId { get; set; }
[Required]
public ICollection<Side> Sides { get; set; }
[Required]
[DataType(DataType.Date)]
public DateTime DateCreated { get; set; }
[Required]
public bool IsReady { get; set; }
}
And the following actions defined for Create:
[HttpGet]
public ActionResult Create(int setId)
{
var model = new CreateCardViewModel();
// attach card to current set
model.SetId = setId;
// create a new Side
var side = new Side() {Content = "Blank Side"};
// Add this to the model's Collection
model.Sides = new Collection<Side> { side };
return View(model);
}
[HttpPost]
public ActionResult Create(CreateCardViewModel viewModel)
{
if (ModelState.IsValid)
{
var set = _db.Sets.Single(s => s.SetId == viewModel.SetId);
var card = new Card {Sides = viewModel.Sides};
set.Cards.Add(card);
_db.Save();
}
return View(viewModel);
}
When I try to create a new card, the Sides property of the viewModel is null, so the ModelState is coming up as null. I can't quite figure out why that initial Side isn't getting passed with the model.
My View looks like this:
<h2>Create</h2>
#using (Html.BeginForm()) {
#Html.ValidationSummary(true)
<fieldset>
<legend>CreateCardViewModel</legend>
<div class="editor-label">
#Html.LabelFor(model => model.SetId)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.SetId)
#Html.ValidationMessageFor(model => model.SetId)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.DateCreated)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.DateCreated)
#Html.ValidationMessageFor(model => model.DateCreated)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.IsReady)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.IsReady)
#Html.ValidationMessageFor(model => model.IsReady)
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
There's nothing in your View that is bound to the Sides property of your ViewModel. Without anything in the form to hold the value of that property, it will be null when model binding occurs. You'll need to somehow capture the Side in your form - how are you adding to/removing from this property? Via user interaction that should take place on the form?
Try adding a second argument on Create() of type ICollection<Side> and see if that gets anything passed to it.

ViewModel IEnum<> property is returning null (not binding) when contained in a partial view?

I have a ViewModel that contains a Product type and an IEnumerable< Product > type. I have one main view that displays the ViewModel.Product at the top of the page but then I have a partial view that renders the ViewModel.IEnumerable< Product > data. On the post the first level product object comes back binded from the ViweModel whereas the ViewModel.IEnumerable< Product > is coming back null.
Of course if I remove the partial view and move the IEnumerable< Product > view to the main View the contents comes back binded fine. However, I need to put these Enumerable items in a partial view because I plan on updating the contents dynamically with Ajax.
Why is the IEnumerable< Prouduct> property not getting binded when it's placed in a partial view? Thx!
Models:
public class Product
{
public int ID { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
public class ProductIndexViewModel
{
public Product NewProduct { get; set; }
public List<Product> Products { get; set; }
}
public class BoringStoreContext
{
public BoringStoreContext()
{
Products = new List<Product>();
Products.Add(new Product() { ID = 1, Name = "Sure", Price = (decimal)(1.10) });
Products.Add(new Product() { ID = 2, Name = "Sure2", Price = (decimal)(2.10) });
}
public List<Product> Products { get; set; }
}
Views:
Main index.cshtml:
#model ViewModelBinding.Models.ProductIndexViewModel
#using (#Html.BeginForm())
{
<div>
#Html.LabelFor(model => model.NewProduct.Name)
#Html.EditorFor(model => model.NewProduct.Name)
</div>
<div>
#Html.LabelFor(model => model.NewProduct.Price)
#Html.EditorFor(model => model.NewProduct.Price)
</div>
#Html.Partial("_Product", Model.Products)
<div>
<input type="submit" value="Add Product" />
</div>
}
Parial View _Product.cshtml:
#model List<ViewModelBinding.Models.Product>
#for (int count = 0; count < Model.Count; count++)
{
<div>
#Html.LabelFor(model => model[count].ID)
#Html.EditorFor(model => model[count].ID)
</div>
<div>
#Html.LabelFor(model => model[count].Name)
#Html.EditorFor(model => model[count].Name)
</div>
<div>
#Html.LabelFor(model => model[count].Price)
#Html.EditorFor(model => model[count].Price)
</div>
}
Controller:
public class HomeController : Controller
{
public ActionResult Index()
{
BoringStoreContext db = new BoringStoreContext();
ProductIndexViewModel viewModel = new ProductIndexViewModel
{
NewProduct = new Product(),
Products = db.Products
};
return View(viewModel);
}
[HttpPost]
public ActionResult Index(ProductIndexViewModel viewModel)
{
// work with view model
return View();
}
}
When you use #Html.Partial("_Product", Model.Products) your input fields do not have correct names. For example instead of:
<input type="text" name="Products[0].ID" />
you get:
<input type="text" name="[0].ID" />
Just look at your generated markup and you will see the problem. This comes from the fact that when you use Html.Partial the navigational context is not preserved. The input fields names are not prefixed with the name of the collection - Products and as a consequence the model binder is not able to bind it correctly. Take a look at the following blog post to better understand the expected wire format.
I would recommend you using editor templates which preserve the context. So instead of:
#Html.Partial("_Product", Model.Products)
use:
#Html.EditorFor(x => x.Products)
and now move your _Product.cshtml template to ~/Views/Shared/EditorTemplates/Product.cshtml. Also since the editor template automatically recognizes that the Products property is an IEnumerable<T> it will render the template for each item of this collection. So your template should be strongly typed to a single Product and you can get rid of the loop:
#model Product
<div>
#Html.LabelFor(model => model.ID)
#Html.EditorFor(model => model.ID)
</div>
<div>
#Html.LabelFor(model => model.Name)
#Html.EditorFor(model => model.Name)
</div>
<div>
#Html.LabelFor(model => model.Price)
#Html.EditorFor(model => model.Price)
</div>
Now everything works by convention and it will properly bind.

Resources