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");
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'm new to MVC and trying to create a wizard-style series of views, passing the same model instance from one view to the next, where the user completes a little more information on each form. The controller looks something like this:-
[HttpGet]
public ActionResult Step1()
{
return View();
}
[HttpPost]
public ActionResult Step1(MyModel model)
{
if (!ModelState.IsValid)
return View(model);
return View("Step2", model);
}
[HttpPost]
public ActionResult Step2(MyModel model)
{
if (!ModelState.IsValid)
return View(model);
return View("Step3", model);
}
// etc..
Questions:-
When I submit the form from the Step1 view, it calls the Step1 POST method and results in the Step2 view being displayed in the browser. When I submit the form on this view, it calls the Step1 POST method again! I got it to work by specifying the action and controller name in Html.BeginForm(), so I'm guessing that the parameterless overload just POSTs back to the action that rendered the view?
I've noticed that the browser's address bar is out of sync with the current view - when I'm on the Step2 view it still shows the Step1 URL, and when on Step3 it shows the Step2 URL. What's going on?
Another approach I've seen for passing a model between views is to put the model in TempData then use RedirectToAction(). What are the pros and cons of this method versus what I'm currently doing?
I won't be providing any "back" buttons of my own in the wizard. Are there any pitfalls to be aware of regarding the browser's back button, and do either of the above two approaches help (or hinder)?
Edit
Prompted by #StephenMuecke's comment I've now rewritten this to use a single view. I tried this once before but had difficulties round-tripping a "step number" to keep track of where I was in the wizard. I was originally using a hidden field created with #Html.HiddenFor', but this wasn't updating as the underlying model property changed. This appears to be "by design", and the workaround is to create the hidden field using vanilla HTML (
Anyway the one-view wizard is now working. The only problem is the old chestnut of the user being able to click the back button after they have completed the wizard, make a change, and resubmit a second time (resulting in a second DB record).
I've tried adding [OutputCache(NoStore = true, Duration = 0, VaryByParam = "None")] to my POST method, but all this does is display (in my case) a Chrome error page suggesting that the user clicks refresh to resubmit the form. This isn't user friendly and doesn't prevent a second submit.
you can use RedirectToAction() in this case without worrying about TempData. Just add your model as a parameter to each action and use RedirectToAction("Step2", model);
[HttpGet]
public ActionResult Step1()
{
return View();
}
[HttpPost]
public ActionResult Step1(MyModel model)
{
if (!ModelState.IsValid)
return View(model);
return RedirectToAction("Step2", model);
}
[HttpGet]
public ActionResult Step2(MyModel model)
{
return View(model);
}
[HttpPost]
public ActionResult Step2(MyModel model)
{
if (!ModelState.IsValid)
return View(model);
return RedirectToAction("Step3", model);
}
// etc..
The answer to #1 is found in #2.. if you dont specify the Action in you Html.BeginForm() it posts to the current url.
Using TempData to avoid model displaying in url.
[HttpGet]
public ActionResult Step1()
{
return View();
}
[HttpPost]
public ActionResult Step1(MyModel model)
{
if (!ModelState.IsValid)
return View(model);
TempData["myModel"] = model;
return RedirectToAction("Step2");
}
[HttpGet]
public ActionResult Step2()
{
var model = TempData["myModel"] as MyModel;
return View(model);
}
[HttpPost]
public ActionResult Step2(MyModel model)
{
if (!ModelState.IsValid)
return View(model);
TempData["myModel"] = model;
return RedirectToAction("Step3");
}
// etc..
Another option would be to add the name of the next action to ViewBag and set your actionName in each BeginForm()
[HttpGet]
public ActionResult Step1()
{
ViewBag.NextStep = "Step1";
return View();
}
[HttpPost]
public ActionResult Step1(MyModel model)
{
if (!ModelState.IsValid)
{
ViewBag.NextStep = "Step1";
return View(model);
}
ViewBag.NextStep = "Step2";
return View("Step2", model);
}
[HttpPost]
public ActionResult Step2(MyModel model)
{
if (!ModelState.IsValid)
{
ViewBag.NextStep = "Step2";
return View(model);
}
ViewBag.NextStep = "Step3";
return View("Step3", model);
}
//View
#using (Html.BeginForm((string)ViewBag.NextStep, "ControllerName"))
{
}
I'd prefer to add NextStep as a property to MyModel and using that instead of using ViewBag though.
I understand the thought behind your approach and don't have any issues with it. Unfortunately, I don't believe that ASP.NET MVC is geared very well for passing the the same view model (with data!) between different actions.
Typically, the scaffolded actions in the controller will either create a model item or find it by identifier in the database.
I don't know if this would help, but you could try to save it to the database on every step, and then retrieve it by identifier, or you could also save it to a session and grab it that way.
One issue I do see with your approach is you have Step2 set as a get, yet you probably want to post data to it from Step1 instead of using a query string. You may need to reconcile that issue.
I have an MVC5 application which has several controllers, scaffolded with EF6 CRUD actions and associated views. One of these controller/view sets is used for managing a table of patient identifiers, and on completion of an edit or delete, the controller returns an action link to the identifiers index view, as expected.
However, the patient identifiers are also displayed on the various views of the patients controller, and from the Patient.Edit view I have Html.ActionLink calls to the identifier controller's edit or delete actions. When the latter are called from the Patient.Edit view, I would like them to return to that on completion.
Is there any way I can accomplish this?
Yes, but this is always a manual process. There's nothing built into MVC specifically for return URLs.
Essentially, your links to edit/delete will need to include a GET param, usually called returnUrl, though the name doesn't matter, which will be set to the current page URL. For example:
#Html.ActionLink("Edit", new { id = patient.Id, returnUrl = Request.RawUrl })
Then, your edit/delete GET action should accept this parameter, and set a ViewBag member:
public ActionResult Edit(int id, string returnUrl = null)
{
ViewBag.ReturnUrl = returnUrl;
return View();
}
In your edit form, add a hidden field:
#Html.Hidden("returnUrl", ViewBag.ReturnUrl)
In your POST edit action, again, accept the param:
[HttpPost]
public ActionResult Edit(int id, Patient model, string returnUrl = null)
But inside this action is where you'll do something different now. Typically, when you've got a successful post and have saved the object or whatever, you then do something like:
return RedirectToAction("Index");
However, instead, you should now check to see if returnUrl has a value, and if it does, redirect to that instead:
if (!string.IsNullOrEmpty(returnUrl))
{
return Redirect(returnUrl);
}
return RedirectToAction("Index");
The MVC5 with Identity sample project has a nice helper method that it uses:
private ActionResult RedirectToLocal(string returnUrl)
{
if (Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction("Index", "Home");
}
}
That would just go into your controller and basically does the same as I've already described with two notable differences:
It uses Url.IsLocalUrl to check that the return url is actually a URL on this site. That's a smart check, as since this is initially passed in the query string of the URL, it's open to be manipulated by a user.
It encapsulates the logic, so you don't have to remember how to this should be handled. When you have a successful POST, you simply return RedirectToLocal(returnUrl), and if there's a return URL set, it will be used. Otherwise, the fallback redirect will used.
This is how I did it in one of my projects:
public ActionResult Edit(int id, string returnUrl)
{
// find the model (code not shown)
return View(model);
}
In the Edit view you don't need to do anything special, in the Post Action you have
[HttpPost]
public ActionResult Edit(Model model)
{
if (ModelState.IsValid)
{
// save Model...
return Redirect(Request.Params["returnUrl"]);
// Request.Query will work as well since it is in the querystring
// of course you should check and validate it as well...
}
// else return the View as usual, not shown
}
To use it, when creating the "Edit" link from your pages you simply need to specify the extra returnUrl parameter:
#Html.ActionLink("Edit", "Edit",
new { controller = "YourController",
returnUrl = Url.Action("Index", "ThisController",)
})
Hope it helps.
I have this mentally disturbing problem where I used the name View for my Views. As normal, created the initial page and the postback to save.
public ActionResult View(int id)
{
Models.PageContent model = Controllers.PageContent.Get(id);
return View(model);
}
[HttpPost]
[ValidateInput(false)]
public ActionResult View(Models.PageContent model)
{
if (ModelState.IsValid)
{
Controllers.PageContent.UpdatePageContent(model.PageContentID, model.Title, model.Text);
ViewBag.Success = true;
return RedirectToAction("Index");
}
else
{
return View(model);
}
}
What is weird is that both the normal view and the post back view get called and it just doesn't work.
Ah, okay, that makes sense then as to what is happening. In your View(int id) method you're calling View(model), where model is of type PageContent. When .NET tries to resolve overloaded methods it picks the most specific, which would be the method right below in the same controller. The method that is in the base control is actually defined as View(Object model) which is less specific than View(PageContent model), so it resolves the call to the HttpPost version of the method rather than the base class version of the method.
The comment about changing the name of the method is correct. Your choice of method name is conflicting with the framework-provided View method.
If you want to use View in the URL you can use the ActionName attribute.
[ActionName( "View" )]
public ActionResult GetView( int id )
[HttpPost]
[ActionName( "View" )]
public ActionResult PostView( Models.PageContent model )
I did a test at home on my VS2013 and I actually got a green squiggly to warn me. For some reason,on both mine and another developers this did not show. Possibly because it was MVC3 and not 4 like my test.
hejdig.
In Aspnetmvc2 I have a model object that I send to a View. A control in the view isn't updated with the value. Why? What obvious trap have I fallen in?
The View:
<%:Html.TextBox(
"MyNumber",
null == Model ? "1111" : Model.MyNumber ) %>
<%:Model.MyNumber%>
is first fetched trough a Get. The "1111" value in the textbox is manually updated to "2222". We post the form to the controller which appends "2222" to the Model object and sends it to the view again.
The Controller:
[HttpGet]
public ActionResult Index()
{
return View();
}
[HttpPost]
public ActionResult Index( MyModel myModel)
{
myModel.MyNumber += " 2222";
return View(myModel);
}
Alltogether we get an output like:
<input id="MyNumber" type="text" value="1111">
1111 2222
As you can see the control doesn't use the Model's attribute but instead falls back to thew viewstate that doesn't exist in Aspnetmvc.
(The same happens with Razor.)
That's normal and it is how HTML helpers work : they look first in the model state and then in the model when binding a value. So if you intend to modify some property in the POST action you need to remove it from the model state first or you will always get the old value:
[HttpPost]
public ActionResult Index(MyModel myModel)
{
ModelState.Remove("MyNumber");
myModel.MyNumber += " 2222";
return View(myModel);
}