Add to model in form, then redisplay form to add more - asp.net-mvc-3

I'm new to MVC3, but so far I have managed to get along with my code just great.
Now, I would like to make a simple form, that allows the user to input a text string, representing the name of an employee. I would then like this form to be submitted and stored in my model, in a sort of list. The form should then re-display, with a for-each loop writing out my already added names. When I'm done and moving on, I need to store this information to my database.
What I can't figure out, is how to store this temporary information, until i push it to my database. Pushing everytime I submit I can do, but this has cause me alot of headaches.
Hope you guys see what I'm trying to do, and have an awesome solution for it. :)
This is a simplified version of what I've been trying to do:
Model
public class OrderModel
{
public virtual ICollection<Employees> EmployeesList { get; set; }
public virtual Employees Employees { get; set; }
}
public class Employees
{
[Key]
public int ID { get; set; }
public string Name { get; set; }
}
View
#model OrderModel
#{
if (Model.EmployeesList != null)
{
foreach (var c in Model.EmployeesList)
{
#c.Name<br />
}
}
}
#using(Html.BeginForm())
{
#Html.TextBoxFor(m => m.Employees.Name)
<input type="submit" value="Add"/>
}
Controller
[HttpPost]
public ActionResult Index(OrderModel model)
{
model.EmployeesList.Add(model.Employees);
// This line gives me the error: "System.NullReferenceException: Object reference not set to an instance of an object."
return View(model);
}

I think you should handle this by burning the employee list into the page. Right now, you're not giving your form any way of recognizing the list.
In an EditorTemplates file named Employees:
#model Employees
#Html.HiddenFor(m => m.ID)
#Html.HiddenFor(m => m.Name);
In your view:
#using(Html.BeginForm())
{
#Html.EditorFor(m => m.EmployeesList)
#Html.TextBoxFor(m => m.Employees.Name)
<input type="submit" value="Add"/>
}
[HttpPost]
public ActionResult Index(OrderModel model)
{
if (model.EmployeesList == null)
model.EmployeesList = new List<Employees>();
model.EmployeesList.Add(model.Employees);
return View(model);
}
As an added bonus to this method, it would be easy to add ajax so the user never has to leave the page when they add new employees (You might be able to just insert a new hidden value with javascript and avoid ajax. It would depend on if you do anything other than add to your list in your post).

I think this would be a good use for TempData. You can store anything in there, kind of like the cache, but unlike the cache it only lasts until the next request. To implement this, change the action method like this (example only):
[HttpPost]
public ActionResult Index(OrderModel model)
{
dynamic existingItems = TempData["existing"];
if (existingItems != null)
{
foreach (Employee empl in existingItems)
model.EmployeesList.Add(empl );
}
model.EmployeesList.Add(model.Employees);
TempData["existing"] = model.EmployeesList;
return View(model);
}

Related

MVC How to pass a list of objects with List Items POST action method

