ValidationSummary not appearing with Partial Views - c#

I have this problem:
I go to a page such as:
/Auction/Details/37
and this calls this action method:
public ActionResult Details(int id)
A particular line in this method is:
return View("DetailsLub", auction);
This view contains this line:
#Html.Action("BidOnAuction", new { auctionId = Model.Id })
Which calls this action method:
public PartialViewResult BidOnAuction(int auctionId)
So far so good?
Now, I have a form in the BidOnAuction view, whcih has a button. When I click on this button, this action method is invloked:
[HttpPost]
public ActionResult BidOnAuction(BidOnAuctionViewModel model)
This action method has a catch statement with the following lines:
ModelState.AddModelError(string.Empty, operation + #" Failure: " + message);
return RedirectToAction("Details", new { id = model.AuctionId });
Now, both the DetailsLUB view and the BidOnAction view contain this line:
#Html.ValidationSummary(true)
But, the issue is that nothing ever gets printed to the screen. What am I doing wrong?

InOrder to get the validation Message on the page you need to return view with Model, as model has the Model State within it, something like this:
return View(Model);
This will return the model BidOnAuction with Validation Summary.

This line of code
return RedirectToAction("Details", new { id = model.AuctionId });
Returns instance of RedirectResult class. That is generally used for redirections and does not render view. If you want to render child action into parent view using #Html.Action, you need to return view from that child action, not RedirectResult. And that RedirectResult will not work even when there's no child action. Returning RedirectResult causes browser to issue fresh, all new request to that action. And model state is lost anyways. You should do something like
try
{
//some actions
return RedirectResult("Details", new { id = model.AuctionId });
}
catch
{
ModelState.AddModelError(string.Empty, operation + #" Failure: " + message);
return View("Details", new { id = model.AuctionId });
}

You can't redirect to a new action and expect the modelstate to be there.
If the modelState is invalid just return (with View(model))
else
redirect to details.
If you need the error information in the details view you will have add it to TempData or pass it in as an optional parameter.

Related

System.ArgumentNullException occured in System.Web.Mvc.dll

I have an application where I need to input data and on submit the data gets saved in database. When I checked in database the input is getting saved successfully but I am getting an exception when the page reloads after httppost.
I am getting exception at :
#Html.DropDownList("LineID", new SelectList(Model.dropConfig, "LineID", "LineID"), "-- Select LineID --", new { required = true, #class = "form-control" })
controller code to get the dropdownlist values, binding with Db:
[ActionName("DetailsForm")]
[HttpGet]
public ActionResult DetailsForm()
{
try
{
var model = new DetailsViewModel() { dropConfig = floorService.DropDownList().ToList() };
return View("DetailsForm", model);
}
catch (Exception ex)
{
return View("_error");
}
}
controller code to http post:
[ActionName("DetailsForm")]
[HttpPost]
public ActionResult DetailsForm(DetailsViewModel model, FormCollection form)
{
DetailsConfiguration detailsConfig = new DetailsConfiguration();
detailsConfig.LineID = Convert.ToString(form["LineID"]);
//Similary for other fields
floorService.SaveDetails(detailsConfig);
ModelState.Clear();
ViewBag.message = "Success";
return View("DetailsForm",model);
}
Snapshot of exception:
Because your view code is using Model.dropConfig to build the SelectList for your dropdown, and you are not setting the dropConfig property value before returning to the view.
Remember, Http is stateless. So even though you set the dropConfig property value in the GET action, It won't be available inside your HttpPost action. When you submit your form, it is a totally new request to the server.
You can fix it by loading dropConfig property again.
model.dropConfig = floorService.DropDownList().ToList();
return View(model);
But ideally you should be following the P-R-G pattern.
P-R-G stands for Post-Redirect-Get. So when you submit your form to an http post action method, you should return a redirect response and the browser will make a new GET call to that action method.
You can use the RedirectToAction method to return a redirect response.
floorService.SaveDetails(detailsConfig);
return RedirectToAction("DetailsForm");
This will send a 302 response back to the browser with the location header set to the url to the DetailsForm action method and the browser will make a new GET request to that.
ViewBag won't work when with redirect response. So you might consider using TempData. TempData can be used to transfer between two requests.
TempData["message"] = "Success";
return RedirectToAction("DetailsForm");
Now you can read TempData["message"] in the DetailsForm action method or the view rendered by that.
For example, you can read it in the view (rendered by DetailsForm GET action method) like this
#if (TempData["message"]!=null)
{
<div class="alert alert-success" id="alert">
<button type="button" class="close" data-dismiss="alert">x</button>
<strong>Success! </strong>#TempData["message"]
</div>
}

TempData gets overrided when I try to ReturnRedirect

I want to redirect to a previous page after I submit some data in database. I am using a TempData to retrieve ID on current Page, then I am going on the page with the form I want to submit (which have an ID), but after the POST method is fired TempData gets overrided by current page ID which is the page with the form and I want to ReturnRedirect to initial page with its ID.
Here the code:
Controller with GET method where I retrieve the ID:
var currentID = Url.RequestContext.RouteData.Values["id"];
TempData["currentId"] = currentID;
Controller with POST method where I try to redirect:
if (ModelState.IsValid)
{
// Editing records in database
....
return RedirectToAction("Details", "Jobs",
new { controller = "JobsController", action = "Details", id = TempData["currentId"] });
}
ModelState.AddModelError("", "Something failed");
return View();
I am using this approach because its working if the current action with a POST method doesn't have an ID.
Thank you for any suggestions.
EDIT:
I have a Details of Jobs:
// GET: Jobs/Details/5
public ActionResult Details(Guid id)
{
var currentID = Url.RequestContext.RouteData.Values["id"];
TempData["currentId"] = currentID;
var currentUserTemp = LoggedUserId;
TempData["userID"] = currentUserTemp;
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Job job = db.Jobs.Find(id);
if (job == null)
{
return HttpNotFound();
}
return View(job);
}
But from this page I want a link to a page where I edit current user data.
View Details page Code:
#Html.ActionLink("Edit Company Details", "Edit", "UserAdmin", new { id = TempData["userID"] }, null)
Now I am on edit User Page, I edit the form and use save button (which triggers POST method) I want to use the other TempData variable to ReturnRedirect on Details Page but since both pages have ID`s, TempData gets overrided with last ID from the URL which is the ID of the user not ID.
Don't forget that TempData exists only during the time of a HTTP Request. Maybe it's a clue to your problem (I can't find all the interactions between Controllers and Views in your code so I can't be sure).

How do I return a different ActionResult from my controller that takes arguments?

I have a Controller with some actions on it as follows:
[HttpPost]
public ActionResult Create(CreateModel model)
{
if (model.SelectedCustomers.Count > 0 &&
model.SelectedVersions.Count > 0 &&
!string.IsNullOrWhiteSpace(model.ScriptName) &&
!string.IsNullOrWhiteSpace(model.ScriptText))
{
Script script;
...save to database...
return Edit(script.Id); //<---------Return other view here
}
else
{
...
}
}
[HttpGet]
public ActionResult Edit(int? scriptId)
{
return View();
}
After the Create action runs, and saves my model to the database successfully, I want to send the user to the Edit view for the newly created script. When I use the code above, specifically return Edit(script.Id); it just sends the user back to the Create view instead of the Edit view. When the user navigates to the Edit action directly, or through the result of an Html.ActionLink pointed at Edit everything works correctly.
What am I doing wrong?
This isn't doing what you think it does:
return Edit(script.Id)
It's not actually telling the framework to go to that action. It's just returning the return value of that method. Purely a C# concern before any components of the ASP.NET MVC Framework are involved at all. And what is that return value:
return View()
So the former is really functionally the same thing as the latter. And any time you use return View() in ASP.NET MVC, the framework will determine that view by examining the action currently being called, which in this case is Create.
What you want isn't to return the Edit view (even if you do, in this case, the user is still on the Create URL, which will cause confusion). What you want is to return a redirect to tell the client to request that next action:
return RedirectToAction("Edit", new { scriptId = script.Id });
You can always call RedirectToAction and return that action result. That will inform the browser to redirect to the different action.
I think you will need something like this:
return RedirectToAction("Edit", new { scriptId = script.Id });
Calling Edit directly is no different than calling a method.
You can do with this RedirecToAction with input parameters.
return RedirectToAction("Action", new { id = 12 });
In Your Case:
return RedirectToAction("Edit", new { scriptId = script.Id });

MVC #Html.ActionLink no-op from controller

I have an #Html.ActionLink inside of a partial view that when clicked I'd like to have either send the user to another view or stay on the current view without changing anything. Is this possible?
Our controller looks like:
public ActionResult Edit(int id)
{
if (ShouldAllowEdit(id))
{
return this.View("Edit", ...edit stuff...)
}
return ????????
}
We tried return new EmptyResult(); but that just dumps the user to a blank page.
This is a little different approach to the issue, but it should do what you want.
Instead of giving the user a link to navigate to, do an ajax call on link/button click, and do the id check. Return either the url to navigate to in a JsonResult, or nothing if the id is invalid.
On return of the ajax call, navigate to the url if appropriate.
(swap out the hard coded ids and the == 0 with your ShouldAllowEdit function in the example of course)
In the View:
<div class="btn btn-danger" id="myButton">Button</div>
#section scripts{
<script>
$("#myButton").click(function () {
$.ajax("#Url.Action("Edit", new { id = 0 })", { type : "POST" })
.success(function (data) {
if (data.url !== "") {
window.location.href = data.url;
}
});
});
</script>
}
In the controller:
[HttpPost]
public JsonResult Edit(int id)
{
if (id == 0)
{
return Json(new {url = ""});
}
else
{
return Json(new { url = Url.Action("EditPage", new { id = id }) });
}
}
An answer is to redirect to the view action - and maybe give some feed back why they failed.
public ActionResult Edit(int id)
{
if (ShouldAllowEdit(id))
{
return this.View("Edit", ...edit stuff...)
}
ModelState.AddModelError("id", "Not allowed to edit this item");
return RedirectToAction(Edit(id));
}
If the user clicks a link they will be taken away. They might be sent back right to the same page, but the page will unload, be requested from the server again, and then re-rendered in the browser. If you don't want that to happen, you don't give the user the link in the first place. In other words, conditionally render the link or not based on the user's roles or whatever.
#if (userCanEdit)
{
#Html.ActionLink(...)
}
Where userCanEdit is whatever logic you need to make that determination.
If the user fails whatever check you determine, then they don't get the link. Simple.
However, since there's malicious people in the world, you can't just leave it entirely there. There's potential for the user to figure out the link to edit something and go there manually. So, to prevent that you check for the edit permission in your action (like you've already got in your code sample), but if the user is not allowed, then you just return a forbidden status code:
return new HttpStatusCodeResult(HttpStatusCode.Forbidden);
Or
return new HttpStatusCodeResult(403);
They both do the same thing.
UPDATE
Based on your comment above, it appears that the user is normally allowed to edit but can't in a particular instance because another user is editing. A 403 Forbidden is not appropriate in that case, so really all you've got is a simple redirect back to the page they were on, perhaps with a message explaining why they're back there.
TempData["EditErrorMessage"] = "Sorry another user is editing that right now.";
return RedirectToAction("Index");

How can I maintain ModelState with RedirectToAction?

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();

Categories

Resources