Better way of creating repeating HTML section - asp.net-mvc

I've asked this once before but without any code to look at, here I have an implementation and I'm wondering if there is a better way to accomplish this.
I want a repeating html section like so:
<div>
<input id=id1 name=id1 type=text/>
</div>
<div>
<input id=id2 name=id2 type=text/>
</div
etc
This could contain any number of input boxes which map to the List of 'something' classes I have in the model, I presently do this with a View
#using (Html.BeginForm())
{
for (int i = 0; i < Model.Somethings.Count; i++)
{
Model.Index = i;
#Html.Action("Index", "HtmlSection", Model);
}
// other stuff
}
and a partial view
#{
int index = Model.Index;
}
#Html.TextBoxFor(x => x.Somethings[index].TheProperty)
Where the model looks like this
public class HtmlSectionModel
{
public List<Something> Somethings { get; set; }
public int Index { get; set; }
}
Finally the action looks like this
public ActionResult Index(HtmlSectionModel model)
{
// do stuff
}
To me this works but isn't ideal
The partial view can now only be used within this context, it uses the top level model rather than just the 'Something' class
I have to pass an index in the model in order to get unique name's for binding, if I didn't do this then textbox would have the same name/id
This seems to me to be a common pattern so others must have solved it in other ways?
I guess what I'm after here is the MVC equivalent of Asp.Net UserControls/Webcontrols (which seem to be child actions/partial views), but, combined with model binding which seems to require unique names

What I wanted can be accomplished with editor templates
Controller
public class UsesEditorController : Controller
{
[HttpGet]
public ActionResult Index()
{
return View(new SomeModel());
}
[HttpPost]
public ActionResult Index(SomeModel model)
{
return View(model);
}
}
Model
public class Blob
{
public string Name { get; set; }
public string Address { get; set; }
public Blob()
{
Name = string.Empty;
Address = string.Empty;
}
}
public class SomeModel
{
public List<Blob> Blobs { get; set; }
public SomeModel()
{
int count = 5;
this.Blobs = new List<Blob>(count);
for (int i = 0; i < count; i++)
{
this.Blobs.Add(new Blob());
}
}
}
View
#model MyProject.Areas.EditorTemplates.Models.SomeModel
#using (Html.BeginForm())
{
for (int i = 0; i < Model.Blobs.Count; i++)
{
#Html.EditorFor(m => m.Blobs[i], "CustomEditorForBlob");
}
<input type="submit" value="Send data back" />
}
And Editor, which can be anywhere in the view folder as I'm referring to it directly
#model MyProject.Areas.EditorTemplates.Models.Blob
#Html.TextBoxFor(m => m.Name)
#Html.TextBoxFor(m => m.Address)
This renders with ids like:
<input class="k-textbox" id="Blobs_1__Name" name="Blobs[1].Name" ...
So this gives me
List item
a repeating structure, just like UserControls in Asp.Net
The editor template only refers to the Blob class, it has no knowledge of the SomeModel class
Binding works (tested it)

It looks to me like what you are trying to accomplish is unique IDs for your inputs, and you certainly don't need a partial to do this. You can output your text box inside your for loop like the following:
#Html.TextBoxFor(x => x.Somethings[i].TheProperty)
This will generate a unique id something like id="Somethings_1_TheProperty". If you don't like that id, you can certainly make your own with something like this:
#Html.TextBoxFor(x => x.Somethings[i].TheProperty, new {id="id" + (i+1)})

Related

MVC Model Binding EF 6 Missing Parent Reference

I have an object with a collection. I am passing the model to the view and upon post the collection is there, however the reference (containing object Id) is not there and the navigation property is null.
Models
public class Order
{
public int OrderId { get; set;}
public List<OrderItem> OrderItems { get; set;}
}
public class OrderItem
{
public int OrderItemId { get; set; }
public int OrderId { get; set; }
public int SomeOtherValue { get; set; }
}
View:
#model Order
<h2>Edit Form</h2>
#using (Html.BeginForm())
{
#Html.HiddenFor(m => m.OrderId)
//Some other form fields for the order...
#for (int i = 0; i < Model.OrderItems.Count; i++)
{
#Html.TextBoxFor(x => Model.OrderItems[i].SomeOtherValue)
}
<input type="submit" value="Submit"/>
}
When I submit the form, the model gets bound correctly and the while debugging I can see the child OrderItems populated with updated information. However, the OrderId on each OrderItem is set to 0.
Controller
public ActionResult Edit(Order myOrder)
{
myOrder.OrderItems.Count(); // These are populated and the edited values are there.
myOrder.OrderItem[0].Order; // this is null (all order items)
myOrder.OrderItem[0].OrderId; // this is set to 0 (all order items)
//...more stuff and return view.
}
What is the best way to get around this problem? Is there some way to ensure that each OrderItem has a reference to the order when posting the form? I'm hoping I can avoid having to manually account for association in the controller when trying to save the object.
You should add hidden inputs for those fields. Check the code bolow:
..
#for (int i = 0; i < Model.OrderItems.Count; i++)
{
#Html.HiddenFor(x => Model.OrderItems[i].OrderId)
#Html.HiddenFor(x => Model.OrderItems[i].Order)
#Html.TextBoxFor(x => Model.OrderItems[i].SomeOtherValue)
}
..

