I'd so appreciate if someone could advise.
I have an Ajax form:
#using (Ajax.BeginForm("Edit", null, new AjaxOptions() {
UpdateTargetId = updateRegion,
InsertionMode = InsertionMode.Replace,
OnFailure = "formFailure" }))
{}
Its UpdateTargetId differs based on the current User role:
#{
if (curUser.IsInputer)
{
updateRegion = "content";
}
else if (curUser.IsAuthorizer)
{
updateRegion = "mainPane";
}
}
If the modelstate is invalid I'd like to return view in mainPane always:
<script>
function formFailure(result)
{
$("#mainPane").html(result.responseText);
}
</script>
However onFailure is not called when ModelState is invalid. For this, I set error code in controller:
public ActionResult Edit(ContractModel entity)
{
if(ModelState.IsValid)
{
if(curUser.isInputer) { return RedirectToAction("SomeAction");}
if(curUser.Authorizer) { return RedirectToAction("OtherAction");}
}
Response.StatusCode = 500;//internal server error to fire OnFailure of form
return PartialView(entity);
}
Then I get the desired result, i.e. I see the model with its errors in mainPane div and internal server error in browser console. However, it works this way when I run the application locally, when I publish and run it on the server, I see the error 500 Internal server error, instead of the partial view. Is there any workaround?
EDIT:
As an option I tried to check Model for errors in form OnSuccess handler:
var isValid = #Html.Raw(Json.Encode(ViewData.ModelState.IsValid));
if (!isValid) {
$("#mainPane").html(result.responseText);
}
But I still get the view in "content" div. Why is this happening?
Here is an option:
Dont use a 500 status error. That is for reporting something going very wrong with your application.
That is not what has happened here, you simply have a form validation error.
Update your server side code to this:
public ActionResult Edit(ContractModel entity)
{
if(ModelState.IsValid)
{
if(curUser.isInputer) { return RedirectToAction("SomeAction");}
if(curUser.Authorizer) { return RedirectToAction("OtherAction");}
}
if(!ModelState.IsValid)
{
entity.FormError = true; // you will need to add this attribute to your model
}
return PartialView(entity);
}
Then in your partial view, put something like this:
if(Model.FormError)
{
#Html.HiddenFor(m => m.FormError)
}
Change your form to handle the OnComplete event instead of OnFailure event. This is because your ajax post is not failing. Its returning successfully and reporting there was a validation error with the forms input:
#using (Ajax.BeginForm("Edit", null, new AjaxOptions() { OnSuccess = "handleFormResponse" }))
{}
Change your handler script to something like this. This will check the response to see if contains a form error
<script>
function handleFormResponse(result)
{
$(result).find("#FormError").length > 0)
{
$("#mainPane").html(result);
}
else
{
$("##updateRegion").html(result);
}
}
</script>
This is likely happening because the server is interpreting the 500 as an unhanded error and returning the default 500 error since your customErrors setting in the web config likely gets flipped to false when you publish the application in release mode.
Edit: Removed mention of using another status code as it caused more confusion and does not solve the problem.
Related
I have a form in a view that is marked with #html.beginForm. This form consists of dropdowns, text boxes and a button. The drop downs are populated dynamically through ajax call. ie. selection of one value from the drop down triggers an ajax call and dynamically populates all the drop down boxes.
This form has a button to post and bring up another form. The problem that I am having is that when an exception happens in the controller, how do i show the error message to the user and preserve the form values filled out by the user?
Here is my view:
<div class="col-lg-6">
<div class="panel panel-default animated zoomInDown" style="padding-top:25px">
<div class="fa fa-spinner fa-spin" id="dashSpinner" style="display:none;text-align:center;"></div>
#using (Html.BeginForm("Create", "MyEntities", FormMethod.Post, new { id = "createEntityForm" }))
{
/*Form consists of dropdowns and text boxes*/
}
</div>
Here is my controller:
[HttpPost]
public ActionResult Create([Bind(Include = Entities)] EntityModel model)
{
try
{
//If everything goes well, redirect to another form
return RedirectToAction("AnotherForm", "Event", new { id = eventid });
}
}
catch (Exception e)
{
//Catch exception and show it to the user?
log.Error(e);
model.Error = e.Message;
}
return View(model);
}
Here is my ajax call to show error message to the user
$("#createEntityForm").on("submit", function (e) {
$("#dashSpinner").show();
debugger;
$.ajax({
url: this.action,
type: this.method,
data: $(this).serialize(),
//success: function (response) {
// debugger;
//},
error: function (response) {
debugger;
$("#dashSpinner").hide();
swal({
title: "Error",
text: "You cannot take this type of action on this event.",
type: "error",
showConfirmButton: true
});
}
});
$("#dashSpinner").hide();
});
The thing that triggers the Error event is getting a non 2xx status code.
Alter your ActionMethod to use a non-2xx status code. You can do this by using Response.StatusCode = 500;.
You are also always returning a view - if you want to show just an error message it may be easier to return a JsonResult and then update your error handling to just show this error. In that case your ActionMethod catch statement could become:
catch (Exception e)
{
log.Error(e);
Response.StatusCode = 500;
Response.TrySkipIisCustomErrors = true;
return Json(new { message = e.Message } );
}
You then need to update your JQuery error handler to show the message as currently it will always show the same error ("You cannot take this type of action on this event."). To do this you need to output the message sent as part of the JSON payload.
error: function (response) {
$("#dashSpinner").hide();
swal({
title: "Error",
text: response.responseJSON.message,
type: "error",
showConfirmButton: true
});
}
The jQuery.ajax error callback is executed when server response has status code different than 200.
You should return response using HttpStatusCodeResult (msdn) or setting Response.StatusCode (msdn) in catch block.
At the Controller
[HttpPost]
public ActionResult Create([Bind(Include = Entities)] EntityModel model)
{
//If something goes wrong return to the form with the errors.
if(!ModelState.isValid()) return Create(model);
//If everything goes well, redirect to another form
return RedirectToAction("AnotherForm", "Event", new { id =
return View(model);
}
You can check too unobtrusive for jquery at the client side. https://exceptionnotfound.net/asp-net-mvc-demystified-unobtrusive-validation/
This question continues from a previous question I posted at the link below:
Passing a filename to a view
In summary: I am trying to delete a file. I now have all the necessary functionality working thanks to the answer at the above post. However, occasionally, if the file was in use (which would happen if I downloaded the file prior to deleting for example), I would get an IOException.
I want to handle this exception and display a message back to the user when this happens.
I tried debugging the jQuery code, but whenever I put a breakpoint on it, I start skipping through 1000's of lines of jQuery libraries. So as a quick alternative I just put alerts everywhere.
I discovered that most of this code is not being executed - so I put comments beside where I think I should be seeing messages but am not. So as a result I am unable to debug or figure out what the code is supposed to be doing.
So my first question is how to get the exception showing. It seems that even though this is Ajax, the whole page still refreshes, which to me is not the expected behaviour for Ajax calls (so if an error is showing its maybe lost when the page refreshes). However, with all the alerts, I should see the error somewhere, but I am not. I have purposefully altered the working code to always throw an exception for now. Again, the delete functionality works, its the error reporting that fails.
My next question is to have a brief explanation of why each section of the code (where highlighted) is needed as I don't know why those sections exist and I cant figure it out because I cant debug into or show alerts for them.
Below is the index view and the code in question:
#model IEnumerable<FileInfo>
#{
ViewBag.Title = "File List";
}
<h2>Index</h2>
<p>#Html.ActionLink("Upload", "Upload")</p>
<p>#Html.ValidationSummary(true, "", new { #class = "text-danger" })</p>
<p>#Html.ValidationMessage("Name", new { #class = "text-danger" })</p>
<span class="message text-danger"></span>
<table class="table">
<tr>
<th>File Name</th>
<th>Actions</th>
</tr>
#foreach (FileInfo file in Model)
{
<tr>
<td>#file.Name</td>
<td>
<form class="deleteform">
#Html.AntiForgeryToken()
<input type="hidden" name="fileName" value="#file.Name" />
<input type="submit" value="delete" />
</form>
</td>
<td>#Html.ActionLink("Download", "Download", new { fileName = file.Name })</td>
</tr>
}
</table>
#section Scripts {
#Scripts.Render("~/bundles/jqueryval")
<script type="text/javascript">
$(document).ready()
{
var url = '#Url.Action("Delete", "FileManagement")';
$('.deleteform').submit(function ()
{
alert(".deleteform.submit entered...");//This alert shows
return confirm("are you sure ...");//This alert shows
var formData = $(this).serialize();
alert(formData);//This does NOT show.
var row = $(this).closest('tr');
alert(row);//This does NOT show.
$.post(url, formData, function (response)
{
alert("$.post() entered...");//This does NOT show.
alert(response);//This does NOT show.
if (response)
{
alert("response true");//This does NOT show.
alert(response);//This does NOT show.
//row.remove(); //This code actually works even though the alert above does not show.
} else
{
alert("response false");//This does NOT show. - I dont know what this section of code is for.
//alert("Error 1 - display message");
// Oops - display message?
}
alert("$.post() finished...");//This does NOT show.
}).fail(function (response)
{
alert("$.fail() entered...");//This does NOT show. - I dont know what this section of code is for.
alert("Error 2 - display another message");//This does NOT show.
// Oops
alert("$.fail() finished...");//This does NOT show.
});
return false; // cancel the default submit
alert(".deleteform.submit finished...");//This does NOT show.
});
}
</script>
}
Below is the controller for this view and the delete actions:
public ActionResult Index()
{
DirectoryInfo dirInfo = new DirectoryInfo(Server.MapPath("~/UserFiles"));
List<FileInfo> files = dirInfo.GetFiles().ToList();
return View(files);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Delete(string fileName)
{
var path = Path.Combine(Server.MapPath("~/UserFiles"), fileName);
if (System.IO.File.Exists(path))
{
try
{
//System.IO.File.Delete(path);
throw new IOException("Hello - Test Message...");
}
catch (IOException e)
{
Response.StatusCode = 500;
Response.StatusDescription = e.Message;
//ModelState.AddModelError("Name", e.Message);
}
}
else
return Json(null);
//return HttpNotFound();
return Json(true);
//return RedirectToAction("Index");
}
I would appreciate any help on this.
You cannot use return confirm("are you sure ..."); in the .submit() handler because it either returns false which cancels everything, or it returns true in which case you will be making a normal submit. In either case, it exits the function.
You need to change the script to
$('.deleteform').submit(function () {
if (confirm("....") {
// make you ajax call here
}
return false;
});
You also need to modify your controller code. Currently your last line (return Json(true);) can never be executed, and the code in the if block is always returning an error, so will always go to the .fail() function. Generally, you should not return the specific details of exceptions your code throws (this just exposes it to malicious users) and it is better to return a more general error, or return null and hard code the error message in the script. There are various ways you can handle this, including
try
{
// delete the file
return Json(true); // to indicate success
}
catch (.... )
{
return Json(null); // indicate failure
}
which in the script means
if (response) { // this will be executed if you returned true
... // delete the row
} else { // this will be executed if you returned null
... // display a message to the user
}
and the .fail() function will be executed if an exception is throw on the server which you have not caught.
Alternatively you could return on object with properties for Success and Message which gives you a bit more control over the message, for example
return Json(new { Success = true });
// or
return Json(new { Success = false, Message = "The file does not exist"});
// or
retrn Json(new { Success = false, Mesage = "The file is in use, please try again late" });
and the in the script
$.post(url, formData, function (response) {
if (response.Success == 'False') {
var message = response.Message; // display it
I am new in MVC .net. I am not able to call view from controller. I have debug the flow. It goes to view successfully but doesn't show view on screen.
Controller name: Downloads
Action name: MakePayment
Redirect view: Success //success is view of View/Downloads/Success.cshtml
Code: DownloadsController
[HttpPost]
public ActionResult MakePayment(Downloads CCM)
{
if (true)
{
return View("Success");
}
else
{
return View("Failure");
}
}
View
#{
ViewBag.Title = "Success";
}
<h2>Your transaction has been completed successfully.</h2>
Method that I use to call the ActionResult MakePayment. I had use Ajax here because I wanted to call javascript function before form get submit.
View: Index.cshtml
#using (Ajax.BeginForm("MakePayment", "Downloads", new AjaxOptions { HttpMethod = "POST", InsertionMode = InsertionMode.Replace, OnBegin = "payByCreditCard" }))
{
//submit button
}
According to the return View("Success");, it should call the view. In fact when I debug the flow, it goes to Success view but doesn't display the view on screen. It keeps old view on screen.
Degug route after success: _ViewStart-->Success.cshtml-->_Layout.cshtml.
Can anybody suggest me if I am missing something?
Since you are making an ajax call using Ajax.BeginForm helper method, you need to specify where the response (of the ajax call) to be replaced in the DOM. You can specify that using the UpdateTargetId property
#using (Ajax.BeginForm("MakePayment", "Downloads", new AjaxOptions { HttpMethod = "POST",
InsertionMode = InsertionMode.Replace,
UpdateTargetId="YourDivToShowResult"
OnBegin = "payByCreditCard" }))
{
<div id="YourDivToShowResult"></div>
<input type="submit" />
}
Since this is an ajax call, It will not do a redirect. Instead,it will update the content of the div (with Id YourDivToShowResult) in the same page with response coming back which is the markup returned by Success view.
Also since we are showing a partial page update, you might consider returning a partial view.
[HttpPost]
public ActionResult MakePayment(Downloads CCM)
{
if (everything is good)
{
return PartialView("Success");
}
else
{
return PartialView("Failure");
}
}
This will return the markup from either of those views without the layout.
If you want to go to other view you have to redirect the page like:
return RedirectToAction("Success", "Downloads");
I am using Ajax.BeginForm() and a UpdateTargetId. However when the user has successfully completed the form and I have done the processing I want to redirect them to a brand new page.
Calling RedirectToAction() and returning a view from there does not work as it targets the id.
I also need to pass parameters to the action. So is my only option to make a JavaScript call of:
return JavaScript("window.location = 'http://www.somewhere.com/blah/blah?aMyParam=123'");
Is there a more elegant way to do this?
Although the way you are doing is correct but more elegant way to do this is shown below :-
Controller -
[HttpPost]
public ActionResult MyAction(MyModel model)
{
if (model.SomeCondition)
{
// return to the client the url to redirect to
return Json(new { url = Url.Action("MyAction2") });
}
else
{
return PartialView("_MyPartialView");
}
}
View -
#using (Ajax.BeginForm("MyAction", "MyController", new AjaxOptions { OnSuccess = "onSuccess", UpdateTargetId = "foo" }))
{
...
}
Javascript -
var onSuccess = function(result) {
if (result.url) {
window.location.href = result.url;
}
}
As shown above you can return Json(having url) from controller action and then redirect from onSuccess javascript function.
How can I return the result of a different action or move the user to a different action if there is an error in my ModelState without losing my ModelState information?
The scenario is; Delete action accepts a POST from a DELETE form rendered by my Index Action/View. If there is an error in the Delete I want to move the user back to the Index Action/View and show the errors that are stored by the Delete action in the ViewData.ModelState. How can this be done in ASP.NET MVC?
[AcceptVerbs(HttpVerbs.Post | HttpVerbs.Delete)]
public ActionResult Delete([ModelBinder(typeof(RdfUriBinder))] RdfUri graphUri)
{
if (!ModelState.IsValid)
return Index(); //this needs to be replaced with something that works :)
return RedirectToAction("Index");
}
Store your view data in TempData and retrieve it from there in your Index action, if it exists.
...
if (!ModelState.IsValid)
TempData["ViewData"] = ViewData;
RedirectToAction( "Index" );
}
public ActionResult Index()
{
if (TempData["ViewData"] != null)
{
ViewData = (ViewDataDictionary)TempData["ViewData"];
}
...
}
[EDIT] I checked the on-line source for MVC and it appears that the ViewData in the Controller is settable, so it is probably easiest just to transfer all of the ViewData, including the ModelState, to the Index action.
Use Action Filters (PRG pattern) (as easy as using attributes)
Mentioned here and here.
Please note that tvanfosson's solution will not always work, though in most cases it should be just fine.
The problem with that particular solution is that if you already have any ViewData or ModelState you end up overwriting it all with the previous request's state. For example, the new request might have some model state errors related to invalid parameters being passed to the action, but those would end up being hidden because they are overwritten.
Another situation where it might not work as expected is if you had an Action Filter that initialized some ViewData or ModelState errors. Again, they would be overwritten by that code.
We're looking at some solutions for ASP.NET MVC that would allow you to more easily merge the state from the two requests, so stay tuned for that.
Thanks,
Eilon
In case this is useful to anyone I used #bob 's recommended solution using PRG:
see item 13 -> link.
I had the additional issue of messages being passed in the VeiwBag to the View being written and checked / loaded manually from TempData in the controller actions when doing a RedirectToAction("Action"). In an attempt to simplify (and also make it maintainable) I slightly extended this approach to check and store/load other data as well. My action methods looked something like:
[AcceptVerbs(HttpVerbs.Post)]
[ExportModelStateToTempData]
public ActionResult ChangePassword(ProfileViewModel pVM) {
bool result = MyChangePasswordCode(pVM.ChangePasswordViewModel);
if (result) {
ViewBag.Message = "Password change success";
else {
ModelState.AddModelError("ChangePassword", "Some password error");
}
return RedirectToAction("Index");
}
And my Index Action:
[ImportModelStateFromTempData]
public ActionResult Index() {
ProfileViewModel pVM = new ProfileViewModel { //setup }
return View(pVM);
}
The code in the Action Filters:
// Following best practices as listed here for storing / restoring model data:
// http://weblogs.asp.net/rashid/archive/2009/04/01/asp-net-mvc-best-practices-part-1.aspx#prg
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;
}
}
// Added to pull message from ViewBag
if (!string.IsNullOrEmpty(filterContext.Controller.ViewBag.Message)) {
filterContext.Controller.TempData["Message"] = filterContext.Controller.ViewBag.Message;
}
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);
}
}
// Restore Viewbag message
if (!string.IsNullOrEmpty((string)filterContext.Controller.TempData["Message"])) {
filterContext.Controller.ViewBag.Message = filterContext.Controller.TempData["Message"];
}
base.OnActionExecuted(filterContext);
}
}
I realize my changes here are a pretty obvious extension of what was already being done with the ModelState by the code # the link provided by #bob - but I had to stumble on this thread before I even thought of handling it in this way.
Please don't skewer me for this answer. It is a legitimate suggestion.
Use AJAX
The code for managing ModelState is complicated and (probably?) indicative of other problems in your code.
You can pretty easily roll your own AJAX javascript code. Here is a script I use:
https://gist.github.com/jesslilly/5f646ef29367ad2b0228e1fa76d6bdcc#file-ajaxform
(function ($) {
$(function () {
// For forms marked with data-ajax="#container",
// on submit,
// post the form data via AJAX
// and if #container is specified, replace the #container with the response.
var postAjaxForm = function (event) {
event.preventDefault(); // Prevent the actual submit of the form.
var $this = $(this);
var containerId = $this.attr("data-ajax");
var $container = $(containerId);
var url = $this.attr('action');
console.log("Post ajax form to " + url + " and replace html in " + containerId);
$.ajax({
type: "POST",
url: url,
data: $this.serialize()
})
.done(function (result) {
if ($container) {
$container.html(result);
// re-apply this event since it would have been lost by the form getting recreated above.
var $newForm = $container.find("[data-ajax]");
$newForm.submit(postAjaxForm);
$newForm.trigger("data-ajax-done");
}
})
.fail(function (error) {
alert(error);
});
};
$("[data-ajax]").submit(postAjaxForm);
});
})(jQuery);
Maybe try
return View("Index");
instead of
return Index();