Multiple Actions per View consisting of partial views bound to models - asp.net-mvc

Extension to: How do you handle multiple submit buttons in ASP.NET MVC Framework?
Let us say a view is composed of partial views bound with related models, let's say a student is required to provide multiple contact persons(partial view bound to Person model) and multiple contact numbers(partial view bound to a model) to get registered, sorry for the bad example. Once a contact person or number is added, an action (child postback or whatever) is called which validates the related model (not student model), adds it in a list and returns to the same page for further processing. Once all are added, the parent/master action validates whole student model and processes it.
How to validate the specific model for which an action is being called, add it to the page and return the same student view with added values in response?

This solution uses #2 (Session) since its simpler to code however this demonstrates the principles.
Views
Index View:
#using StackOverflow.Models
<div>
#{ Html.RenderPartial("PersonGrid", Model.Persons, new ViewDataDictionary()); }
#Html.Partial("NewPerson", new Person())
#{ Html.RenderPartial("ContactGrid", Model.Contacts, new ViewDataDictionary()); }
#Html.Partial("NewContact", new Contact())
#using(Html.BeginForm("Validate", "Home", FormMethod.Post))
{
<input type="submit" value="Validate" />
}
</div>
Person Grid
#model IList
<table>
<thead>
<tr>
<td>First Name</td>
<td>Last Name</td>
</tr>
</thead>
<tbody>
#if (Model != null && Model.Any())
{
foreach (var person in Model)
{
<tr>
<td>#person.FirstName</td>
<td>#person.LastName</td>
</tr>
}
}
else
{
<tr>
<td colspan="2" style="text-align: center">No persons available</td>
</tr>
}
</tbody>
</table>
Contact Grid
#model IList
<table>
<thead>
<tr>
<td>Phone</td>
</tr>
</thead>
<tbody>
#if (Model != null && Model.Any())
{
foreach (var contact in Model)
{
<tr>
<td>#contact.Phone</td>
</tr>
}
}
else
{
<tr>
<td>No contacts available</td>
</tr>
}
</tbody>
</table>
New Person
#model StackOverflow.Models.Person
#using (Html.BeginForm("NewPerson", "Home", FormMethod.Post))
{
<div>
#Html.Hidden("PersonViewState", TempData["PersonViewState"])
#Html.LabelFor(m => m.FirstName)<br />
#Html.TextBoxFor(m => m.FirstName)<br />
<br />
#Html.LabelFor(m => m.LastName)<br />
#Html.TextBoxFor(m => m.LastName)<br />
<br />
<input type="submit" value="Submit" />
</div>
}
New Contact
#model StackOverflow.Models.Contact
#using (Html.BeginForm("NewContact", "Home", FormMethod.Post))
{
<div>
#Html.LabelFor(m => m.Phone)<br />
#Html.TextBoxFor(m => m.Phone)<br />
<br />
<input type="submit" value="Submit" />
</div>
}
Models
public class Person
{
[Display(Name = "First Name")]
public string FirstName { get; set; }
[Display(Name = "Last Name")]
public string LastName { get; set; }
}
public class Contact
{
[Display(Name = "Phone")]
public string Phone { get; set; }
}
public class HomeModel
{
public IList<Person> Persons { get; set; }
public IList<Contact> Contacts { get; set; }
}
Helpers
public static class PersistenceMechanism
{
public static IList GetPersons()
{
return (IList<Person>) HttpContext.Current.Session["__Persons"];
}
public static IList GetContacts()
{
return (IList<Contact>) HttpContext.Current.Session["__Contacts"];
}
public static void Update(IList<Person> persons)
{
HttpContext.Current.Session["__Persons"] = persons;
}
public static void Update(IList<Contact> contacts)
{
HttpContext.Current.Session["__Contacts"] = contacts;
}
}
Controller
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new HomeModel
{
Persons = PersistenceMechanism.GetPersons(),
Contacts = PersistenceMechanism.GetContacts()
};
return View(model);
}
[HttpGet]
public ActionResult PersonGrid()
{
var persons = PersistenceMechanism.GetPersons();
return PartialView(persons);
}
[HttpGet]
public ActionResult ContactGrid()
{
var contacts = PersistenceMechanism.GetContacts();
return PartialView(contacts);
}
[HttpPost]
public ActionResult NewPerson(Person model)
{
var persons = PersistenceMechanism.GetPersons() ?? new List<Person>();
persons.Add(model);
PersistenceMechanism.Update(persons);
return RedirectToAction("Index");
}
[HttpPost]
public ActionResult NewContact(Contact model)
{
var contacts = PersistenceMechanism.GetContacts() ?? new List<Contact>();
contacts.Add(model);
PersistenceMechanism.Update(contacts);
return RedirectToAction("Index");
}
[HttpPost]
public ActionResult Validate()
{
var persons = PersistenceMechanism.GetPersons();
var contacts = PersistenceMechanism.GetContacts();
// validate
// ...
return RedirectToAction("Index");
}
}