MVC 3 using #model twice

I would like to using two times #model to get data from another part of my website is it possible? Because now I have got error but if I have only this first #model everything working correct.
Look -> MVC 3 - Exception Details: System.InvalidOperationException
Error 2 'SportsStore.Entities.Kategorie' does not contain a definition for 'Opis' and no extension method 'Opis' accepting a first argument of type 'SportsStore.Entities.Kategorie' could be found (are you missing a using directive or an assembly reference?) c:\Users\Rafal\Desktop\MVC ksiązka\moj projekt\sklep\SportsStore.WebUI\Views\Product\List.cshtml 16 4 SportsStore.WebUI
#model IEnumerable<SportsStore.Entities.Towar>
#model IEnumerable<SportsStore.Entities.Kategorie>
#{
ViewBag.Title = "List";
}
<h2>List</h2>
#foreach (var p in Model)
{
<div class="item">
<h3>#p.Nazwa</h3>
#p.Opis
<h4>#p.Cena.ToString("c")</h4>
</div>
}
You only can have one Model per View. But you can use another object to declarate the model:
public class SomeViewModel
{
public IEnumerable<Towar> Towars;
public IEnumerable<Category> Categories;
public SomeViewModel(IEnumerable<Towar> towars, IEnumerable<Category> categories) {
Towars = towars;
Categories = categories;
}
}
And then use it in your view like this:
#model SportsStore.Entities.SomeViewModel
#foreach (var item in Model.Towars)
{
<div class="item">
<h3>#p.Nazwa</h3>
#p.Opis
<h4>#p.Cena.ToString("c")</h4>
</div>
}
#foreach (var item in Model.Categories) {
#item.Name #* or what you need down here *#
}
I would also recommend you to use english names in MVC. It's more clear to read and understand ;).
I think this would be a case to create a ViewModel (to combine the two entities you have) and then base a View off that ViewModel.
It is best to create a view model to represent your data. Your view model must only contain what you need to display on your view. Anything that is not used you can remove, no point of it being there. Your view model can look like this:
using SportsStore.Entities;
public class YourViewModel
{
public IEnumerable<Towar> Towars { get; set; }
public IEnumerable<Kategorie> Categories { get; set; } // I assume this is categories
}
Lets say that you have to use this view model in a create view then your create action can look something like this:
public class YourController : Controller
{
private readonly ITowarRepository towarRepository;
private readonly ICategoryRepository categoryRepository;
public YourController(ITowarRepository towarRepository, ICategoryRepository categoryRepository)
{
this.towarRepository = towarRepository;
this.categoryRepository = categoryRepository;
}
public ActionResult Create()
{
YourViewModel viewModel = new YourViewModel
{
Towars = towarRepository.GetAll(),
Categories = categoryRepository.GetAll()
};
return View(viewModel);
}
}
And then in your view:
#model YourProject.DomainModel.ViewModels.YourViewModel
#foreach (var towar in Model.Towars)
{
// Do whatever
}
#foreach (var category in Model.Categories)
{
// Do whatever
}

My view won't bind with my model in MVC