I want to post a List of items to controller from Razor view , but i am getting a List of objects as null
My class structre is
Model:
List<Subjects> modelItem
class Subjects
{
int SubId{get;set;}
string Name{get;set;}
List<Students> StudentEntires{get;set;}
}
class StudentEntires
{
int StudId{get;set;}
string Name{get;set;}
int Mark{get;set;}
}
The model itself is a list of items and every items contain List of child items as well. Example model is a list of Subjects and every subject contains a List of Students, and i want to input mark for every student
My View is like
#model IList<Subjects>
#{
Layout = "~/Views/Shared/_Layout.cshtml";
}
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
if (Model.Count > 0)
{
#for (int item = 0; item < Model.Count(); item++)
{
<b>#Model[item].Name</b><br />
#foreach (StudentEntires markItem in Model[item].StudentEntires)
{
#Html.TextBoxFor(modelItem => markItem.Mark)
}
}
<p style="text-align:center">
<input type="submit" class="btn btn-primary" value="Update" />
</p>
}
}
And in controller
[HttpPost]
public ActionResult OptionalMarks(int Id,ICollection<Subjects> model)
{
//BUt my model is null. Any idea about this?
}
You're finding this difficult because you're not utilising the full power of the MVC framework, so allow me to provide a working example.
First up, let's create a view model to encapsulate your view's data requirements:
public class SubjectGradesViewModel
{
public SubjectGradesViewModel()
{
Subjects = new List<Subject>();
}
public List<Subject> Subjects { get; set; }
}
Next, create a class to represent your subject model:
public class Subject
{
public int Id { get; set; }
public string Name { get; set; }
public List<Student> StudentEntries { get; set; }
}
Finally, a class to represent a student:
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
public int Grade { get; set; }
}
At this point, you have all the classes you need to represent your data. Now let's create two controller actions, including some sample data so you can see how this works:
public ActionResult Index()
{
var model = new SubjectGradesViewModel();
// This sample data would normally be fetched
// from your database
var compsci = new Subject
{
Id = 1,
Name = "Computer Science",
StudentEntries = new List<Student>()
{
new Student { Id = 1, Name = "CompSci 1" },
new Student { Id = 2, Name = "CompSci 2" },
}
};
var maths = new Subject
{
Id = 2,
Name = "Mathematics",
StudentEntries = new List<Student>()
{
new Student { Id = 3, Name = "Maths 1" },
new Student { Id = 4, Name = "Maths 2" },
}
};
model.Subjects.Add(compsci);
model.Subjects.Add(maths);
return View(model);
}
[HttpPost]
public ActionResult Index(SubjectGradesViewModel model)
{
if (ModelState.IsValid)
{
return RedirectToAction("Success");
}
// There were validation errors
// so redisplay the form
return View(model);
}
Now it's time to construct the views, and this part is particularly important when it comes to sending data back to a controller. First up is the Index view:
#model SubjectGradesViewModel
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
#Html.EditorFor(m => m.Subjects) <br />
<input type="submit" />
}
You'll notice I'm simply using Html.EditorFor, whilst passing Subjects as the parameter. The reason I'm doing this is because we're going to create an EditorTemplate to represent a Subject. I'll explain more later on. For now, just know that EditorTemplates and DisplayTemplates are special folder names in MVC, so their names, and locations, are important.
We're actually going to create two templates: one for Subject and one for Student. To do that, follow these steps:
Create an EditorTemplates folder inside your view's current folder (e.g. if your view is Home\Index.cshtml, create the folder Home\EditorTemplates).
Create a strongly-typed view in that directory with the name that matches your model (i.e. in this case you would make two views, which would be called Subject.cshtml and Student.cshtml, respectively (again, the naming is important)).
Subject.cshtml should look like this:
#model Subject
<b>#Model.Name</b><br />
#Html.HiddenFor(m => m.Id)
#Html.HiddenFor(m => m.Name)
#Html.EditorFor(m => m.StudentEntries)
Student.cshtml should look like this:
#model Student
#Html.HiddenFor(m => m.Id)
#Html.HiddenFor(m => m.Name)
#Html.DisplayFor(m => m.Name): #Html.EditorFor(m => m.Grade)
<br />
That's it. If you now build and run this application, putting a breakpoint on the POST index action, you'll see the model is correctly populated.
So, what are EditorTemplates, and their counterparts, DisplayTemplates? They allow you to create reusable portions of views, allowing you to organise your views a little more.
The great thing about them is the templated helpers, that is Html.EditorFor and Html.DisplayFor, are smart enough to know when they're dealing with a template for a collection. That means you no longer have to loop over the items, manually invoking a template each time. You also don't have to perform any null or Count() checking, because the helpers will handle that all for you. You're left with views which are clean and free of logic.
EditorTemplates also generate appropriate names when you want to POST collections to a controller action. That makes model binding to a list much, much simpler than generating those names yourself. There are times where you'd still have to do that, but this is not one of them.
Change the action method signature to
public ActionResult OptionalMarks(ICollection<Subjects> model)
Since in your HTML, it does not look like there is anything named Id in there. This isn't your main issue though.
Next, do the following with the foor loop
#for(int idx = 0; idx < Model[item].StudentEntires.Count();idx++)
{
#Html.TextBoxFor(_ => Model[item].StudentEntries[idx])
}
Possibly due to the use of a foreach loop for the StudentEntries, the model binder is having trouble piecing everything together, and thus a NULL is returned.
EDIT:
Here's an example:
Controller
public class HomeController : Controller
{
public ActionResult Index()
{
var viewModel = new IndexViewModel();
var subjects = new List<Subject>();
var subject1 = new Subject();
subject1.Name = "History";
subject1.StudentEntires.Add(new Student { Mark = 50 });
subjects.Add(subject1);
viewModel.Subjects = subjects;
return View(viewModel);
}
[HttpPost]
public ActionResult Index(IndexViewModel viewModel)
{
return new EmptyResult();
}
}
View
#model SOWorkbench.Controllers.IndexViewModel
#{
ViewBag.Title = "Home Page";
}
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
if (Model.Subjects.Any())
{
int subjectsCount = Model.Subjects.Count();
for (int item = 0; item < subjectsCount; item++)
{
<b>#Model.Subjects[item].Name</b><br />
int studentEntriesCount = Model.Subjects[item].StudentEntires.Count();
for(int idx = 0;idx < studentEntriesCount;idx++)
{
#Html.TextBoxFor(_ => Model.Subjects[item].StudentEntires[idx].Mark);
}
}
<p style="text-align:center">
<input type="submit" class="btn btn-primary" value="Update" />
</p>
}
}
When you post the form, you should see the data come back in the viewModel object.

