There are already several good discussions about how model properties set in the form post controller won't show up in view because of model state. To take the discussion a step further I have an example where items in the VIEW are updated but the model is not, again due to Model state. That's right, what your user sees will NOT match the model properties that are returned in the post!
say you have a model with a list
public class FruitBasket {
public string OwnersName { get; set; }
public IEnumerable<Fruit> Fruits { get; set; }
public string FruitColor { get; set; }
public FruitBasket() {
this.OwnersName = "Bob";
this.FruitColor = "Red";
}
}
public class Fruit {
public string Name { get; set; }
public string Color { get; set; }
}
And a data access class has methods that retrieves a list of fruits based on color
Data Access code
public IEnumerable<Fruit> GetFruits(string color) {
if (color == "Red") {
List<Fruit> fruits = new List<Fruit> {
new Fruit { Name = "Apple", Color = "Red" },
new Fruit { Name = "Grape", Color = "Red" }
};
return fruits;
} else {
List<Fruit> fruits = new List<Fruit> {
new Fruit { Name = "Banana", Color = "Yellow"},
new Fruit { Name = "Apple", Color = "Red" },
new Fruit { Name = "Grape", Color = "Red" }
};
return fruits;
}
}
Controller (where db is the data access class)
public ActionResult FruitBasket() {
FruitBasket basket = new FruitBasket();
basket.Fruits = db.GetFruits("Red");
return View(basket);
}
[HttpPost]
public ActionResult FruitBasket(FruitBasket basket) {
basket.Fruits = db.GetFruits("All");
return View(basket);
}
View
#model GEWeb.Models.FruitBasket
<!DOCTYPE html>
<html>
<head>
<title>FruitBasket</title>
</head>
<body>
#using (Html.BeginForm()) {
<p>#Html.DisplayFor(model => Model.OwnersName)</p>
<table>
#Html.EditorFor(model => model.Fruits)
</table>
<input type="submit" value="filter" />
}
</body>
</html>
Editor Template
(the template for editorfor is in views/shared/EditorTemplates)
the hiddenfor helpers are included so that the model will be populated with list data on post back
#model GEWeb.Models.Fruit
<tr>
<td>
#Html.DisplayFor(model => model.Name)
#Html.HiddenFor(model => model.Name)
</td>
<td>
#Html.DisplayFor(model => model.Color)
#Html.HiddenFor(model => model.Color)
</td>
<td>
#Html.CheckBoxFor(model => model.Remove)
</td>
</tr>
Scenario
- The user requests the page and sees the list of red fruits.
- The user clicks the filter button, the post controller loads all fruits, the user sees a list of all three fruits
- The user clicks the filter button a second time. Now inspect the basket properties as passed to the post controller, the list will not match what the user saw.
Now imagine that you wanted to use that checkbox to select fruits to remove from the basket. You could pass the basket.fruits model to a method in your data access class which would delete the fruits and then you could have your conroller reload the basket.fruits list. But it will not work because the list returned to the controller does not match what the user is seeing.
This "feature" can be subverted by putting the following into the Post controller
ModelState.Clear();
But realy, I shouldn't have to. Views and controllers should pass the model back and forth accurately. ModelState is an attempt to persist View properties between page loads and that doesn't seem to be in keeping with the MVC phylosophy where state is managed by the database, and not the view!
Related
I have a MVC 3 page that returns a list of user responses with a partial view called "memo" (which displays/add memos) for each response. When I add a memo to a response, it should update the db and the list of memos for that response. It should be partial page update via ajax, which effects only the partial view "memo".
The view Response.chtml that contains "memo":
#using (Html.BeginForm("Response", "User", FormMethod.Post, new { id = "UserResponse" }))
{
.... code removed ....
#foreach (var response in Model)
{
<div class="qna"><input type="text" id=#response.responseId value="#response.ResponseText" />
<div>#Html.Partial("_memo", response.responseId)</div>
}
.....
The partial page "_memo.chtml":
<div>add memo</div>
<ul id="memos">
#foreach (var memo in Model) {
<li>#memo.Text</li>
}
</ul>
<form method="post" id="memoForm"
action="#Url.Action("AddMemo")">
#Html.TextArea("Memo", new { rows = 5, cols = 50 })
<br />
<input type="submit" value="Add" />
</form>
Controller for view User/Response:
[HttpGet]
public ActionResult Response(id)
{
.....
return View(responses);
I just started with the code above, need help filling the blanks.
If I pass the response Id to the partial view, how do I pull the list of memos for that response? Will it involve ajax? (instead of ..Partial("_memo", response.memos))
How do I update the partial view via ajax call. What is ajax call (sample code) on the client side and how would the controller look? When the ajax call is successful, how do I update the list memos div="memos" to reflect the new memo?
Will the form action from Response conflict with form action of the partial view Memo?
Answers to Questions:
You shouldn't pass the responseId to the partial, you should pass the memo collection from your response object and make your partial view strongly typed to that collection.
See full code example below.
You don't need the form in the partial since you're making a simple ajax call to add the new memo. See full code example below.
This is a modified example from a project I am currently working on:
There is a bit of code to follow, so here goes:
This is my model. There are several sections on a career planning form, one of which is a section to select and update competencies. The SelectCompetencies model has a collection of competencies within it. The user will have the ability to add competencies. When they do, it will be added to the database and will update the list of competencies in the partial.
public class CareerPlanningFormViewModel
{
// code removed ...
public SelectCompetenciesModel SelectCompetencies { get; set; }
// code removed ...
}
public class SelectCompetenciesModel
{
public int CareerPlanningFormID { get; set; }
public IList<CompetencyModel> Competencies { get; set; }
public byte MaximumCompetenciesAllowed { get; set; }
}
public class CompetencyModel
{
public int CompetencyID { get; set; }
public int? CompetencyOptionID { get; set; }
public string ActionPlan { get; set; }
public IDictionary<int, string> CompetencyOptions { get; set; }
}
The main view of the career planning form: /Views/CPF/CareerPlanningForm.cshtml
#model MyNamespace.Models.CareerPlanningForm.CareerPlanningFormViewModel
<link rel="stylesheet" href="#Url.Content("~/Content/CreateCPF.css")" />
#using (Html.BeginForm())
{
// other sections loaded here...
// code removed for brevity...
#Html.Partial("SelectCompetencies", Model.SelectCompetencies)
// other sections loaded here...
// code removed for brevity...
}
The SelectCompetencies partial: /Views/CPF/SelectCompetencies.cshtml
The user will fill in the new action plan text and click the add competency button.
That will post via ajax to CPFController/NewCompetencyTemplate
#model MyNamespace.Models.CareerPlanningForm.SelectCompetenciesModel
#Html.HiddenFor(m => m.CareerPlanningFormID)
<h3>Select Competencies</h3>
<p class="guidance">
Select up to #Model.MaximumCompetenciesAllowed competencies to focus on improving.
</p>
<table id="CompetenciesTable">
<thead>
<tr>
<th>Competency</th>
<th>Action Plan:</th>
</tr>
</thead>
<tbody>
#for (int i = 0; i < Model.Competencies.Count(); i++)
{
#Html.EditorFor(m => m.Competencies[i])
}
</tbody>
<tfoot id="CompetenciesTableFooter" class="#(Model.Competencies.Count() < Model.MaximumCompetenciesAllowed ? "" : "hidden")">
<tr>
<td colspan="2">
#Html.TextArea("NewActionPlanText")
#Html.Button(ButtonType.Button, "Add Another Competency", "add", new { id = "AddCompetencyButton" })
</td>
</tr>
</tfoot>
</table>
#section script
{
<script>
jQuery(document).ready(function ($) {
var competenciesTableBody = $('#CompetenciesTable tbody'),
competenciesTableFooter = $('#CompetenciesTableFooter'),
addCompetencyButton = $('#AddCompetencyButton'),
newCompetencyTemplateUrl = '#Url.Content("~/CPF/NewCompetencyTemplate")',
count = competenciesTableBody.find('tr').length,
newActionPlanText = $('#NewActionPlanText'),
careerPlanningFormID = $('#CareerPlanningFormID');
addCompetencyButton.click(function () {
$.ajax({
url: newCompetencyTemplateUrl(),
type: 'POST',
data: {
careerPlanningFormID: careerPlanningFormID,
actionPlan: newActionPlanText,
itemCount: count
},
dataType: 'html',
success: function (data) {
var elements = $(data);
// other code removed here...
competenciesTableBody.append(elements);
// other code removed here...
}
});
});
});
</script>
}
Views/CPF/EditorTemplates/CompetencyModel.cshtml
#model MyNamespace.Models.CareerPlanningForm.CompetencyModel
<tr class="competency">
<td>
#Html.DropDownListFor(m => m.CompetencyOptionID, new SelectList(Model.CompetencyOptions, "Key", "Value"), "Select competency...")
</td>
<td>
#Html.TextAreaFor(m => m.ActionPlan, new { #class = "competencyActionPlan" })
#Html.HiddenFor(m => m.CompetencyID)
</td>
</tr>
The controller containing the action to add the new competency: /Controllers/CPFController.cs
This will call the CareerPlanningFormService to add the new competency and will return a partial view for NewCompetencyTemplate that will render out the new competency
public class CPFController : Controller
{
private readonly ICareerPlanningFormService careerPlanningFormService;
public CPFController(ICareerPlanningFormService careerPlanningFormService)
{
this.careerPlanningFormService = careerPlanningFormService;
}
[HttpPost]
public PartialViewResult NewCompetencyTemplate(int careerPlanningFormID, int itemCount, string newActionPlanText)
{
var count = itemCount + 1;
// Even though we're only rendering a single item template, we use a list
// to trick MVC into generating fields with correctly indexed name attributes
// i.e. Competencies[1].ActionPlan
var model = new SelectCompetenciesModel
{
Competencies = Enumerable.Repeat<CompetencyModel>(null, count).ToList()
};
model.Competencies[count - 1] = this.careerPlanningFormService.BuildNewCompetencyModel(careerPlanningFormID, newActionPlanText);
return this.PartialView(model);
}
}
My service class: CareerPlanningFormService.cs
This handles the business logic and makes the calls to the repository to add the item to the database and returns a new CompetencyModel
public class CareerPlanningFormService : ICareerPlanningFormService
{
private readonly IMyRenamedRepository repository;
private readonly IPrincipal currentUser;
public CareerPlanningFormService(
IMyRenamedRepository repository,
IPrincipal currentUser)
{
this.repository = repository;
this.currentUser = currentUser;
}
public CompetencyModel BuildNewCompetencyModel(int careerPlanningFormID, string newActionPlanText)
{
var competency = new Competency
{
CareerPlanningFormID = careerPlanningFormID,
CompetencyOptionID = null,
ActionPlan = newActionPlanText
};
this.repository.Add(competency);
this.repository.Commit();
return new CompetencyModel
{
CompetencyID = competency.CompetencyID,
CompetencyOptionID = competency.CompetencyOptionID,
ActionPlan = competency.ActionPlan,
CompetencyOptions = this.GetCompetencyOptionsForCareerPlanningFormID(careerPlanningFormID)
};
}
}
Now, the partial for NewCompetencyTemplate: Views/CPF/NewCompetencyTemplate.cshtml
This is very simple, it simply renders the same editor template as above, for the last competency in the collection (which we just added)
#model MyNamespace.Models.CareerPlanningForm.SelectCompetenciesViewModel
#Html.EditorFor(m => m.Competencies[Model.Competencies.Count() - 1])
When the ajax call succeeds, it will receive this partial back from the controller action method it called. It then takes the partial and appends it to the competencies table body
// snippet from ajax call above
competenciesTableBody.append(elements);
I hope this helps. Let me know if you have any additional questions.
While you're correct that you can do it just by returning a partial view containing the updated content, you may also consider using jQuery's load method.
Look here, in particular at the "loading page fragments" section. Basically you can just get the original page again and jQuery will "extract" the content you want as long as it can be targetted by a selector (such as a div id).
Note, this solution is not suitable in all cases as there will be redundant markup in the response from the server because you will be discarding the rest of the page content and just using the updated part.
using the JQuery sortable, and trying to send the new order back to my controller, but not having a whole lot of luck. My view is:
using (Ajax.BeginForm("EditTickerOrder", new AjaxOptions { InsertionMode = InsertionMode.Replace, HttpMethod = "POST", }))
{
<div id="editableticker">
#Html.HiddenFor(m => m.ProjectGUID)
<ul id="sortablediv">
#foreach (DGI.CoBRA.Tools.BussinessObjects.CollabLibrary.TickerObjects.Ticker t in Model)
{
<li class="ui-state-default" id="#t.pKeyGuid.ToString()">
<p>#Html.CheckBox(t.pKeyGuid.ToString(), t.Display, new { #class = "activechk" })
<span style="font-weight: bold">
#t.Text
</span>
</p>
</li>
}
</ul>
<input type="submit" value="Save New Ticker Order" />
}
and my controller is:
[HttpPost]
public ActionResult EditTickerOrder(Guid ProjectGUID, List<string> items)
{
TickerCollectionModel TickerData = new TickerCollectionModel();
TickerData.ProjectGUID = ProjectGUID;
TickerData.ListAllBySession(ProjectGUID);
return PartialView("TickerList", TickerData);
}
yet the list<string> items is always null. Any ideas?
You are writing foreach loops, most definitely violating the naming conventions for your form input fields that the default model binder expects for working with collections. If you don't respect the established wire format, you cannot expect the default model binder to be able to rehydrate your models in the POST action.
In fact, why don't you use view models and editor templates? They make everything trivial in ASP.NET MVC.
So let's define a view model that will reflect your view requirements (or at least those shown in your question => you could of course enrich it with additional properties that you want to handle):
public class TickerViewModel
{
public Guid Id { get; set; }
public bool IsDisplay { get; set; }
public string Text { get; set; }
}
public class ProjectViewModel
{
public Guid ProjectGUID { get; set; }
public IEnumerable<TickerViewModel> Tickers { get; set; }
}
and then a controller whose responsibility is to query your DAL layer, retrieve a domain model, map the domain model into the view model we defined for this view and pass the view model to the view. Inversely, the POST action receives a view model from the view, maps the view model back into some domain model, passes the domain model to your DAL layer for processing and renders some view or redirects to a success action:
public class HomeController : Controller
{
public ActionResult Index()
{
// TODO: those values come from a data layer of course
var model = new ProjectViewModel
{
ProjectGUID = Guid.NewGuid(),
Tickers = new[]
{
new TickerViewModel { Id = Guid.NewGuid(), Text = "ticker 1" },
new TickerViewModel { Id = Guid.NewGuid(), Text = "ticker 2" },
new TickerViewModel { Id = Guid.NewGuid(), Text = "ticker 3" },
}
};
return View(model);
}
[HttpPost]
public ActionResult Index(ProjectViewModel model)
{
// Everything will be correctly bound here => map the view model
// back into your domain model and pass the domain model to
// your DAL layer for processing ...
return Content("Thanks for submitting");
}
}
a view (it is worth noting that in this example I have used a standard form instead of AJAX but it is trivial to convert it into an AJAX form):
#model ProjectViewModel
#using (Html.BeginForm())
{
#Html.HiddenFor(m => m.ProjectGUID)
<div id="editableticker">
<ul id="sortablediv">
#Html.EditorFor(x => x.Tickers)
</ul>
</div>
<button type="submit">OK</button>
}
and finally the corresponding editor template which will automatically be rendered for each element of the Tickers collection (~/Views/Home/EditorTemplates/TickerViewModel.cshtml):
#model TickerViewModel
<li class="ui-state-default">
<p>
#Html.CheckBoxFor(x => x.IsDisplay, new { #class = "activechk" })
#Html.LabelFor(x => x.IsDisplay, Model.Text)
#Html.HiddenFor(x => x.Text)
#Html.HiddenFor(x => x.Id)
</p>
</li>
I have the below code in my VIEW, and thereafter a submit button. I do have many of these checkboxes in my view, so that the user can click on as many as he wishes.
#Html.CheckBox("code" + l_code, false, new { Value = #item.expertiseCode })
In my controller i have the fll., which is the HTTPPost method
public ActionResult RegisterSP(RegisterModel model, FormCollection collection)
However, when debugging, i see that ALL the checkboxes are being passed back to the controller, and not just the ones that were clicked on. I just want the ones that were clicked on and ignore the rest for i need to add these to the DB. Also, the checbox values passed in contains TRUE/FALSE. Because of this the false value is also being added to the DB. If i use the below method (not using the htmlHelper), i dont have the above problem. But i wud like to use the htmlHelper:
<input type="checkbox" name="code#(l_code)" value="#item.expertiseCode" />
IF you have a collection of Checkboxes, Create a ViewModel like this
public class ExpertiseCodeViewModel
{
public string Name { set;get;}
public int ExpertiseId { set;get;}
public bool IsSelected { set;get;}
}
Now In your main ViewModel, Add a collection of this as a property
public class UserViewModel
{
public List<ExpertiseCodeViewModel > Expertises{ set; get; }
public UserViewModel()
{
if(this.Expertises==null)
this.Expertises=new List<ExpertiseCodeViewModel>();
}
}
And in your create an editor template called ExpertiseCodeViewModel
#model ExpertiseCodeViewModel
#Html.CheckBoxFor(x => x.IsSelected)
#Html.LabelFor(x => x.IsSelected, Model.Name)
#Html.HiddenFor(x => x.ExpertiseId )
Include this in your Main View
#model UserViewModel
#using (Html.BeginForm())
{
//other elements
#Html.EditorFor(m=>m.Expertises)
<input type="submit" value="Save" />
}
In your HTTPPost Action method,
[HttpPost]
public ActionResult Save(UserViewModel model)
{
List<int> items=new List<int>();
foreach (ExpertiseCodeViewModel objItem in model.Expertises)
{
if (objPVM.IsSelected)
{
//you can get the selected item id here
items.Add(objItem.ExpertiseId);
}
}
}
Try
#Html.CheckBox("code" + l_code, false, new { #value = item.expertiseCode })
or
string name = "code" + l_code;
#Html.CheckBox(name, false, new { #value = item.expertiseCode })
I'm sure this is easy, but maybe I haven't searched well ...
I want to know how to get results from a partial view back to the model and/or controller.
If the user enters a FirstName, Gender (from drop down) and Grade (from drop down), I only find then FirstName and Gender in the model. I want to know how to get the Grade from the drop down in the partial view all the way back into the model, so I can see it in the controller.
Please look for this question in the controller code:
What do I need to do to get the GradeLevel from the partial class to be here: <<<<<
Note: this is not the exact code. There may be small, insignificant typo's.
EDIT: Apparently you can't add a long comment, so I will add here:
Thank you, Tom and Mystere Man. Tom got me thinking as to why it doesn't work. I didn't think through the model binding. With the design I proposed, the HTML gets rendered and the Grade drop down has this id: "Grade". The property on the model I want to bind to is: "GradeLevelID". If I change the helper in the partial view to be #Html.DropDownList("GradeLevelID" ... it works perfectly.
But that is not a good solution. My idea was to abstract the partial view from the main view. Hard coding the name blows that! I did work up a slightly improved solution. In the main view, I change the #Html.Partial statement to pass the model property name to the partial. Like such:
#Html.Partial("GradeDropDown", (SelectList)Model.GradeSelectList, new ViewDataDictionary { { "modelPropertyName", "GradeLevelID" } })
Then I could change the partial view to say
#model System.Web.Mvc.SelectList
#Html.DropDownList((string)ViewData["modelPropertyName"], Model)
But that also seems like a goofy way to approach things. Thanks for the help. I'll look at EditorTemplates.
Here is my model:
public class RegisterModel{
public MemberRegistration MemberRegistration{
get{
if (HttpContext.Current.Session["MemberRegistration"] == null){
return null;
}
return (MemberRegistration)HttpContext.Current.Session["MemberRegistration"];
}
set{
HttpContext.Current.Session["MemberRegistration"] = value;
}
}
public string FirstName{
get{
return MemberRegistration.FirstName;
}
set{
MemberRegistration.FirstName = value;
}
}
public SelectList GenderSelectList{
get{
List<object> tempList = new List<object>();
tempList.Add(new { Value = "", Text = "" });
tempList.Add(new { Value = "M", Text = "Male" });
tempList.Add(new { Value = "F", Text = "Female" });
return new SelectList(tempList, "value", "text", MemberRegistration.Gender);
}
}
[Required(ErrorMessage = "Gender is required")]
public string Gender{
get{
return MemberRegistration.MemberPerson.Gender;
}
set{
MemberRegistration.MemberPerson.Gender = value;
}
}
public SelectList GradeLevelSelectList{
get{
List<object> tempList = new List<object>();
tempList.Add(new { Value = "", Text = "" });
tempList.Add(new { Value = "1", Text = "1st" });
tempList.Add(new { Value = "2", Text = "2nd" });
tempList.Add(new { Value = "3", Text = "3rd" });
tempList.Add(new { Value = "4", Text = "4th" });
return new SelectList(tempList, "value", "text", MemberRegistration.GradeLevel);
}
}
[Required(ErrorMessage = "Grade is required")]
public Int32 GradeLevel{
get{
return MemberRegistration.GradeLevel;
}
set{
MemberRegistration.GradeLevel = value;
}
}
}
Here is my main view:
#model RegisterModel
#using (Html.BeginForm())
{
<p class="DataPrompt">
<span class="BasicLabel">First Name:</span>
<br />
#Html.EditorFor(x => x.FirstName)
</p>
<p class="DataPrompt">
<span class="BasicLabel">Gender:</span>
<br />
#Html.DropDownListFor(x => x.Gender, Model.GenderSelectList)
</p>
<p class="DataPrompt">
<span class="BasicLabel">Grade:</span><span class="Required">*</span>
<br />
#Html.Partial("GradeDropDown", (SelectList)Model.GradeLevelSelectList)
</p>
<p class="DataPrompt">
<input type="submit" name="button" value="Next" />
</p>
}
Here is my partial view (named "GradeDropDown"):
#model System.Web.Mvc.SelectList
#Html.DropDownList("Grade", Model)
Here is my controller:
[HttpPost]
public ActionResult PlayerInfo(RegisterModel model)
{
string FirstName = model.Registration.FirstName;
string Gender = model.Registration.Gender;
>>>>> What do I need to do to get the GradeLevel from the partial class to be here: <<<<<
Int32 GradeLevel = model.Registration.GradeLevel;
return RedirectToAction("Waivers");
}
I don't even know why you are using a partial view. All you're doing is using one helper method, you could replace the partial view with the helper method in the view and it would be less code.
Second, you should be using Html.DropDownListFor() instead of Html.DropDownList(), then it will correctly name the html controls for you.
Just do this:
<p class="DataPrompt">
<span class="BasicLabel">Grade:</span><span class="Required">*</span>
<br />
#Html.DropDownListFor(m => m.GradeLevel, (SelectList)Model.GradeLevelSelectList)
</p>
try this to get the correct naming for the elements when they get posted.
On your main view
#Html.Partial("GradeDropDown", Model) //Pass the Model to the partial view
Here is your partial view (named "GradeDropDown"):
#model RegisterModel
#Html.DropDownList("Grade", (SelectList)Model.GradeLevelSelectList)
this is my Model1 class
namespace chetan.Models
{
public class Model1
{
public string selectedItem { get; set; }
public IEnumerable<SelectListItem> items { get; set; }
}
}
this is my controller class
public class HomeController : Controller
{
private rikuEntities rk = new rikuEntities();
public ActionResult Index()
{
var model = new Model1
{
items = new[]
{
new SelectListItem { Value = "Theory", Text = "Theory" },
new SelectListItem { Value = "Appliance", Text = "Appliance" },
new SelectListItem { Value = "Lab", Text = "Lab" }
}
}; return View(model);
}
public ActionResult viewToController(Model1 m)
{
string getSelectedName = m.selectedItem;
return Content(getSelectedName);
}
}
this is my view...
#using (Html.BeginForm("viewToController", "Home"))
{
#Html.ValidationSummary(true)
<fieldset>
<legend>emp</legend>
<div class="editor-field">
#Html.DropDownListFor(x => x.selectedItem,
new SelectList(Model.items, "Value", "Text"))
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
i want to add a drop downlist and i want to use selected value in viewToController action of homeController. and there is also one error in View page is "an expression tree may not contain dynamic operation" in (x=>x.selectedItem). Please solve my problem .
I don't understnad what you exactly need. You want to dynamicly add items to the drop down from the database?
I'm big fan of jQuery. You can do everything what you want with HTML using jQuery. So if you are looking how to automaticly add items to the drop down, take look at this: How do I add options to a DropDownList using jQuery?