I have the following viewmodel:
public class CityViewModel
{
public CityViewModel() {
CityDetails = Enumerable.Range(1,2).Select(x => new CityDetail()).ToList();
}
public City City { get; set; }
public IList<CityDetail> CityDetails { get; set; }
public class CityDetail
{
public CityDetail() {
Correct = true;
}
public bool Correct { get; set; }
}
In my view I have the following:
#foreach (int index in Enumerable.Range(0, Model.CityDetails.Count()))
{
<input type="hidden" name="CityDetails[#index]_Correct)" value="false" />
<input type="checkbox" id="CityValid" name="CityDetails[#index]_Correct" value='#Model.CityDetails[index].Correct' />
I can't get any binding from the view to the model for the field named "Correct" so I am wondering about the naming that I used. Does anyone have an idea what might be wrong?
The easy and best way to map collection is using the indexer. Use the following code snippet
#for (int i = 0; i < Model.CityDetails.Count; i++)
{
#Html.EditorFor(x => Model.CityDetails[i].Correct)
}
There's no need for your view to look like that. Create an editor template, and then use Html.EditorFor(x => x.CityDetails). That will handle enumerating the collection and properly writing out the elements so that they map back into the list.

referencing a list<Model> in a looping ParialView

I have been trying to loop partial views, which share the same model as the main view. Basically the idea is that there is a registration page where a parent can register multiple children, the children info is in partial view so that i can add multiple instances of it on the main view.
I can get the partial view to display multiple times but it only saves the first student. if i replace the partial view (#Html.Partial...) with #Html.EditorFor(f => f.student[i].Person.FirstName) in the main view then it works fine and i can add multiple textboxes and save multiple students
how can i use the partial views and be able to pass in the ParentModel and correctly reference it?
hope all this makes sense... any help is appreciated. thanks!
Model:
public partial class Person()
{
public string FirstName { get; set; }
}
public partial class Student
{
public int Student_PersonID { get; set; }
public int Father_PersonID { get; set; }
public virtual Person Father { get; set; }
public virtual Person Person { get; set; }
}
public class ParentModel
{
public List<Student> student { get; set; }
}
Main View
#model WebPortal.Models.ParentModel
<div>
#for (int i = 0; i < cnt; i++)
{
#Html.Partial("_StudentPartial", Model, new ViewDataDictionary { { "loopIndex", i.ToString() } });
}
</div>
<div>
#Html.EditorFor(f => f.student[0].Father.FirstName)
</div>
Partial View (_StudentPartial)
#model WebPortal.Models.ParentModel
#{
int i = Convert.ToInt32(ViewData["loopIndex"].ToString());
}
#using (Html.BeginForm())
{
<div class="editor-field">
#Html.EditorFor(m => m.student[i].Person.FirstName)
</div>
}
Controller
public ActionResult Register(ParentModel pm)
{
using (var db = new SMEntities())
{
for (int i = 0; i < pm.student.Count; i++)
{
if (i != 0)
{
pm.student[i].Father = pm.student[0].Father;
}
db.Students.Add(pm.student[i]);
}
db.SaveChanges();
}
}
A better way to structure things will be like this :
Type the _StudentPartial View to Student like this -
#model WebPortal.Models.Student
#using (Html.BeginForm())
{
<div class="editor-field">
#Html.EditorFor(m => m.Person.FirstName)
</div>
}
Then model your main view like this :
#model WebPortal.Models.ParentModel
<div>
#for (int i = 0; i < cnt; i++)
{
#Html.Partial("_StudentPartial", Model.student[i]);
}
</div>
<div>
#Html.EditorFor(f => f.student[0].Father.FirstName)
</div>
Try to use this as a guideline : Give to the view only the information it needs, not more.

Validating MVC Data Collections

I have a simple Question data model:
public class Question {
int QuestionId { get; set; }
string Text { get; set; }
string Answer { get; set; }
string ValidationMessage { get; set; }
};
Using this class I have built a view Model:
public class QuestionViewModel {
string Introduction { get; set; }
IEnumerable<Question> Questions { get; set; }
};
My Controller the builds the view model (from a data source) and renders the view:
#model QuestionViewModel
#using (Html.BeginForm()) {
if (Model.Questions != null) {
<ol>
#Html.EditorFor(m => Model.Questions)
</ol>
}
#Html.ValidationSummary("Unable to process answers...")
<input type="submit" value="submit" />
}
This view utilises an EditorTemplate:
#model Question
<li>
#Html.HiddenFor(m => m.Questionid)
#Html.TextBoxFor(m => m.Answer)
#Html.ValidationMessageFor(m => m.Answer)
</li>
For now, when the page is posted back, the controller validates the response:
[HttpPost]
public ActionResult Response(QuestionViewModel model) {
if (ModelState.IsValid) {
for (int i = 0; i < model.Questions.Count(); i++) {
Question q = model.Questions[i];
string questionId = String.Format("Questions[{0}]", i);
if (String.IsNullOrWhiteSpace(q.Answer)) {
ModelState.AddModelError(questionId, q.ValidationMessage);
}
}
}
}
The problem I'm having is that most of this works fine - the validates and the Validation Summary shows the correct validation messages. The problem is that I can't get individual field validators to render the error:
<span class="field-validation-valid" data-valmsg-replace="true" data-valmsg-for="Questions[0].StringValue"></span>
As you can see, when I call the ModelState.AddModelError() method, I am currently using key value of the format "Questions[0]", but I have also tried "Questions_0" and various other combinations.
Any help/guidance would be much appreciated.
[Apologies for the overly long post]
I have found the answer - as with so many things, it was obvious once I broke the problem down - the ModelState.AddModelError() just needed a fully qualified key!
Modify the HttpPost Controller as follows:
[HttpPost]
public ActionResult Response(QuestionViewModel model) {
if (ModelState.IsValid) {
for (int i = 0; i < model.Questions.Count(); i++) {
Question q = model.Questions[i];
/*
** The key must specify a fully qualified element name including
** the name of the property value, e.g.
** "Questions[0].Answer"
*/
string questionId = String.Format("Questions[{0}].Answer", i);
if (String.IsNullOrWhiteSpace(q.Answer)) {
ModelState.AddModelError(questionId, q.ValidationMessage);
}
}
}
}

Resources