context.SaveChanges() works, but database is never updated? (MVC 3)

I've written a form in ASP.NET MVC3, and I can't get the entry to save the changes I make in the database, but while debugging, I noticed that the changes were reflected in the data context. I am experiencing no errors running this code. Let me know if you need more. Thanks!
Controller
[HttpPost]
public ActionResult Edit(Tool tool, FormCollection collection)
{
if (collection["Tool.Person.PersonID"] != "")
{
tool.Person= context.People.Find(
Convert.ToInt32(collection["Tool.Person.PersonID"])
);
}
if (collection["Tool.Company.CompanyID"] != "")
{
tool.Company = context.Companies.Find(
Convert.ToInt32(collection["Tool.Company.CompanyID"])
);
}
if (ModelState.IsValid)
{
context.Entry(tool).State = EntityState.Modified;
context.SaveChanges();
return RedirectToAction("Index");
}
return View(tool);
}
The first two if statements are checking to see if the user inputted a person or company, and the information is passed via the FormCollection. PersonID and CompanyID are primary keys for Person and Company, respectively. I went through the method line by line multiple times and achieve the same result - after context.SaveChanges();, the context reflects the changes, but the database entries remain null for both Person_PersonID and Company_CompanyID.
Try using a view model and accessing the database after the user submits the form.
This should get you well on your way.
ViewModel
using System.ComponentModel.DataAnnotations;
namespace Project.ViewModels
{
public class _tools
{
[Required(ErrorMessage="ToolID is required")]
public int32 ToolID{ get; set; } //whatever ID you use to retrieve the Tool from the database.
[Required(ErrorMessage="PersonID is required")]
public int32 PersonID{ get; set; }
[Required(ErrorMessage="CompanyID is required")]
public int32 CompanyID{ get; set; }
}
}
Controller Post
[HttpPost]
public ActionResult Edit(_tool viewModel)
{
if (ModelState.IsValid)
{
Tool tool = db.GetTool(viewModel.ToolID) //whatever method you use to get a current version of the row. You already do this before you send the data to the client, so just copy that code
tool.Person = viewModel.PersonID
tool.Company = viewModel.CompanyID
context.Entry(tool).State = EntityState.Modified;
context.SaveChanges();
return RedirectToAction("Index");
}
return View(tool);
}
View
#model = _tool
#using(Html.BeginForm("Edit", "ControllerNameHere", FormMethod.Post, null))
{
#Html.HiddenFor(model => model.ToolID)
#*Also add whatever inputs you use to get PersonID and CompanyID from the user.
Make sure to either use the #Html helpers or to give them names that correspond.
i.e. name one input PersonID and the other CompanyID*#
<input type="submit" value="Edit">
}

