I'm using MVC 4 and Entity Framework to develop an web app. I have a table which contains persons. There is also an Edit button which invokes a modal window and thanks to it, the user can edit a person. I'm using a partial view to do so.
My question is : in my action, I return a View but I just want that when I click on the Save button, the modal window disappear and my table is updated. Any idea?
The actions :
[HttpGet]
public ActionResult EditPerson(long id)
{
var person = db.Persons.Single(p => p.Id_Person == id);
ViewBag.Id_ProductPackageCategory = new SelectList(db.ProductPackageCategories, "Id_ProductPackageCategory", "Name", person.Id_ProductPackageCategory);
return PartialView("_EditPerson", person);
}
[HttpPost]
public ActionResult EditPerson(Person person)
{
ViewBag.Id_ProductPackageCategory = new SelectList(db.ProductPackageCategories, "Id_ProductPackageCategory", "Name", person.Id_ProductPackageCategory);
if (ModelState.IsValid)
{
ModelStateDictionary errorDictionary = Validator.isValid(person);
if (errorDictionary.Count > 0)
{
ModelState.Merge(errorDictionary);
return View(person);
}
db.Persons.Attach(person);
db.ObjectStateManager.ChangeObjectState(person, EntityState.Modified);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(person);
}
The partial view (actually, the modal window) :
#model BuSIMaterial.Models.Person
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h3 id="myModalLabel">Edit</h3>
</div>
<div>
#using (Ajax.BeginForm("EditPerson", "Person", FormMethod.Post,
new AjaxOptions
{
InsertionMode = InsertionMode.Replace,
HttpMethod = "POST",
UpdateTargetId = "table"
}))
{
#Html.ValidationSummary()
#Html.AntiForgeryToken()
#Html.HiddenFor(model => model.Id_Person)
<div class="modal-body">
<div class="editor-label">
First name :
</div>
<div class="editor-field">
#Html.TextBoxFor(model => model.FirstName, new { maxlength = 50 })
#Html.ValidationMessageFor(model => model.FirstName)
</div>
<div class="editor-label">
Last name :
</div>
<div class="editor-field">
#Html.TextBoxFor(model => model.LastName, new { maxlength = 50 })
#Html.ValidationMessageFor(model => model.LastName)
</div>
<div class="editor-label">
National number :
</div>
<div class="editor-field">
#Html.EditorFor(model => model.NumNat, new { maxlength = 11 })
#Html.ValidationMessageFor(model => model.NumNat)
</div>
<div class="editor-label">
Start date :
</div>
<div class="editor-field">
#Html.TextBoxFor(model => model.StartDate, new { #class = "datepicker", #Value = Model.StartDate.ToString("yyyy/MM/dd") })
#Html.ValidationMessageFor(model => model.StartDate)
</div>
<div class="editor-label">
End date :
</div>
<div class="editor-field">
#if (Model.EndDate.HasValue)
{
#Html.TextBoxFor(model => model.EndDate, new { #class = "datepicker", #Value = Model.EndDate.Value.ToString("yyyy/MM/dd") })
#Html.ValidationMessageFor(model => model.EndDate)
}
else
{
#Html.TextBoxFor(model => model.EndDate, new { #class = "datepicker" })
#Html.ValidationMessageFor(model => model.EndDate)
}
</div>
<div class="editor-label">
Distance House - Work (km) :
</div>
<div class="editor-field">
#Html.EditorFor(model => model.HouseToWorkKilometers)
#Html.ValidationMessageFor(model => model.HouseToWorkKilometers)
</div>
<div class="editor-label">
Category :
</div>
<div class="editor-field">
#Html.DropDownList("Id_ProductPackageCategory", "Choose one ...")
#Html.ValidationMessageFor(model => model.Id_ProductPackageCategory) <a href="../ProductPackageCategory/Create">
Add a new category?</a>
</div>
<div class="editor-label">
Upgrade? :
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Upgrade)
#Html.ValidationMessageFor(model => model.Upgrade)
</div>
</div>
<div class="modal-footer">
<button class="btn btn-inverse" type="submit">Save</button>
</div>
}
</div>
And my script which invokes the modal :
$(document).ready(function () {
$('.edit-person').click(function () {
var id = $(this).data("id");
var url = '/Person/EditPerson/'+id;
$.get(url, function(data) {
$('#edit-person-container').html(data);
$('#edit-person').modal('show');
});
});
});
a couple of changes would need to be made but here is what I would do this case
1) change the EditPerson post method from an actionresult to a JsonResult
[HttpPost]
public JsonResult EditPerson(Person person)
{
// code here to save person
bool success = true; // somehow determine if the save was successful
string msg = ""; // error message is needed?
return JsonResult(new {success,msg, person});
}
2) Add a javascript function to close the modal
function closeModal(response){
// the response is the Json Result sent back from the action
if (response.success === true){
// actually got a true response back
}
$('#edit-person').modal('show'); // or similar code
}
3) Then Update your Ajax call to perform code on when its successful
#using (Ajax.BeginForm("EditPerson", "Person", FormMethod.Post,
new AjaxOptions
{
InsertionMode = InsertionMode.Replace,
HttpMethod = "POST",
UpdateTargetId = "table",
OnSuccess = "closeModal(response);" // or javascript code to close the modal, you can also
}))
{ ...
a couple of tips
I don't like the MVC ajax helpers. I think they are bloated and I feel there are other frameworks better at it. Thats my opinion. To each their own though. I would prefer to use the jQuery ajax library myself. I think its easier to use but again, its up to you.
The OnSuccess means on "Server Success" not save success. so be careful.
Disclaimer: I wrote this while tired and so it might not be 100% let me know of any issues.
Good luck
In your POST action you could return the following:
return Json(new { error = false, message = "Person edited." });
In the AjaxOptions in Ajax.BeginForm add this:
OnSuccess = "Modal.onAjaxSuccess"
Then somewhere, say in script.js:
function onAjaxSuccess(data, status, xhr) {
if (data.error) {
$.notify({
type: "error",
text: data.message
});
}
else {
$('.modal').modal('hide');
}
}
This will close the window, but I'm still unable to get the darkened screen associated with Bootstrap modals to go away, it also won't update the DIV using AJAX - maybe Darin can shed some light?
If you're still wondering how to do this, this is how:
You have a GET action that returns the list of people:
[HttpGet]
public ActionResult People()
{
return PartialView("_ListOfPeoplePartial");
}
Then have a JS function that fires when you click save (i.e. btn-save-new-person) on the modal that allows you to create new people:
$(function () {
$(document.body).on('click', '#btn-save-new-person', function (e) {
$('#create-new-person-modal').modal('hide');
$('body').removeClass('modal-open');
$('.modal-backdrop').remove();
var url = "/Home/People";
$.get(url, function (data){
$('#list-of-people').html(data);
});
});
});
in my action, I return a View but I just want that when I click on the Save button, the modal window disappear and my table is updated. Any idea?
Instead of returning a view from this controller action you could return a partial view containing the table. And then in the success callback of the AJAX call simply update the corresponding container.
Related
I stumbled across a better way to display my comment form within the post window with <p id="createPostButton">#Html.Action("Create", "Comment", new { id = Model.PostId })</p> (I was originally just trying to create a button to take you to the comment page sending the post id). However, I now get an error when I try to pass back the post/details/{id} view in the CommentController. It keeps trying to look in comment or shared folders passing either post OR details/{id} instead of post/details/{id}.
Post/Details Razor HTML file:
#model FantaC.Models.Post
#{
ViewBag.Title = #Html.DisplayFor(model => model.PostName);
}
<h2>#Html.DisplayFor(model => model.PostName)</h2>
<div class="row">
<div class="col-md-8 whiteBorder scroll">
<div class="postName">
<h4>Written by: #Html.DisplayFor(model => model.UserName)</h4>
<img src="#Html.DisplayFor(model => model.PostImage)" />
</div>
<div class="postContent">
<p>#Html.DisplayFor(model => model.PostContent)</p>
</div>
</div>
<div class="col-md-4 whiteBorder scroll">
<h4>Comments</h4>
#foreach (var comment in Model.PostComments)
{
<h5>#Html.DisplayFor(modelItem => comment.UserName)</h5>
<h5>#Html.DisplayFor(modelItem => comment.CommentSubject)</h5>
<p>#Html.DisplayFor(modelItem => comment.CommentContent)</p>
<p>
#Html.ActionLink("Edit", "../Comment/Edit", new { id = comment.CommentId }) |
#Html.ActionLink("Details", "../Comment/Details", new { id = comment.CommentId }) |
#Html.ActionLink("Delete", "../Comment/Delete", new { id = comment.CommentId })
</p>
}
<p id="createPostButton">#Html.Action("Create", "Comment", new { id = Model.PostId })</p> <!--**********This is the line that is important-->
#*#=Html.RenderAction("Create", "Comments", new { postId = Model.PostId });*#
#*#Html.Partial("Comments")*#
</div>
</div>
<p>
#*#Html.ActionLink("Add a Comment", "Create", "Comment")*#
#Html.ActionLink("Comment", "Create", "Comment", new { id = Model.PostId }, null) |
#Html.ActionLink("Back to List", "Index")
The Comment/Create Razor HTML file that is getting pulled in by the Html.Action:
#model FantaC.Models.Comment
#{
Layout = null;
}
#{
ViewBag.Title = "Create";
}
<h2>Create</h2>
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>Comment</h4>
<hr />
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
<div class="form-group">
#Html.LabelFor(model => model.CommentSubject, htmlAttributes: new { #class = "control-label col-md-10 displayBlock" })
<div class="col-md-12">
#Html.EditorFor(model => model.CommentSubject, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.CommentSubject, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.CommentContent, htmlAttributes: new { #class = "control-label col-md-10 displayBlock" })
<div class="col-md-12">
#Html.EditorFor(model => model.CommentContent, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.CommentContent, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
</div>
}
#*<div>
#Html.ActionLink("Back to List", "Index")
</div>*#
#section Scripts {
#Scripts.Render("~/bundles/jqueryval")
}
The important section from the CommentController:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(string id, [Bind(Include = "CommentSubject,CommentContent")] Comment model)
{
if (ModelState.IsValid)
{
ApplicationUser user = System.Web.HttpContext.Current.GetOwinContext().GetUserManager<ApplicationUserManager>().FindById(System.Web.HttpContext.Current.User.Identity.GetUserId());
var commentId = (23817 + db.Comment.Count()).ToString().PadLeft(10, '0');
var comment = new Comment
{
CommentId = commentId,
PostId = id,
UserName = user.UserName,
PostTime = DateTime.Now,
CommentSubject = model.CommentSubject,
CommentContent = model.CommentContent
};
db.Comment.Add(comment);
db.SaveChanges();
return View("Details/" + id, "Post");
}
return View(model);
}
I also tried return View("../post/details/" + id); to no avail. How can I get back up to the post view url (post/details/{id} from the CommentController?
As a side note, I had it almost working by taking out the returns and making the method void, but after clicking the submit comment button the whole comment/create form would disappear. I would be fine going back to that way of doing things if anyone knows a way to make the form stay after clicking the create comment button.
Thanks for any help! :)
Edit:
I forgot to mention that I tried this too. It returns an error that looks like this:
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.
Exception Details: System.InvalidOperationException: Child actions are not allowed to perform redirect actions.
Source Error:
Line 32: }
Line 33:
Line 34: #Html.Action("Create", "Comment", new { id = Model.PostId })
Line 35:
You should follow the PRG (POST-REDIRECT-GET) pattern. After saving the comment, you should redirect to the post details page.
You may use the RedirectToAction method to return a RedirectResponse back to the browser which will make a new GET request to the post details action method.
So replace
return View("Details/" + id, "Post");
with
return RedirectToAction("Details" , "Post", new {id=id});
So my partial view looks like -
#model Example.Models.ForgotPasswordViewModel
#Html.ValidationSummary("", new { #class = "text-danger" })
<div class="form-group">
#Html.LabelFor(m => m.Email, new { #class = "col-md-2 control-label" })
<div class="col-md-10">
#Html.TextBoxFor(m => m.Email, new { #class = "form-control" })
</div>
</div>
<div style="font-size: 16px; text-align:center; margin-top:10px;">
#Html.ActionLink("Email Link", "ForgotPassword", "Account")
</div>
as part of asp.net's out of the box ForgotPassword view, but I turned it into a partial view. The original view was wrapped in "Html.BeginForm(..", but I obviously can't do that. (It results in a form inside a form)
How do I get it to call the Account controller's ForgotPassword method that expects a model by passing the declared #model?
Html generated by #Html.TextBoxFor(m => m.Email, new { #class = "form-control" }) :
<input class="form-control" id="Email" name="Email" type="text" value="" />
signature for your AccountController.ForgetPassword(..) :
//
// POST: /Account/ForgotPassword
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ForgotPassword(ForgotPasswordViewModel model)
{
if (ModelState.IsValid)
{
var user = await UserManager.FindByNameAsync(model.Email);
if (user == null || !(await UserManager.IsEmailConfirmedAsync(user.Id)))
{
// Don't reveal that the user does not exist or is not confirmed
return View("ForgotPasswordConfirmation");
}
// For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=320771
// Send an email with this link
string code = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
var callbackUrl = Url.Action(
"ResetPassword", "Account",
new { userId = user.Id, code = code },
protocol: Request.Url.Scheme);
await UserManager.SendEmailAsync(user.Id, "Reset Password", "Please reset your password by clicking here");
return View("ForgotPasswordConfirmation");
}
// If we got this far, something failed, redisplay form
return RedirectToAction("Index", "Home");
}
Also as Erik pointed out, need to make sure fields are uniquely identified within the partial view. The post had Email twice.
#ErikPhilips How would I use jQuery to change the form's destination? :)
I would change the Action link to a button:
#Html.ValidationSummary("", new { #class = "text-danger" })
<div class="form-group">
#Html.LabelFor(m => m.Email, new { #class = "col-md-2 control-label" })
<div class="col-md-10">
#Html.TextBoxFor(m => m.Email, new { #class = "form-control" })
</div>
</div>
<div style="font-size: 16px; text-align:center; margin-top:10px;">
<input type="button"
value="Email Link"
class="change-form-url"
data-url="#Url.Action("ForgotPassword", "Account")"/>
</div>
Jquery:
$(document).ready(funciton()
{
$('.change-form-url').on('click', function()
{
var url = $(this).data('url');
var form = $(this).closest('form');
form.prop('action', url);
form.submit();
});
});
I have not tested this, it should be very close to what you want.
BeingForm has some overloads:
#using (Html.BeginForm("ActionName","Controller"))
{
#Html.TextBox("Name");
#Html.Password("Password");
<input type="submit" value="Sign In">
}
Add the action name and controller name into the beginForm call and you can override where the Post is by default.
I suggest you call your partial view outside the main form so you will have two forms on one page (not nested). Then you can point the forgotpassword model to the correct controller method.
I have a popup dialog inside the partial view. When a user clicks "Create" button, a popup dialog is shown, inside the popup dialog is a form where user types information and the information is then save into the database. I got everything working and didn't get any errors but the data I typed didn't get saved into the database.
Please help, I am new to programming, Thank you.
Index page:
<li>#Html.Partial("_Create")</li>
In my _Create partial view, I scaffolded the partial view using the Create template
#model test.Models.Question
<li>Create</li>
<div class="dialog-form-create-question" title="Create a question">
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
<div class="form-group">
#Html.LabelFor(model => model.Title, htmlAttributes: new { #class = "control-label col-md-1" })
<div class="col-md-11">
#Html.EditorFor(model => model.Title, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.Title, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-8">
<input type="submit" value="Submit" class="btn btn-primary"/>
</div>
</div>
</div>
}
</div>
My JQuery scripts for the form dialog nested inside the partial view:
$(document).ready(function () {
$(".dialog-form-create-question").dialog({
autoOpen: false,
height: 510,
width: 800,
modal: true,
draggable: false,
resizable: false
});
$('.create-question').click(function () {
$('.dialog-form-create-question').dialog("open");
});
$('.cancel-button').click(function () {
$('.dialog-form-create-question').dialog("close");
});
});
In my Home controller, I changed the ActionResult Create(from scaffolding) to _Create:
// GET: Home/Create
public ActionResult _Create()
{
return View();
}
// POST: Home/Create
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult _Create([Bind(Include = "Id,Title")] Question question)
{
if (ModelState.IsValid)
{
db.Questions.Add(question);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(question);
}
please try changing
#using (Html.BeginForm())
{
}
to
#using (Html.BeginForm("_Create","Home",FormMethod.Post))
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
<div class="form-group">
#Html.LabelFor(model => model.Title, htmlAttributes: new { #class = "control-label col-md-1" })
<div class="col-md-11">
#Html.EditorFor(model => model.Title, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.Title, "", new { #class = "text-danger" })
<input type="submit" value="submit"/>
</div>
</div>
</div>
}
and see that your debugger strikes to _Create (post) method in your home controller with the values
I have a problem with a variable that take the value undefined.
In the file Create.cshtml i have:
#using (Html.BeginForm("Create", "Enrollment", FormMethod.Post, new
{
id = "YearCourseFormId",
data_courseListAction = #Url.Action("CourseList")
})) {
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<fieldset>
<legend>Enrollment</legend>
<div class="editor-label">
#Html.LabelFor(model => model.StudentId, "Student")
</div>
<div class="editor-field">
#Html.DropDownList("StudentId", ViewBag.Student as SelectList, String.Empty, new { #class = "chosen-select", data_placeholder = "Please Select a Student...",style="width:350px;" })
#Html.ValidationMessageFor(model => model.StudentId)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.Course.YearId, "Year")
</div>
<div class="editor-field">
#Html.DropDownList("YearId", ViewBag.Year as SelectList, String.Empty, new { #class = "chosen-select", data_placeholder = "Please Select a Year...",style="width:250px;" })
#Html.ValidationMessageFor(model => model.Course.YearId)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.CourseId, "Course")
</div>
<div class="editor-field">
#Html.DropDownList("CourseId", String.Empty)
#Html.ValidationMessageFor(model => model.CourseId)
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
In the Jquery File i have:
$(function () {
$('#YearId').change(function () {
var URL = $('#YearCourseFormId').data('courseListAction');
alert(URL);
$.getJSON(URL + '/' + $('#YearId').val(), function (data) {
var selectList = $("#CourseId");
selectList.empty();
var option = $('<option>');
selectList.append(option);
$.each(data, function (index, optionData) {
option = $('<option>').text(optionData.Text).val(optionData.Value);
selectList.append(option);
});
});
});
});
In this part alert(URL);the result is undefined.
In the EnrollmentController.cs i have:
public ActionResult CourseList(string ID)
{
int Year = int.Parse(ID);
var courses = from s in db.Courses
where s.YearId == Year
select s;
if (HttpContext.Request.IsAjaxRequest())
return Json(new SelectList(
courses.ToArray(),
"Id",
"CourseName")
, JsonRequestBehavior.AllowGet);
return RedirectToAction("Index");
}
But this method is not called due to error.
What is the problem?
Your form is rendered like this:
<form action="/Enrollment/Create" data-courselistaction="..." id="YearCourseFormId" method="post">
</form>
You can access using any of these:
$('#YearCourseFormId').data('courselistaction');
$('#YearCourseFormId').attr('data-courselistaction');
$('#YearCourseFormId').attr('data-courseListAction');
Given T.J. Crowder's comments about quirks with data(), it would probably be safer to go with one of the attr() solutions.
Edit - The plot thickens..
Chrome inspecter:
Source:
I'm not sure what the implications of that are, but my javascript snippet did work to get the value.
I have a strange problem where the unobtrusive validations inside a partial view (which is being rendered by a Ajax.ActionLink) does not work.
This is the Partial View:
#model MyWeb.Models.PersonSkill
#using (Ajax.BeginForm("EditSkill", null, new AjaxOptions { UpdateTargetId = "skills" }, new { id = "EditSkillForm" }))
{
#Html.HiddenFor(model => model.Id)
<div class="editor-label">
#Html.LabelFor(model => model.Name)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Name)
#Html.ValidationMessageFor(model => model.Name)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.YearsOfExperience)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.YearsOfExperience)
#Html.ValidationMessageFor(model => model.YearsOfExperience)
</div>
<p>
<input type="submit" value="Save"/>
</p>
}
And this is called by:
#Ajax.ActionLink("Edit", "LoadEditSkill", new { id = item.Id }, new AjaxOptions() { UpdateTargetId = "editSkillDialog" }, new { #class = "editSkill" })
The view is rendered fine. It posts back data to the server, but it just doesn't validate.
Many thanks to Joe Tuskan for pointing me at the right direction.
I fixed the issue by adding an OnSuccess subscriber to my call:
#Ajax.ActionLink("Edit", "LoadEditSkill", new { id = item.Id }, new AjaxOptions() { UpdateTargetId = "editSkillDialog", OnSuccess = "onSuccess" }, new { #class = "editSkill" })
and adding calling parse() like explained here:
var onSuccess = function () {
// enable unobtrusive validation for the contents
// that was injected into the <div id="result"></div> node
$.validator.unobtrusive.parse($("#editSkillDialog"));
};
Always make sure you included the necessary references to
jquery.validate.min.js and jquery.validate.unobtrusive.min.js and of course the main jquery library, this is usually the most common problem, and as i can see in your cshtml they are not present in your page either.