I have a weird need in an ASP.NET MVC 3 application which blocks my current progress. Here is the case:
I have a little search engine for the products and I render this search engine on multiple pages. This SE makes a HTTP POST request to product controller's search action. It fine till here.
Let's assume that I am on home controller's index action (/home/index). I make a search and check if ModelState.IsValid. As a result, it is not valid. So, I should return this back with the entered model (so that user won't lose the values) and model state errors. But when I do that I ended up with different URL (/product/search) as expected.
If I do a redirect, I lose the ModelState and cannot display error messages.
I have different solutions so far and they all look dirty. Any idea?
Edit
Here is a little project which demonstrates this:
This is the ProductController:
public class ProductController : Controller {
[HttpPost]
public ActionResult Search(SearchModel searchModel) {
if (ModelState.IsValid) {
//Do some stuff...
return RedirectToAction("Index", "SearchResult");
}
return View(searchModel);
}
}
This is the SearchModel:
public class SearchModel {
[Required]
public string ProductCategory { get; set; }
[Required]
public string ProductName { get; set; }
}
This is the *_SearchPartial*:
#model MvcApplication20.SearchModel
#using (Html.BeginForm("search", "product"))
{
#Html.EditorForModel()
<input type="submit" value="Search" />
}
And finally this is the Home controller Index action view which renders the *_SearchPartial*:
#{
ViewBag.Title = "Home Page";
}
<h2>#ViewBag.Message</h2>
#Html.Partial("_SearchPartialView")
Here, when I submit the form and if the model state fails, how should I proceed at the Product controller Search action?
Here, when I submit the form and if the model state fails, how should
I proceed at the Product controller Search action?
Normally in this case you should render the _SearchPartialView but not as a partial but as a full view with layout so that the user can fix his errors. No need to stay at Home/Index in this case:
[HttpPost]
public ActionResult Search(SearchModel searchModel) {
if (ModelState.IsValid) {
//Do some stuff...
return RedirectToAction("Index", "SearchResult");
}
// since we are returning a view instead of a partial view,
// the _SearchPartialView template should be displayed with the layout
return View("_SearchPartialView", searchModel);
}
And if you wanted to stay on the same page upon error you could use an AJAX call to perform the search. So you would AJAXify this search form and then in the success callback test the result of the Search action and based on it decide whether to refresh the partial in order to show the error or redirect to the results action using window.location.href:
something along the lines of:
$(document).on('submit', '#searchForm', function() {
$.ajax({
url: this.action,
type: this.method,
data: $(this).serialize(),
success: function(result) {
if (result.redirectTo) {
// no validation errors we can redirect now:
window.location.href = result.redirectTo;
} else {
// there were validation errors, refresh the partial to show them
$('#searchContainer').html(result);
// if you want to enable client side validation
// with jquery unobtrusive validate for this search form
// don't forget to call the .parse method here
// since we are updating the DOM dynamically and we
// need to reattach client side validators to the new elements:
// $.validator.unobtrusive.parse(result);
}
}
});
return false;
});
This obviously assumes that you have now wrapped the partial call in a div with id="searchContainer" and that you provided an id="searchForm" when generating the search form:
<div id="searchContainer">
#Html.Partial("_SearchPartialView")
</div>
and now the search action:
[HttpPost]
public ActionResult Search(SearchModel searchModel) {
if (ModelState.IsValid) {
//Do some stuff...
return Json(new { redirectTo = Url.Action("Index", "SearchResult") });
}
return PartialView("_SearchPartialView", searchModel);
}
As far as I know the ModelState is lost when doing a RedirectToAction, the solution would be to save the modelstate in the TempData one example of this, that I'm using is this:
http://weblogs.asp.net/rashid/archive/2009/04/01/asp-net-mvc-best-practices-part-1.aspx#prg
This is also discussed in various posts for instance MVC Transfer Data Between Views
Related
Overview: I am currently attempting to build a create account form. The form is rendered on another razor page. All works correctly, the form displays, sends the form data to a controller, sends data to a class, performs all DB actions, but then upon the completion of the previous items , the program attempts to find a page "CreateAccount.something" when all I want it to do for the time being is to return the initial view upon the return call.
Within said project, a form is displayed via: #RenderPage("~/Views/Home/AccountCreationForm.cshtml")
The form:
#model SuperDuperProject.Models.AccountCreationModel // AccountCreationModel is only a class file containing the necessary variables
...
#using (Html.BeginForm("CreateAccount", "Home", FormMethod.Post))
{
<table cellpadding="0" cellspacing="0">
...
#Html.TextBoxFor(m => m.name)
...
<input type="submit" value="Submit"/>
</table>
}
The Controller file (HomeController.cs):
...
public ActionResult UserLogin() // the page containing the form
{
return View();
}
[HttpPost]
public ActionResult CreateAccount(AccountCreationModel ACM)
{
Console.WriteLine("CreateAccount within HomeController");
Helpers.CreateAccount a = new Helpers.CreateAccount(...);
a.AccountCreationQuery();
return Index(); // ********** Doesn't seem to operate correctly **********
}
Instead of returning Index() or anything placed there, the program attempts to find a CreateAccount view that does not exist.
What am I missing so I can simply return to a desired page, such as Index?
Any assistance would be greatly appreciated.
[HttpPost]
public ActionResult CreateAccount(AccountCreationModel ACM)
{
Console.WriteLine("CreateAccount within HomeController");
Helpers.CreateAccount a = new Helpers.CreateAccount(...);
a.AccountCreationQuery();
return RedirectToAction("Index");
// return Redirect("Home/Index"); alternatively can use Redirect
}
You could consider using RedirectToAction or Redirect.
RedirectToAction returns an HTTP 302 response to the browser, which causes the browser to make a GET request to the specified action. Redirect takes a string type URL parameter and redirects to that specified the URL.
Check out this post for more info:
https://www.codeproject.com/Articles/595024/Controllers-and-Actions-in-ASP-NET-MVC
Try this code
public ActionResult UserLogin() // the page containing the form
{
return View();
}
[HttpPost]
public ActionResult CreateAccount(AccountCreationModel ACM)
{
Console.WriteLine("CreateAccount within HomeController");
Helpers.CreateAccount a = new Helpers.CreateAccount(...);
a.AccountCreationQuery();
return View("Index"); //index is the view here. You can define the view //name which you want to return from this controller action
}
By default controller searches for the view name as the name of the controller action that's why it is attempting to locate the view CreateAccount because controller action name is CreateAccount.
I have a number of admin pages where the user is presented with a grid of items. The grids has a search panel above them where the user can filter by a number of fields.
When the user clicks search i do a post which then redirects to the Index page. I am sure the code below can be done in a better way (in some scenarios i have up to 20 filter fields).
[HttpPost]
public ActionResult Filter(MySearchModel model)
{
var searchParams = new
{
fielda = model.FieldA,
fieldb = model.FieldB,
fieldc = model.FieldC
};
return RedirectToAction("Index", searchParams);
}
public ActionResult Index(MySearchModel model)
{
//do filtering, return view etc in here
}
Rather than performing a post with a redirect, just change your original form to use a GET method and target the "Index" action directly.
using(Html.BeginForm("Index", null, FormMethod.Get))
{
...
}
That way you can eliminate an HTTP round-trip and get rid of the "Filter" action completely.
I am working with a screen that needs to perform a payment based on the selected "Payment Methods" exposed with a checkbox for each one as you can see in the following mockup.
The most important thing, is that every payment method has its own logic based on the API that each provide to us where some of them need to redirect a URL for login on their site (i.e. PayPal), and some others only to specify some credentials and execute the payment itself. So, according with this, we have been forced to create a controller for each one in order to delegate the interaction with the user once he chose an option. Being more clearer, it will be something like this:
public class PayPalController : BaseController, IPaymentController
{
public ActionResult Pay()
{
//logic for a pay with PayPal
return View("PaymentSuccess");
}
}
public class BankOfNigeriaController : BaseController, IPaymentController
{
public ActionResult Pay()
{
//logic for a pay with Bank Of Nigeria
return View("PaymentSuccess");
}
}
public class BankOfAngolaController : BaseController, IPaymentController
{
public ActionResult Pay()
{
//logic for a pay with Bank Of Angola
return View("PaymentSuccess");
}
}
public interface IPaymentController
{
ActionResult Pay();
}
Focused on the view that I exposed on the top, I am wondering which is the best practice to call the proper action method.
Option 1: consists on create a middle controller that redirects to the proper action.
The ViewModel has an string that it will be used for the redirect URL as selected option. In example: "PayPal" or "BankOfNigeria".
public class PayPalPaymentViewModel
{
[Required]
public string PaymentSelected { get; set; }
}
The View shows a begin form that will POST to a middle controller that, according with the chosen option will perform another redirect.
#model Bollore.IES.Web.Models.PaymentMethodViewModel
#{
ViewBag.Title = "Payment Methods";
Layout = "~/Views/Shared/_Layout.cshtml";
}
#using (Html.BeginForm("RedirectPay", "PaymentMethod", FormMethod.Post))
{
//HTML WITH LIST OF CHECKBOXES
<input type="submit" />
}
The MIDDLE Controller receives in the ViewModel the chosen method, and redirects to their own controller. I didn't test it, at the moment is only pseudo-code.
public class PaymentMethodController : BaseController
{
[HttpGet]
public ActionResult RedirectPay()
{
//returns the view
return View();
}
[HttpPost]
public ActionResult RedirectPay(PaymentMethodViewModel model)
{
//it could be "PayPal/Pay" or "BankOfNigeria/Pay", etc.
return RedirectToAction("Pay", model.PaymentSelected);
}
}
Option 2: consists on redirect the action directly from the client-side of the view without passing over the middle controller, which in this case, it will only exist for the GET of the view.
The View will be gotten by the PaymentMethodController, but the POST the the "payment method" action, will be done by javascript.
#model Bollore.IES.Web.Models.PaymentMethodViewModel
#{
ViewBag.Title = "Payment Methods";
Layout = "~/Views/Shared/_Layout.cshtml";
}
//LIST OF CHECKBOXES AND ALL THE STUFF
<input id="clickMe" type="button" value="Save" onclick="callPaymentMethodAction();" />
<script type="text/javascript">
function callPaymentMethodAction(e) {
var selectedMethodUrl = GetMethodSelectedUrl(); //it will return 'PayPal', 'BankOfAngola', etc
$.ajax({
url: 'selectedMethodUrl',
data: { id: id },
success: function(){
alert('Payed');
}
});
};
</script>
I repeat that is a pseudo-code that could not compile, but I hope you get the idea.
So, which of this two is the best approach to deal with a problem like
this? How can you manage the fact to be able to call multiple
controllers from the view?
I have a HomeController with an Index.cshtml Razor view that uses an InitialChoicesViewModel with validation attributes. The Index view contains the following form:
#using (Html.BeginForm("CreateCharacter", "DistributePoints", FormMethod.Get))
This goes to a different controller (which is what I want):
public class DistributePointsController : Controller
{
public ActionResult CreateCharacter(/* my form parameters */)
// ...
}
How do I perform server-side validation on the form (such as checking ModelState.IsValid), returning my original Index view with a correct ValidationSummary on error? (On success I want to return the CreateCharacter view of the other controller.)
Based on John H's answer, I resolved this as follows:
#using (Html.BeginForm("CreateCharacter", "Home"))
HomeController:
[HttpPost]
// Only some of the model fields are posted, along with an additional name field.
public ActionResult CreateCharacter(InitialChoicesViewModel model, string name)
{
if (ModelState.IsValid)
{
return RedirectToAction("CreateCharacter", "DistributePoints",
new {name, model.Level, model.UseAdvancedPointSystem});
}
// Unsure how to post a collection - easier to reload from repository.
model.ListOfStuff = _repository.GetAll().ToList();
return View("Index", model);
}
I had to add a parameterless constructor to my view model, too.
[HttpPost]
public ActionResult CreateCharacter(InitialChoicesViewModel model)
{
if (ModelState.IsValid)
return RedirectToAction("SomeSuccessfulaction");
return View("~/Views/Home/Index.cshtml", model);
}
The ~/ denotes the relative root of your site.
The code above complies with the Post-Redirect-Get pattern, in order to prevent some types of duplicate form submission problems. It does that by redirecting to a separate action when the form submission is successful, and by returning the current view, complete with ModelState information, on error.
By default, ASP.NET MVC checks first in \Views\[Controller_Dir]\, but after that, if it doesn't find the view, it checks in \Views\Shared.
If you do return View("~/Views/Wherever/SomeDir/MyView.aspx") You can return any View you'd like.
But for now in your case, try the following
public ActionResult CreateCharacter(SomeModel model)
{
if(!ModelState.IsValid){
return View("~/Views/Home/Index.cshtml", model )
}
return View();
}
To check your ModelState just use an if statement in Controller:
if(ModelState.IsValid)
{
...
}
If there is any error add you can add an error message to the ModelState Dictionary like this:
ModelState.AddModelError("Somethings failed", ErrorCodeToString(e.StatusCode));
After that return your same View and pass it to your model
return View(model);
If you add "#Html.ValidationSummary()" in your View, it will get the errors from the ModelState Dictionary and display them.But if you show values yourself maybe with different styles you can do it manually, take a look at this question
And if there is no error you can return your CreateCharacter View like this, just redirect user to the appropriate action:
return RedirectToAction("CreateCharacter","DistributePoints");
Below Scenario, I think I must see the START text in my form when first loaded.
When I click send data button and submit, I was waiting to see FINISH text in my form.
Buy the START text never changes when I click the button and post the form...
Anybody can tell the problem?
MY CONTROLLER:
namespace MvcApplication1.Controllers
{
public class BuyController : Controller
{
public ActionResult Index(BuyModel model)
{
if (Request.HttpMethod == "GET")
{
model.Message= "START";
return View(model);
}
else
{
BuyModel newModel = new BuyModel();
newModel.Message= "FINISH";
return View(newModel);
}
}
}
}
MY VIEW :
#model MvcApplication1.Models.BuyModel
#using (Html.BeginForm("Index", "Buy", FormMethod.Post))
{
#Html.TextBoxFor(s => s.Message)
<button type="submit" >Send</button>
}
MY MODEL:
public class BuyModel
{
public string Message { get; set; }
}
public class BuyController : Controller
{
public ActionResult Index()
{
BuyModel model = new BuyModel();
model.Message= "START";
return View(model);
}
[HttpPost]
public ActionResult Index(BuyModel model)
{
model = new BuyModel();
model.Message= "FINISH";
ModelState.Clear(); // the fix
return View(model);
}
}
View:
#model MvcApplication1.Models.BuyModel
#using (Html.BeginForm("Index", "Buy"))
{
#Html.TextBoxFor(s => s.Message)
<button type="submit" >Send</button>
}
Your issue is because your original code, that Action Method will only be executed as an HTTP GET request. ASP.NET MVC allows you to specify a post with the [HttpPost] attribute (see above code).
I'm not sure what you are getting at with your POST desired-behavior. It seems like you are just wiping out whatever form values are pushed on the POST. So modify my above code accordingly, but it should give you the general idea.
Edit: it seems to be that the text box is retaining its value after the POST. It's not just with "START", but if you type anything into that text box and hit submit, you'll have a POST with the exact same text in the text box that was there when you submitted the form.
Edit Edit: see the changed code. Call ModelState.Clear() in your POST action method and you'll have the right value reflected.
If you are posting, and not returning a RedirectResult, by default the helpers will use the value from ModelState. You either need to clear ModelState or have a different approach.
The PRG (post redirect get) pattern in MVC is very important. So if its a post, and you aren't redirecting, the helpers assume there is an error that needs to be corrected and the value is pulled from ModelState.