To repeat the question to ensure that I have the idea of what you are asking.
Your page page is build from two partial views with different models. Both of these partial views contain a form which will on submit build a grid of records on the UI. The main page will have a further validate button which will then validate the entire contents of both grids on postback?
In this situation I will like to have two forms where the submit event is powered by Ajax. Either jQuery / Microsoft Ajax. Both forms will submit to two separate actions which will accept their respective models, Person and Contact. Each submit button will return its respective partial view which will be a grid showing the collection of items that have been submitted so far. The partial view returned will update a specified target (e.g. div) since we are using AJAX.
Of course it will be necessary to remember the previous items that were submitted so far in order reconstruct the grid with the new item added. This will mean some soft of persistent storage is required. Some options available are:
Database
Session
Hidden form field(s) (preferred). It is possible to use the array model binding mechanism to support this or a simple serialized object (or list of objects) in a single hidden field.
Validation now becomes simple as the model are available server side through your persistent storage mechanism on postback.

Related

Why list of checkbox selections always posted as null in ASP.NET MVC-5 [duplicate]

This question already has answers here:
Post an HTML Table to ADO.NET DataTable
(2 answers)
Closed 5 years ago.
I am new to ASP .NET MVC. My problem is - I want to 'POST' a collection of the items , so that controller can process it.
My model is collection of -
public class CheckedRentalProperty
{
public bool IsSelected { get; set; }
public int Id { get; set; }
public String Address { get; set; }
}
My controller is defined like this -
public class RentalPropertiesController : Controller
{
public ActionResult Index()
{
List<CheckedRentalProperty> checkHsList = new List<CheckedRentalProperty>();
// Fill the list
return View(checkHsList);
}
[HttpPost]
public ActionResult Save(IEnumerable<CheckedRentalProperty> checkHsList)
{
// why checkHsList is coming as null ??
}
}
And the view is like this -
#model IEnumerable<XXX.Models.CheckedRentalProperty>
#using (Html.BeginForm("Save", "RentalProperties", FormMethod.Post))
{
<div class="form-horizontal">
<div class="form-group">
<table class="table">
<tr>
<th>
</th>
<th>
#Html.DisplayNameFor(model => model.Address)
</th>
</tr>
#foreach (var item in Model)
{
<tr>
<td>#Html.CheckBoxFor(modelItem => item.IsSelected)</td>
<td>
#Html.DisplayFor(modelItem => item.Address)
</td>
</tr>
}
</table>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
}
My expectations was - when I hit the "Save" button, the Model, which is IEnumerable<CheckedRentalProperty> item, will be passed to the Save() action of the controller. However, I find that the passed parameter is "null" all the time. What am I missing?
Model that are solely IEnumerable are not too friendly as MVC Model.
There are many issues arise here, but in a nutshell, MVC webform bindings needs form name requests to be send in the following format: PropertyName[Index].Property
Which is not the case at your example.
It is a good design practice, to create a wrapping ViewModel which will hold the properties you need for the given controller + pages.
ViewModel
public class RentalPropertiesViewModel
{
public List<CheckedRentalProperty> CheckedRentalProperties { get; set; }
}
Controller: Next we will want to use this ViewModel in our controller.
public ActionResult Index()
{
var checkHsList = new List<CheckedRentalProperty>();
checkHsList.Add(new CheckedRentalProperty { Id = 1, Address = "Address1", IsSelected = true });
checkHsList.Add(new CheckedRentalProperty { Id = 2, Address = "Address2", IsSelected = false });
checkHsList.Add(new CheckedRentalProperty { Id = 3, Address = "Address3", IsSelected = true });
var model = new RentalPropertiesViewModel
{
CheckedRentalProperties = checkHsList
};
return View(model);
}
[HttpPost]
public ActionResult Save(RentalPropertiesViewModel model)
{
// why checkHsList is coming as null ??
return null;
}
View: Now in our view we should set the Model as the new ViewModel type we created.
#model TestBindings.Models.RentalPropertiesViewModel
And our view form should be something like:
<table class="table">
<tr>
<th>
Is Selected
</th>
<th>
Address
</th>
</tr>
#for (int i = 0; i < Model.CheckedRentalProperties.Count(); i++)
{
<tr>
#Html.HiddenFor(model => model.CheckedRentalProperties[i].Id);
<td>#Html.CheckBoxFor(model => model.CheckedRentalProperties[i].IsSelected)</td>
<td>#Html.TextBoxFor(model => model.CheckedRentalProperties[i].Address)</td>
</tr>
}
</table>
I've use the following format model => model.CheckedRentalProperties[i].IsSelected and now MVC InputExtensions will bind it correctly. e.g: CheckedRentalProperties[0].IsSelected
Important Note: Notice i'm passing Id property as hidden, so MVC Binder will know to set the Id to the correct item.

