I am having difficulty passing an IEnumerable as a model. The data is populating a form on one page - and doing so correctly. Upon submission the model returns as null.
I've seen various posts on this and they mostly reference naming-conventions so I have attempted different methods of naming the parameters to try to avoid any confusion in the model binding.
I have also tried various models and helpers to try and pass the data and all have the same result.
Current implementation:
Models:
public class UserProfileListModel
{
public IEnumerable<UserProfileViewModel> UserProfileViewModels { get; set; }
}
public class UserProfileViewModel
{
public UserProfile UserProfile { get; set; }
public Role UserRole { get; set; }
public Team UserTeam { get; set; }
public Scope UserScope { get; set; }
}
View:
#model Project.WebUI.Models.UserPRofileListModel
SNIP
<fieldset>
<legend>Administrate Users:</legend>
<table class="adminTbl">
<thead>
<tr>
<th>UserName:</th>
<th>Role:</th>
<th>Team:</th>
<th>Scope:</th>
<th>Update:</th>
<th>Delete:</th>
</tr>
</thead>
<tbody>
#{foreach (var user in Model.UserProfileViewModels)
{
<tr>
<td>
<p>#user.UserProfile.UserName
#{if (!user.UserProfile.Membership.IsConfirmed)
{
using (Html.BeginForm("Confirm", "Account", FormMethod.Post, null)){
#Html.AntiForgeryToken()
#Html.Hidden("Token", user.UserProfile.Membership.ConfirmationToken)
#Html.Hidden("Name", user.UserProfile.UserName)
}
<input type="submit" value="Confirm" />}
}
</p>
</td>
#{using (Html.BeginForm("SaveUserChanges", "Account", FormMethod.Post, null))
{
#Html.AntiForgeryToken()
#Html.HiddenFor(u => user.UserProfile)
if (user.UserProfile.UserName != User.Identity.Name && user.UserProfile.Membership.IsConfirmed)
{
<td>
#Html.DropDownListFor(u => user.UserRole, Project.WebUI.Controllers.AccountController.RoleList, new { #class = "formdrop" })
</td>
<td>
#Html.DropDownListFor(u => user.UserTeam, Project.WebUI.Controllers.AccountController.TeamList, new { #class = "formdrop" })
</td>
<td>
#Html.DropDownListFor(u => user.UserScope, Project.WebUI.Controllers.AccountController.ScopeList, new { #class = "formdrop" })
</td>
<td>
<input type="submit" value="Save Changes" onclick="return confirm('Are you sure you wish to update this user? ')" />
</td>
}
else
{
/*If user is self or not yet confirmed these are here to buffer the delete button into the last cell*/
<td></td>
<td></td>
<td></td>
<td></td>
}
}
}
<td>
#Html.ActionLink("Delete", "Delete", new { user.UserProfile.UserId }, new
{
onclick = "return confirm('Warning: Action cannot be undone. Are you sure you wish to permanently delete this entry?')"
})
</td>
</tr>
}
}
</tbody>
</table>
</fieldset>
Controller:
Populate View:
public ActionResult AdministrateUsers()
{
populateLists();
var query = repository.UserProfiles.OrderBy(e => e.UserName);
List<UserProfileViewModel> list = new List<UserProfileViewModel>();
foreach(UserProfile up in query)
{
UserProfileViewModel vm = new UserProfileViewModel() { UserProfile = up };
list.Add(vm);
}
UserProfileListModel models = new UserProfileListModel()
{
UserProfileViewModels = list.OrderBy(up => up.UserProfile.UserName)
};
return View(models);
}
Accept Post:
public ActionResult SaveUserChanges(UserProfileListModel model)
{
foreach (UserProfileViewModel upvm in model.UserProfileViewModels)
{
UserProfile up = new UserProfile()
{
UserId = upvm.UserProfile.UserId,
UserEmail = upvm.UserProfile.UserName,
UserName = upvm.UserProfile.UserName
};
if (ModelState.IsValid)
{
repository.SaveUserProfile(up);
}
else
{
return View(model);
}
}
return RedirectToAction("Index", "Admin");
}
The code does still need a lot of work but I can't get past getting the model back to the controller on post. I have also tried returning the UserProfileViewModel instead of the entire list.
Can anyone tell what I am doing wrong?
Thanks!
You have a lot of invalid html including form elements as child elements of tr elements and duplicate id attributes. If you want to post back UserProfileListModel then you need a single form element and use an EditorTemplate or a for loop (not foreach) to render the controls so they are correctly named with indexers.
You are also trying to bind your dropdown lists to complex objects (for example UserProfile, Role etc.). <select> elements (and all form controls) only post back key/value pairs so you need to bind to a value type (for example UserProfile.UserId).
Your SaveUserChanges() post method is also trying access properties of UserProfile but you don't even have controls for properties of UserProfile in the form that post back to this method (for example UserId = upvm.UserProfile.UserId, UserEmail = upvm.UserProfile.UserName, ...) so they will always be null.
You probalby need to bind properties in POST method like here:
public ActionResult Create([Bind(Include = "Id,Subject,Text,IsImportant")] Announcment announcment) {... }
So it will be:
public ActionResult SaveUserChanges([Bind(Include = "UserProfile,Role,UserTeam,UserScope")]UserProfileListModel model)
Have you specified your action method is for HTTP Post? And change your action method to accept UserProfileViewModels instead.
[HttpPost]
public ActionResult SaveUserChanges(UserProfileViewModels model)
{
You are also only posting back one model: UserProfileViewModels.
You have your form in your foreach loop, so each UserProfileViewModels has its own form. If you want to change it to post back your UserProfileListModel, move
#{using (Html.BeginForm("SaveUserChanges", "Account", FormMethod.Post, null))
outside of your foreach.
Related
Advance warning, I am extremely new to ASP.NET.
I'm working on a project which will display rows of data from a db table. When a user clicks the "Ignore" button next to a row, it should update the corresponding "Ignore" column on that row with true in the database.
The view itself works fine, it displays all the data as expected. But when "Ignore" is clicked, and it calls the Ignore() method on the controller, the model is which is passed to the controller is null.
My model, generated by entity framework (with extraneous properties removed):
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace IgnoreDailyItems.Models
{
[Table("DataChecks.tbl.DailyItems")]
public partial class DataChecksTblDailyItems
{
[Column("entryId")]
public int EntryId { get; set; }
[Column("ignore")]
public bool? Ignore { get; set; }
}
}
The view:
#model IEnumerable<IgnoreDailyItems.Models.DataChecksTblDailyItems>
#{
ViewBag.Title = "Placeholder";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<table class="table">
<tr>
<th>
#Html.DisplayNameFor(model => model.EntryId)
</th>
</tr>
#{ var item = Model.ToList(); }
#for(int i = 0; i < Model.Count(); i++)
{
<tr>
<td>
#Html.DisplayFor(modelItem => item[i].EntryId)
</td>
<td>
#using (Html.BeginForm("Ignore", "Home", FormMethod.Post))
{
#Html.HiddenFor(modelItem => item[i].EntryId)
<button type="submit" class="btn btn-danger">Ignore</button>
}
</td>
</tr>
}
</table>
And the Ignore() method on the controller:
[HttpPost]
public ActionResult Ignore(DataChecksTblDailyItems modelData)
{
using (var context = new IgnoreDailyItemsContext())
{
var query = context.DataChecksTblDailyItems
.Where(b => b.EntryId.Equals(modelData.EntryId));
foreach (var q in query)
{
q.Ignore = true;
}
context.SaveChanges();
return RedirectToAction("Index", "Home");
}
}
You're generating the form in wrong way.
#Html.HiddenFor(modelItem => item[i].EntryId)
It will generate an input hidden with item[0].EntryId, item[1].EntryId... as name/id for each row in the table, for that reason the post model definition does not match.
To solve it, set the input hidden name manually:
#Html.Hidden("EntryId", item[i].EntryId)
You need to pass IEnumerable<IEnumerable> as a parameter.
public ActionResult Ignore(IEnumerable<DataChecksTblDailyItems> modelData)
{
In my view this is what I have
#foreach (var match in Model.CommonMatches)
{
<tr>
<td>#match.StartDateTime</td>
<td>#match.EndDateTime</td>
<td>#match.AvailableAttendees.Count()</td>
<td>#Html.ActionLink("Accept", "AcceptAppointment", "Appointment", new {commonMatch = #match })</td>
</tr>
}
Model.CommonMatches is of type List<Window>
public class Window
{
public DateTime StartDateTime { get; set; }
public DateTime EndDateTime { get; set; }
public IEnumerable<DataModels.Attendee> AvailableAttendees { get; set; }
}
This is how the value is being passed from controller
[HttpGet]
public ActionResult ViewStatus(Guid appointmentId)
{
var status = new ViewStatus
{
AttendeesWhoResponded = _appointmentRepository.GetAppointmentDetails(appointmentId).Attendees.Where(a=>a.HasResponded == true).ToList(),
NotAttending = _appointmentRepository.GetAppointmentDetails(appointmentId).Attendees.Where(a=>a.HasResponded == true && a.Responses == null).ToList(),
CommonMatches = _appointmentRepository.FindCommonMatches(appointmentId)
};
return View(status);
}
ViewStatus class
public class ViewStatus
{
public ViewStatus()
{
AttendeesWhoResponded = new List<DataModels.Attendee>();
NotAttending = new List<DataModels.Attendee>();
}
public List<DataModels.Attendee> AttendeesWhoResponded { get; set; }
public List<DataModels.Attendee> NotAttending { get; set; }
public IEnumerable<Window> CommonMatches { get; set; }
}
The action in controller that ActionLink of view is calling is:
[HttpGet]
public ActionResult AcceptAppointment(Window commonMatch)
{
return Content("ac");
}
When I inspect the value of commonMatch in controller's action. I'm getting the StartDateTime and EndDateTime but i'm not getting all the value of AvailableAttendees. It is currently shown as null.
AvailableAttendees that I'm expecting is of type IEnumerable<Attendee>. Is is not possible to pass the object the way I'm passing?
What should I do, to also get all the values of AvailableAttendees in controller along with dates?
Edit 1:
<table class ="table-hover table-striped">
<thead>
<tr>
<td>Start time</td>
<td>End time</td>
<td>Number of Attendees</td>
</tr>
</thead>
#for (var count = 0; count < Model.CommonMatches.Count();count++ )
{
using (Html.BeginForm("AcceptAppointment", "Appointment", FormMethod.Post))
{
<tr>
<td>#Model.CommonMatches[count].StartDateTime</td>
<td>#Model.CommonMatches[count].EndDateTime</td>
<td>#Model.CommonMatches[count].AvailableAttendees.Count()</td>
#*<td>#Html.ActionLink("Accept", "AcceptAppointment", "Appointment", new { commonMatch = #match })</td>*#
#for(var j=0;j<Model.CommonMatches[count].AvailableAttendees.Count();j++)
{
<td>#Model.CommonMatches[count].AvailableAttendees[j].FirstName</td>//to check if the value is null or not, just a test
<td>#Html.HiddenFor(m=>Model.CommonMatches[count].AvailableAttendees[j].FirstName)</td>
<td>#Html.HiddenFor(m=>Model.CommonMatches[count].AvailableAttendees[j].LastName)</td>
<td>#Html.HiddenFor(m=>Model.CommonMatches[count].AvailableAttendees[j].Email)</td>
<td>#Html.HiddenFor(m=>Model.CommonMatches[count].AvailableAttendees[j].AttendeeId)</td>
}
<td><input type="submit" value="Accept"/></td>
</tr>
}
}
</table>
You need to post your model back, this would involve changing your controller method to this:
Controller
[HttpPost]
public ActionResult AcceptAppointment(List<Window> model)
{
return Content("ac");
}
View
You view would need a form and a submit button rather than an ActionLink. I have take the table formatting out to simplify the below.
Indexing your collections with a for loop so the model binder knows how to handle them, this is actually two loops as it is a collection within a collection. The hidden values have to be rendered too in order to be posted back (please forgive any typos).
#for(var i = 0; i < Model.CommonMatches.Count; i ++)
{
<div>
#using (Html.BeginForm("AcceptAppointment", "Appointment", FormMethod.Post)
{
#Html.HiddenFor(m => Model.CommonMatches[i].StartDateTime)
#Html.HiddenFor(m => Model.CommonMatches[i].EndDateTime)
#Model.CommonMatches[i].StartDateTime <br/>
#Model.CommonMatches[i].EndDateTime <br/>
#for(var j = 0; Model.CommonMatches[i].AvailableAttendees.Count; j++)
{
#Html.HiddenFor(m => Model.CommonMatches[i].AvailableAttendees[j].Prop1)<br/>
#Html.HiddenFor(m => Model.CommonMatches[i].AvailableAttendees[j].Prop2)<br/>
}
<input type="submit" value="Accept" />
</div>
}
}
There are plenty of things you need to taken care
<td>#Html.ActionLink("Accept", "AcceptAppointment", "Appointment", new {commonMatch = #match })</td>
Calls
[HttpGet]
public ActionResult AcceptAppointment(Window commonMatch)
{
return Content("ac");
}
Here you are navigating using a link <a href>. Basically you are issuing a get request. In get request, you can pass the data to server only via Query String. But your case, preparing a query string dynamically before navigating to url is bit more complex. But you can do it with a JavaScript like onclick=prepareHref(this);
#Html.ActionLink("Accept", "AcceptAppointment", "Appointment",
new {commonMatch = #match }, new {onclick=prepareHref(this)})
Then in Javascript
function prepareHref(obj)
{
var qsData="?StartDateTime='2014-02-25'&EndDateTime='2014-02-25'&AvailableAttendees[0].prop1=value1, etc"; // data should be obtained from other td elements
obj.href=obj.href+qsData;
}
But this is not a suggested way of doing it.
In case, if you want to open other page and show the url, better pass the id and load data again.
Option 1:
The better way could be submit the detail in the hidden field as explained by #hutchonoid.
Option 2:
or submit the details in jQuery ajax $.post method. Either way you need to use POST
#Html.ActionLink("Accept", "AcceptAppointment", "Appointment",
new {commonMatch = #match }, new {onclick=postMyData()})
function postMyData(){
var postData={};
postData.StartDateTime='';
postData.EndDateTime='';
postData.AvailableAttendees=[];
//for each AvailableAttendees prepare object
postData.AvailableAttendees[0]= {};
postData.AvailableAttendees[0].prop1=value1;
$.post('/Appointment/AcceptAppointment/',{data:postData},function(data){
});
return false;
}
[HttpPost]
public ActionResult AcceptAppointment(Window commonMatch)
{
return Content("ac");
}
I am able to display values in ListBoxFor control from controller to view. However when I move values from one listbox to another and post the view the model on controller side does not preserve the values in the listboxes.
Here is my Model
namespace MvcApplication1.Models
{
public class EmployeeClass
{
public int EmpCode { get; set; }
public string EmpName { get; set; }
}
public class EmployeeViewModel
{
public string Department { get; set; }
public list<EmployeeClass> AvailalableEmployee { get; set; }
public list<EmployeeClass> SelectedEmployee { get; set; }
public int [] AvailableEmpCodeArray { get; set; }
public int [] SelectedEmpCodeArray { get; set; }
}
}
Here is my Controller
namespace MvcApplication1.Controllers
{
public class EmployeeController : Controller
{
public ActionResult EmployeeDisplayTool()
{
EmployeeViewModel model = new EmployeeViewModel();
model.Department = "Techology"
model.AvailalableEmployee.Add(new Employee(1,"Emp1"));
model.AvailalableEmployee.Add(new Scenario(2,"Emp2"));
model.AvailalableEmployee.Add(new Scenario(3,"Emp3"));
model.SelectedEmployee.Add(new Scenario(4,"Emp4"));
model.SelectedEmployee.Add(new Scenario(5,"Emp5"));
model.SelectedEmployee.Add(new Scenario(6,"Emp6"));
return View(model);
}
[HttpPost]
public ActionResult EmployeeDisplayTool(EmployeeViewModel model)
{
//model.Department is populated as "Technology"
//All of the follownig collections are null.
//model.AvailalableEmployee is null.
//model.AvailableEmpCodeArray is null.
//model.SelectedEmployee is null.
//model.SelectedEmpCodeArray is null.
}
}
}
Here is my View and JavaScript
#using (Html.BeginForm())
{
<table id="MainTable" border="0">
<tr>
<td colspan="3">
#Html.TextBoxFor(m => Model.Department, new { id = "txtDepartment" })
</td>
</tr>
<tr>
<td>
#Html.ListBoxFor(m => Model.AvailableEmpCodeArray, new MultiSelectList(Model.AvailalableEmployee, "EmpCode", "EmpName", Model.AvailableEmpCodeArray), new { id = "lbxAvailableEmployees" })
</td>
<td>
<input type="button" id="btnSelectEmployee" value=" >> "/>
<br/>
<br/>
<input type="button" id="btnUnSelectEmployee" value=" << "/>
</td>
<td>
#Html.ListBoxFor(m => Model.SelectedEmpCodeArray, new MultiSelectList(Model.SelectedEmployee, "EmpCode", "EmpName", Model.SelectedEmpCodeArray), new { id = "lbxSelectedEmployees" })
</td>
</tr>
</table>
}
<script type="text/javascript">
$(document).ready(function()
{
$('#btnSelectEmployee').click(function(e)
{
var SelectedEmps = $('#lbxAvailableEmployees option:selected');
if (SelectedEmps.length == 0)
{
e.preventDefault();
}
$('#lbxSelectedEmployees').append($(SelectedEmps).clone());
$(SelectedEmps).remove();
e.preventDefault();
});
$('#btnUnSelectEmployee').click(function(e)
{
var SelectedEmps = $('#lbxSelectedEmployees option:selected');
if (SelectedEmps.length == 0)
{
e.preventDefault();
}
$('#lbxAvailableEmployees').append($(SelectedEmps).clone());
$(SelectedEmps).remove();
e.preventDefault();
});
});
</script>
Inside your HttpPost action you cannot expect the AvailalableEmployee and SelectedEmployee collections to be populated. That's due to the nature of how HTML works. Only the selected values are sent to the server when you submit the form. The text values of the select fields are never sent to your server when you submit the form.
So all you can hope of getting populated inside this HttpPost action is the AvailableEmpCodeArray and the SelectedEmpCodeArray collections. They will contain the values of the selected items in the corresponding select fields. Notice that I have bolded selected. This means that only if you select any items in the corresponding boxes those collections will be bound. If you don't select anything, they will remain null and that's perfectly normal.
Here's an example of the user that has selected nothing. He just used the << and >> buttons to toggle the values between the lists, but please notice how absolutely nothing is selected:
Notice how nothing is selected? So that's all you will get in your controller action - nothing.
Now please compare with the following screenshot:
Do you see the difference? Notice how the user explicitly selected the Emp2, Emp5 and Emp6 values (using the Shift key to select multiple values)? That's what you're gonna get inside your HttpPost action. The AvailableEmpCodeArray collection will contain a single element with the value of 2 and the SelectedEmpCodeArray will contain 2 elements with the values of 5 and 6.
Now to your second problem. If you intend to redisplay the same view from your HttpPost controller action the first thing you need to ensure is that you have assigned values to the AvailalableEmployee and SelectedEmployee collections. This should be done the same way you did in the HttpGet action:
model.AvailalableEmployee.Add(new Employee(1,"Emp1"));
model.AvailalableEmployee.Add(new Scenario(2,"Emp2"));
model.AvailalableEmployee.Add(new Scenario(3,"Emp3"));
model.SelectedEmployee.Add(new Scenario(4,"Emp4"));
model.SelectedEmployee.Add(new Scenario(5,"Emp5"));
model.SelectedEmployee.Add(new Scenario(6,"Emp6"));
or if those values are coming from a database you will have to re-query this database inside the HttpPost action because those values, as I already explained, will never get bound from the view.
i have two html.actionlinks:
<%= Html.ActionLink("Activate", "ActivateJob", "Management", new { selectedObject = Model.ID }, new { #class = "actions" })%>
|
<%= Html.ActionLink("Deactivate", "DeactivateJob", "Management", new { selectedObject = Model.ID }, new { #class = "actions" })%>
here is part of my table:
foreach (WPM.Logic.Job item in this.Model.Jobs)
{
Model.ID = item.ID;
%>
<tr style="background-color: #FEF0D7">
<td style="border-bottom: solid 1px #f3ad44; width: 80px;" align="center">
<%= i = i + 1 %>
</td>
<td style="border-bottom: solid 1px #f3ad44; width: 120px;">
<input type="checkbox" name="selectedObject" value="<%= Model.ID %>" />
</td>
in page source i have follow results:
<td style="border-bottom: solid 1px #f3ad44; width: 120px;">
<input type="checkbox" name="selectedObject" value="8cdc5c7a-72ba-4883-99b9-272c866c27a9" />
</td>
<td style="border-bottom: solid 1px #f3ad44; width: 120px;">
<input type="checkbox" name="selectedObject" value="fa6b304c-9eee-483f-8208-e2febd077e50" />
</td>
question is: how to get these two checkbox values in HTML.ActionLink selectedObject? I'm getting just a last result in both html.actionlinks but i need value of selected checkbox. i have many of them.
it is a action witch will be called from html.actionlink.
[HttpGet]
public ActionResult ActivateJob(Guid[] selectedObject)
{
foreach (Guid guid in selectedObject)
{
}
return View();
}
[HttpGet]
public ActionResult DeactivateJobs(Guid[] selectedObject)
{
foreach (Guid guid in selectedObject)
{
}
return View();
}
Checkboxes usually go along with HTML forms, not action links. So put them inside a form and use a submit button which will automatically send the checked values to the corresponding controller action. If you want to use links you will need to write javascript code that will subscribe for the click event of the link, extract the values of checkboxes, modify the URL this link is pointing to in order to append those values to the query string which IMHO is too much of a work for something so simple. Of course you can have multiple submit buttons with different names inside a single HTML <form> and in the corresponding controller action you will be able to get the name of the button that was clicked so that you could perform different action.
Also I would strongly recommend you using the HTTP POST or PUT verb for something that is modifying state on the server.
UPDATE:
As requested in the comments section I include an example.
As always you start with a model:
public class JobViewModel
{
public string Guid { get; set; }
public bool Selected { get; set; }
}
public class MyViewModel
{
public IEnumerable<JobViewModel> Jobs { get; set; }
}
then you move on the controller:
public class JobsController: Controller
{
public ActionResult Edit()
{
var model = new MyViewModel
{
// Obviously those will be coming from some data store
// and you could use AutoMapper to map your business entities
// to the corresponding view model
Jobs = new[]
{
new JobViewModel { ID = Guid.NewGuid() },
new JobViewModel { ID = Guid.NewGuid() },
new JobViewModel { ID = Guid.NewGuid() },
}
};
return View(model);
}
[HttpPut]
public ActionResult Update(MyViewModel model, string activate)
{
if (!string.IsNullOrEmpty(activate))
{
// the Activate button was clicked
}
else
{
// the Deactivate button was clicked
}
// TODO: model.Jobs will contain the checked values =>
// do something with them like updating a data store or something
// TODO: return some view or redirect to a success action
return View("Edit", model);
}
}
then you would have a strongly typed view in which you will use editor templates:
<% using (Html.BeginForm("Update", "Jobs")) { %>
<%= Html.HttpMethodOverride(HttpVerbs.Put) %>
<table>
<thead>
<tr>
<th>Foo bar column ...</th>
</tr>
</thead>
<tbody>
<%= Html.EditorFor(x => x.Jobs) %>
</tbody>
</table>
<input type="submit" value="Activate" name="activate" />
<input type="submit" value="Dectivate" name="deactivate" />
<% } %>
and the last part would be the corresponding editor template which will be rendered for each item in the Jobs collection (~/Views/Jobs/EditorTemplates/JobViewModel.ascx):
<%# Control
Language="C#"
Inherits="System.Web.Mvc.ViewUserControl<AppName.Models.JobViewModel>"
%>
<tr>
<%= Html.HiddenFor(x => x.ID) %>
<%= Html.CheckBoxFor(x => x.Selected) %>
</tr>
Maybe I am complete out of scope. But I have solved my problem of "Simulating a checkbox behavior with an ActionLink" in the following (dirty) way using two ASCII-Characters to visualize my two states:
Index.cshtml:
#Html.ActionLink($"{(HomeController.IsExpertMode ? "☑️" : "⬜")}Expert-Mode", "ToggleExpertMode", "Home")
HomeController.cs:
public class HomeController : Controller
{
...
public bool IsExpertMode { get; private set; }
public ActionResult ToggleExpertMode()
{
IsExpertMode = !IsExpertMode;
return RedirectToAction("Index");
}
...
}
Hopefully this can help somebody searching for a simple solution for that problem - which brought me on this page, too...
I have a view with model BlogPostViewModel:
public class BlogPostViewModel
{
public BlogPost BlogPost { get; set; }
public PostComment NewComment { get; set; }
}
This view is rendered when action method BlogPost is hit. The view displays information regarding the blog post as well as a list of comments on the blog post by iterating over Model.BlogPost.PostComments. Below that I have a form allowing users to post a new comment. This form posts to a different action AddComment.
[HttpPost]
public ActionResult AddComment([Bind(Prefix = "NewComment")] PostComment postComment)
{
postComment.Body = Server.HtmlEncode(postComment.Body);
postComment.PostedDate = DateTime.Now;
postCommentRepo.AddPostComment(postComment);
postCommentRepo.SaveChanges();
return RedirectToAction("BlogPost", new { Id = postComment.PostID });
}
My problem is with validation. How do I validate this form? The model of the view was actually BlogPostViewModel. I'm new to validation and am confused. The form uses the strongly-typed helpers to bind to the NewComment property of BlogPostViewModel and I included the validation helpers as well.
#using (Html.BeginForm("AddComment", "Blog")
{
<div class="formTitle">Add Comment</div>
<div>
#Html.HiddenFor(x => x.NewComment.PostID) #* This property is populated in the action method for the page. *#
<table>
<tr>
<td>
Name:
</td>
<td>
#Html.TextBoxFor(x => x.NewComment.Author)
</td>
<td>
#Html.ValidationMessageFor(x => x.NewComment.Author)
</td>
</tr>
<tr>
<td>
Email:
</td>
<td>
#Html.TextBoxFor(x => x.NewComment.Email)
</td>
<td>
#Html.ValidationMessageFor(x => x.NewComment.Email)
</td>
</tr>
<tr>
<td>
Website:
</td>
<td>
#Html.TextBoxFor(x => x.NewComment.Website)
</td>
<td>
#Html.ValidationMessageFor(x => x.NewComment.Website)
</td>
</tr>
<tr>
<td>
Body:
</td>
<td>
#Html.TextAreaFor(x => x.NewComment.Body)
</td>
<td>
#Html.ValidationMessageFor(x => x.NewComment.Body)
</td>
</tr>
<tr>
<td>
</td>
<td>
<input type="submit" value="Add Comment" />
</td>
</tr>
</table>
</div>
}
How in the AddComment action method do I implement validation? When I detect Model.IsValid == false then what? What do I return? This action method is only binding to the PostComment property of the pages initial BlogPostViewModel object because I don't care about any other properties on that model.
You need to repopulate the model and send to view. However, you don't need to do this by hand, you can use action filters.
see:
http://weblogs.asp.net/rashid/archive/2009/04/01/asp-net-mvc-best-practices-part-1.aspx#prg
Specifically:
public abstract class ModelStateTempDataTransfer : ActionFilterAttribute
{
protected static readonly string Key = typeof(ModelStateTempDataTransfer).FullName;
}
public class ExportModelStateToTempData : ModelStateTempDataTransfer
{
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
//Only export when ModelState is not valid
if (!filterContext.Controller.ViewData.ModelState.IsValid)
{
//Export if we are redirecting
if ((filterContext.Result is RedirectResult) || (filterContext.Result is RedirectToRouteResult))
{
filterContext.Controller.TempData[Key] = filterContext.Controller.ViewData.ModelState;
}
}
base.OnActionExecuted(filterContext);
}
}
public class ImportModelStateFromTempData : ModelStateTempDataTransfer
{
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
ModelStateDictionary modelState = filterContext.Controller.TempData[Key] as ModelStateDictionary;
if (modelState != null)
{
//Only Import if we are viewing
if (filterContext.Result is ViewResult)
{
filterContext.Controller.ViewData.ModelState.Merge(modelState);
}
else
{
//Otherwise remove it.
filterContext.Controller.TempData.Remove(Key);
}
}
base.OnActionExecuted(filterContext);
}
}
Usage:
[AcceptVerbs(HttpVerbs.Get), ImportModelStateFromTempData]
public ActionResult Index(YourModel stuff)
{
return View();
}
[AcceptVerbs(HttpVerbs.Post), ExportModelStateToTempData]
public ActionResult Submit(YourModel stuff)
{
if (ModelState.IsValid)
{
try
{
//save
}
catch (Exception e)
{
ModelState.AddModelError(ModelStateException, e);
}
}
return RedirectToAction("Index");
}
In your AddComment ActionResult, do this:
if(ModelState.IsValid)
{
// Insert new comment
..
..
// Redirect to a different view
}
// Something is wrong, return to the same view with the model & errors
var postModel = new BlogPostViewModel { PostComment = postComment };
return View(postModel);
After much time spent I have realized that I have to repopulate the view model and render the correct view, passing in the fully-populated model. Kind of a pain but at least I understand what's going on.