ASP.NET MVC 3 (Razor) form submit not working

Using Fiddler I can see that the request is not even being made but I can't see why.
Here's the form:
#using (Html.BeginForm("Index", "FileSystemChannelIndex", FormMethod.Post, new {
channelId = #Model.ChannelId }))
{
#Html.HiddenFor(model => model.Id)
#Html.HiddenFor(model => model.ChannelId)
<div class="editor-label">
Select File Source
</div>
<div class="editor-field">
#Html.DropDownListFor(
model => model.SelectedFileSourceValue,
new SelectList(Model.AvailableFilesSources, "Id", "Name"),
new { id = "selectFileSource" })
</div>
<p>
<input class="t-button" type="submit" value="Save" />
</p>
}
The View originally came from:
public ViewResult Create(int channelId)
{
var channel = this.fullUOW.GetFileSystemChannelRepository().All.Where(c => c.Id == channelId);
var vm = new FileSystemChannelIndexViewModel(channelId, new FileSystemChannelIndex());
return View("Edit", vm);
}
I've tried adding the "name" attribute to the but that didn't make any difference.
Any ideas?
EDIT: More info for Jim et al...
Domain:
public class FileSystemChannel
{
public int Id {get; set; }
public ICollection<FileSystemChannelIndex> ChannelIndexes { get; set; }
}
public class FileSystemChannelIndex
{
public int Id { get; set; }
public FileSystemChannel ParentChannel { get; set; }
}
Due to a 0...* association, in the UI we have to create a FileSystemChannel first then add a FileSystemChannelIndex to it. So that's why I pass in the channelId to the FileSystemChannelIndex Create View. When submitting the new FileSystemChannelIndex the following action should be called:
[HttpPost]
public ActionResult Index(int channelId, FileSystemChannelIndexViewModel vm)
{
//TODO: get the Channel, add the Index, save to db
return View("Index");
}
So thanks to Mark's comment it's due to a Select failing client side validation. Using IE dev tools to inspect the element:
<select name="SelectedFileSourceValue" class="input-validation-error" id="selectFileSource" data-val-required="The SelectedFileSourceValue field is required." data-val-number="The field SelectedFileSourceValue must be a number." data-val="true">
empo,
further to my comment above:
empo - can you post both public ActionResult Create(////) methods (i.e. HttpPost and HttpGet) into the question as this could highlight if the issue is related to ambiguous method signatures, which i suspect could well be the case as you are posting back the same signature as the HttpGet actionresult
try adding the appropriate HttpPost actionresult along the lines of:
[HttpPost]
public ActionResult Create(FileSystemChannelIndex domainModel)
{
if (!ModelState.IsValid)
{
return View(PopulateEditViewModel(domainModel));
}
_serviceTasks.Insert(domainModel);
_serviceTasks.SaveChanges();
return this.RedirectToAction("Edit", new {id = domainModel.ChannelId});
}
your original HttpGet (which feels 'wierd' to me):
[HttpGet]
public ViewResult Create(int channelId) {
var channel = this.fullUOW.GetFileSystemChannelRepository().All
.Where(c => c.Id == channelId);
var vm = new FileSystemChannelIndexViewModel(channelId,
new FileSystemChannelIndex());
return View("Edit", vm);
}
and inside your Edit actionresult, you'd grab the entity based on the passed in id. might work, might not. not sure without a fuller picture of your domain and logic.
obviously, your own plumbing will vary, but this should give an idea of what should be expected.
How can you have Model.Id when you are creating something? Maybe Model.Id is null and because you cannot post

To show Create and detail view in one view using mvc3

Hi i want to show a page where i'm allowing user to create a new record and show the other related records of same table below .....
i need to add data in Hobbydetail class:
public class HobbyDetail
{
public virtual HobbyMasters Hobbymaster { get; set; }
public virtual Course course { get; set; }
public virtual StudyMedium StudyMedium { get; set; }
public virtual decimal Fees { get; set; }
}
I want my view "Create" to let the user create a new record and to also to show existing record below it...
I Dont want to use a viewmodel...
Can sumbody help me
Thanx in advance
One way to accomplish this is: In your controller, create a child action that renders the list, then render that action in your "Create" view using Html.RenderAction (also see this). I have included some code below (I have not tested this, but it should give you the basic idea). Please note this is not the only way to accomplish this - You could use a partial view see this. Please also understand the difference between html.RenderAction and html.Action, see this.
//In HobbyDetail Controller
[HTTPGet]
public ActionResult Create()
{
var model = new HobbyDetail ();
return View(model);
}
[HTTPPost]
public ActionResult Create(HobbyDetail model)
{
if(ModelState.isValid)
{
//logic to persist model
}
else
{
//logic when validation fails...
}
}
[ChildActionOnly]
public ActionResult ListAll()
{
List<Hobbydetail> model = //query to DB, or Data store to get Hobbydetails
return View(model);
}
//View for ListAll
#model List<HobbyDetail>
{
Layout = null; //No layout here...
}
<ul>
#foreach(var h in Model)
{
<li>#h.HobbyMasters.Name</li> //for example...
}
</ul>
//View for Create
#model HobbyDetail
...
#{html.renderAction("ListAll");}

MVC3 Only posted form values retained?

I am using strongly typed views in an MVC3 web app. I've noticed that when a form is submitted, the ViewModel that is passed to the controller only has values for properties that have form elements associated with them. For instance, the example below shows a simple confirmation View with a checkbox and a phone number that the user must confirm before proceeding. When the form is submitted to the controller action, the UserConfirmed property contains a value, but the PhoneNumber property is null.
Is there any way for the ViewModel to retain all of its values or do I have to repopulate the ViewModel properties that do not have form elements associated with them?
The View
#model WebMeterReplacement.ViewModels.Appointment.ScheduleConfirmationViewModel
#using (Html.BeginForm()) {
#Html.ValidationSummary(false)
#Html.CheckBoxFor(model => model.UserConfirmed)
<span>Please confirm before proceeding</span>
<div>
Phone Number: #Model.PhoneNumber
</div>
<input type="submit" value="Confirm"/>
The Controller
[HttpPost]
public ActionResult ScheduleConfirmation(ScheduleConfirmationViewModel model)
{
if (model.UserConfirmed)
{
// add ViewModel data to repository
}
else
{
ModelState.AddModelError("ERROR", WebResources.strERROR_ConfirmSchedule);
}
return View(model);
}
Since your writing the phonenumber as output to the page it won't be automatically posted back (you've found out that part) What you can do is populate an hidden or read-only field with the phonenumber so that it will be posted back to your controller. An second option is to make a new call to your datasource and repopulate your object before saving it back to your datasource.
I generally POST back information like this in a hidden input. I personally use this heavily to pass data needed to return the user exactly where they where before pressing edit.
In your case it's as simple as
#model WebMeterReplacement.ViewModels.Appointment.ScheduleConfirmationViewModel
#using (Html.BeginForm()) {
#Html.ValidationSummary(false)
#Html.CheckBoxFor(model => model.UserConfirmed)
<span>Please confirm before proceeding</span>
<div>
#Html.HiddenFor(m => m.PhoneNumber)
Phone Number: #Model.PhoneNumber
</div>
<input type="submit" value="Confirm"/>
For future reference:
If your passing complex objects back you need one hidden field per attribute (Hiddenfor does NOT iterate)
View
WRONG
#Html.HiddenFor(m => m.PagingData)
RIGHT
#Html.HiddenFor(m => m.PagingData.Count)
#Html.HiddenFor(m => m.PagingData.Skip)
#Html.HiddenFor(m => m.PagingData.PageSize)
Action
public HomeController(AViewModel Model)
{
PagingData PagingData = Model.PagingData;
Skip = PagingData.Skip;
}
If your passing Arrays you can do it like this
View
#if (Model.HiddenFields != null)
{
foreach (string HiddenField in Model.HiddenFields)
{
#Html.Hidden("HiddenFields", HiddenField)
}
}
Action
public HomeController(AViewModel Model)
{
String[] HiddenFields = Model.HiddenFields;
}
Well, the form will only POST elements that you have created. As you found out, simply writing the phone number out to the page will not suffice. The model binder can only bind those properties which exist in the posted data.
Generally you have a couple of options here:
1) You can create Input elements for all of the properties in your model, using visible elements (like a textbox) for those properties you want to edit, and hidden elements which should be posted back but have no UI
2) Post back a partial representation of your model (as you are doing now), read the entity back in from it's data source (I assume you're using some kind of data source, EF maybe) and then alter the properties of that entity with the ones from your form.
Both scenarios are common but it really depends on the complexity of your model.
I know this thread is a bit old, but thought I'd resurrect it to get feed back on my solution to this.
I'm in a similar situation where my objects are passed to a view, and the view may only display part of that object for editing. Obviously, when the controller receives the model back from the default model binder, and values not posted back become null.. and saving this means that a DB value becomes null just because it wasn't displayed/returned from a view.
I didn't like the idea of creating a model for each view. I know it's probably the right way... but I was looking for a reusable pattern that can be implemented fairly quickly.
See the "MergeWith" method... as this would be used to take a copy of the object from the database and merge it with the one returned from the view (posted back)
namespace SIP.Models
{
[Table("agents")]
public class Agent
{
[Key]
public int id { get; set; }
[Searchable]
[DisplayName("Name")]
[Column("name")]
[Required]
[StringLength(50, MinimumLength = 4)]
public string AgentName { get; set; }
[Searchable]
[DisplayName("Address")]
[Column("address")]
[DataType(DataType.MultilineText)]
public string Address { get; set; }
[DisplayName("Region")]
[Searchable]
[Column("region")]
[StringLength(50, MinimumLength = 3)]
public string Region { get; set; }
[DisplayName("Phone")]
[Column("phone")]
[StringLength(50, MinimumLength = 4)]
public string Phone { get; set; }
[DisplayName("Fax")]
[Column("fax")]
[StringLength(50, MinimumLength = 4)]
public string Fax { get; set; }
[DisplayName("Email")]
[RegularExpression(#"(\S)+", ErrorMessage = "White space is not allowed")]
[Column("email")]
[StringLength(50, MinimumLength = 4)]
public string Email { get; set; }
[DisplayName("Notes")]
[Column("notes")]
[DataType(DataType.MultilineText)]
public string Notes{ get; set; }
[DisplayName("Active")]
[Column("active")]
public bool Active { get; set; }
public override string ToString()
{
return AgentName;
}
public bool MergeWith(Agent a, string[] fields)
{
try
{
foreach (PropertyInfo pi in this.GetType().GetProperties())
{
foreach (string f in fields)
{
if (pi.Name == f && pi.Name.ToLower() != "id")
{
var newVal = a.GetType().GetProperty(f).GetValue(a,null);
pi.SetValue(this, newVal, null);
}
}
}
}
catch (Exception ex)
{
return false;
//todo: Log output to file...
}
return true;
}
}
}
And to use this in the controller.. you'd have something like..
[HttpPost]
public ActionResult Edit(Agent agent)
{
if (ModelState.IsValid)
{
Agent ag = db.Agents.Where(a => a.id == agent.id).ToList<Agent>().First<Agent>();
ag.MergeWith(agent, Request.Params.AllKeys);
db.Entry(ag).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(agent);
}
This way, during post back, it takes the object from the database, and updates it with object from view... but only updates the values that were posted back.. So if you have a field like "address" or something that doesn't appear in the view.. it doesn't get touched during the update.
I've tested this so far and i works for my purposes, tho i welcome any feedback as I'm keen to see how others have overcome this situation. It's a first version and i'm sure it can be implemented better like through an extension method or something.. but for now the MergeWith can be copy/pasted to each model object.
Yes, Just place hidden fields in the form for those values which you are not using and want to return to server control.
Thanks

Resources