Asp.net mvc how to pass multiple checkbox values to controller

Im working on an application which can download emails. In my view if checkbox/checkboxes are checked, i want to pass an ID to controller to download this messages. I will try to explain with code:
#using (Html.BeginForm("DownloadData", "Messages", FormMethod.Post))
{
<table class="table table-bordered table-hover">
#for (int i = 0; i < Model.Count; i++)
{
<tr>
<td><input type="checkbox"
name="selectedIds[#i].messageID"/></td>
<td>#Model[i].messageFrom</td>
<td>#Model[i].messageSubject</td>
<td>#Model[i].messageDate</td>
</tr>
}
</table>
<div class="col-md-offset-2 col-md-10">
<input type="submit" class="btn btn-default" value="Download" />
</div>
}
The #i in name="selectedIds[#i].messageID" doesnt going to be good because its increasing and if i check: 1st the 2nd and the 4th message, it will add only the first two elements to my list.
The controller:
[HttpPost]
public ActionResult DownloadData(List<MessagesModels> selectedIds)
{
return View(...);
}
The model:
public class MessagesModels
{
public int messageID { get; set; }
public string messageSubject { get; set; }
...
}
I think it isnt hard, but cant find the sollution. Thanks for those who can help!
To avoid changing your message model just for this view (or if you are unable to begin with) I would recommend creating a new model for this view to handle selecting like this:
public class MessageSelectionViewModel
{
public MessagesModels Message { get; set; }
public bool IsSelected { get; set; }
}
Your view would now be a list of these new view objects so would look more like this:
<td>#Html.CheckBoxFor(item => item[i].IsSelected)</td>
<td style="display:none;">#Html.HiddenFor(item => item[i].Message.Id)</td>
<td>#Model[i].Message.messageFrom</td>
<td>#Model[i].Message.messageSubject</td>
<td>#Model[i].Message.messageDate</td>
Your post method would become:
[HttpPost]
public ActionResult DownloadData(IEnumerable<MessageSelectionViewModel> selectedMessages)
{
foreach (int messageId in selectedMessages.Where(m => m.IsSelected == true).Select(m => m.Message.Id))
{
}
return View(...);
}
Of course if it's not a big deal you could save some time and effort and just add IsSelected to your model like Stephen suggested.

Trying to Post a Model back to the Controller, but it creates a new model instead

Tpa class is my base model.
public class Tpa
{
public bool selected { get; set; }
public int Id { get; set; }
}
Data class creates a list of Tpa objects.
public class Data
{
public List<Tpa> Tpas { set; get; }
public Data()
{
this.Tpas = new List<Tpa>();
this.Tpas.Add(new Tpa()
{
selected = false ,
Id = 1,
});
this.Tpas.Add(new Tpa()
{
selected = false,
Id = 2,
});
this.Tpas.Add(new Tpa()
{
selected = true,
Id = 3,
});
}
}
This is my Get.
[HttpGet]
public virtual ActionResult Index()
{
var model = new Data();
return View(model);
}
This is my view.
#model TpaUpload_2.Models.Data
#using (Html.BeginForm(MVC.TpaUpload_2.Home.ReceiveID(), FormMethod.Post))
<table class="table">
<tr>
#for (int count = 0; count < Model.Tpas.Count; count++)
{
var item = Model.Tpas[count];
<tr>
<td>
<input type="checkbox"
name=#Html.Raw("'s" + count + "CheckBox'")
id=#Html.Raw("'s" + count + "CheckBox'")
#*checked="#(item.selected == true)"*# />
<label for=#Html.Raw("'s" + count + "CheckBox'" )></label>
<input type='hidden'
id=#Html.Raw("'s" + count + "CheckBox'" )
name='item.selected'
value=#Html.Raw("'"+item.selected+"'")/>
</tr>
</table>
<input type="submit" value="Submit" />
This my Post.
[HttpPost]
public virtual ActionResult ReceiveID(Data myData)
{
...
}
I'm trying to use the checkbox value to change the "selected" on the model, and post back the model.
The problem is after the Form is submitted to the Post, the program will construct a new Data object, instead of using the Data model passed to the controller.
What did I do wrong? Any help will be greatly appreciated.
Your constructing html with name attributes that have absolutely no relationship to you model. When you submit, the DefaultModelBinder first initializes your Data model (which means that 3 new Tpa objects are added to its Tpas property. It then tries to find name/value pairs in the form collection that match you model properties but there are none.
First you need to modify you constructor to include only the initialization of the list, and remove the adding of the new items
public class Data
{
public List<Tpa> Tpas { set; get; }
public Data()
{
Tpas = new List<Tpa>();
}
}
And add the items in the GET method
public virtual ActionResult Index()
{
var model = new Data();
model.Tpas .Add(new Tpa(){ selected = false, Id = 1 });
// add other items
return View(model);
}
Then you need to construct you view correctly using the strongly typed html helpers so that your form controls are correctly named in relationship to your model
<table class="table">
#for (int i = 0; i < Model.Tpas.Count; i++)
{
<tr>
<td>
#Html.HiddenFor(m => m.Tpas[i].Id)
#Html.CheckBoxFor(m => m.Tpas[i].selected)
#Html.LabelFor(m => m.Tpas[i].selected)
</td>
</tr>
}
</table>
This give your controls the correct name attribute for model binding, for example
<input type="hidden" name="Tpas[0].Id" ... />
<input type="hidden" name="Tpas[1].Id" ... />
<input type="hidden" name="Tpas[2].Id" ... />
I suggest you compare that with what your currently generating to understand the difference.
Note also your current html is invalid - you have multiple <tr> elements inside a <tr> elements and you need to include the hidden input for the Id property or else this will not post back and you will end up with 3 Tpa objects all with id = 0.

ViewModel not posting back

I have this in my controller:
public ActionResult Index()
{
var viewModels = _dataSyncService.Get().Select(provider => new IndexViewModel
{
Selected = false, Provider = provider
}).ToList();
return View(viewModels);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Index(IEnumerable<IndexViewModel> viewModels)
{
//Breakpoint on this to check viewModels
}
ViewModel:
public class IndexViewModel
{
public bool Selected { get; set; }
public IDataSyncProvider Provider { get; set; }
}
And my Index.cshtml:
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<button type="submit" class="btn blue">Trigger Selected</button>
#foreach (var sync in Model)
{
<tr>
<td>
#Html.CheckBoxFor(s => sync.Selected)
</td>
<td>#sync.Provider.FriendlyName</td>
</tr>
}
}
But my models are posted back, viewModels always comes back as null. I read alot about having to assign Id's to the check box etc but I thought that's what Html.CheckBoxFor is for.
I'm sure I'll be kicking myself about this at some point but I could do with some guidance please.
If you want to use the built in Html helpers like Html.CheckBoxFor then you need to use a for loop instead of the foreach in order to the helpers render the correct input names:
#for(int sync = 0; sync < Model.Count; sync++)
{
<tr>
<td>
#Html.CheckBoxFor(m => Model[sync].Selected)
</td>
<td>#Model[sync].Provider.FriendlyName</td>
</tr>
}
You can read more about binding to lists here: Model Binding To A List
This is because, you are just redirecting it using return RedirectToAction("Index");
In your POST action
try :
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Index(IEnumerable<IndexViewModel> viewModels)
{
return View(viewModels);
}
Hope will help.

Posting Ienumerable Values and Saving to M-2-M Relationship

VS'12 KendoUI InternetApplication Template C# asp.net EF Code First
My Question is how to pass both the Regular ( are passing now ) values and the Ienumerable(passing null) into my controller and saving them to the Database using EF Code First in a Many-2-Many Relationship manor.
The Following is what i have tried
Main View
#model OG.Models.UserProfiles
#using (Html.BeginForm())
{
<div class="editor-field">
<div class="Containter">
<div>
#Html.DisplayFor(model => model.UserName)
</div>
<div class="contentContainer">
#foreach (var item in Model.Prospects)
{
<table>
<tr>
<td>
#Html.Label("Current Prospects")
</td>
</tr>
<tr>
<td>
#Html.DisplayNameFor(x=>item.ProspectName)
</td>
</tr>
</table>
}
</div>
</div>
<div class="contentContainer2">
#Html.Partial("_UsersInProspectsDDL", new OG.ModelView.ViewModelUserInProspects() { Users = Model.UserName })
</div>
</div>
}
Partial View
#model OG.ModelView.ViewModelUserInProspects
<label for="prospects">Prospect:</label>
#(Html.Kendo().DropDownListFor(m=>m.Prospects)
.Name("Prospects")
.HtmlAttributes(new { style = "width:300px"}) //, id = "countys"})
.OptionLabel("Select Prospect...")
.DataTextField("ProspectName")
.DataValueField("ProspectID")
.DataSource(source => {
source.Read(read =>
{
read.Action("GetCascadeProspects", "ChangeUsersInfo")
.Data("filterProspects");
})
.ServerFiltering(true);
})
.Enable(false)
.AutoBind(false)
.CascadeFrom("Clients")
</div>
Model for PartialView
public class ViewModelUserInProspects
{
public string Clients { get; set; }
public IEnumerable<dbClient> AvailableClients { get; set; }
public string Prospects { get; set; }
public IEnumerable<dbProspect> AvailableProspects { get; set; }
public string Users { get; set; }
public IEnumerable<UserProfiles> AvailableUsers {get;set;}
}
}
Main Model
Standart SimpleMemberShipUserTable
Post Method
[HttpPost]
public ActionResult UsersInProspect(
[Bind(Include= "ProspectName, ProspectID")]
UserProfiles userprofiles, ViewModelUserInProspects values, FormCollection form)
//<- Trying different things sofar
{
if (ModelState.IsValid)
{
//string something = form["Prospects"];
int prosID = Convert.ToInt16(values.Prospects);
int UserID = userprofiles.UserID; // <- THIS VALUE is null atm.
This is where i need to save both ID's to the EF Generated / Mapped Table. Unsure how.
db.Entry(userprofiles).CurrentValues.SetValues(userprofiles);
db.Entry(userprofiles).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(userprofiles);
}
Please take a look Here
Goes over ViewModels
What EditorTemplate are and how to use them
What the GET Method would look like
What the Edit View would look like
Give you a View Example
What the Post Method would look like

Resources