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.
Related
How do I retrieve the values in the Verify View that I am passing in the Create View. I want to display these values from the cshtml file.
[HttpPost]
public ActionResult Create(Ticket ticket)
{
return RedirectToAction("Verify", ticket);
}
[HttpGet]
public ActionResult Verify()
{
return View();
}
</div>
</body>
Try setting the value in TempData and access it in the verify action and pass it over to your verify view and now you can access the Ticket model in verify view. TempData is persisted for the subsequent request. RedirectToAction infact does a 302 request to the browser which in turn redirects to the Verify action and you will get the ticket from TempData there.
[HttpPost]
public ActionResult Create(Ticket ticket)
{
TempData["ticket"] = ticket;
return RedirectToAction("Verify");
}
[HttpGet]
public ActionResult Verify()
{
Ticket ticket = (Ticket)TempData["ticket"];
//Do something
return View(ticket);
}
I don't think it is a good idea to set a complex type in RouteDataDictionary.
I am using the same model between 2 views, but when posting the model to the second view it puts all the previously entered data in the URL. Is it possible to send the populated model to the second view without posting the data in the URL?
Controller code:
[HttpPost]
public ActionResult ViewExample1(.Models.RegisterModel model)
{
if (ModelState.IsValid)
{
return RedirectToAction("ViewExample2", model);
}
return View(model);
}
public ActionResult ViewExample2(Models.RegisterModel model)
{
return View(model);
}
Second view code where I use HiddenFor to persist the data when this view is posted back:
<% using (Html.BeginForm(null, null, FormMethod.Post, new { id="ViewExample2"})) { %>
<%: Html.HiddenFor(model => model.UserName)%>
<% } %>
When you redirect to an action with RedirectToAction(), you're doing that by GET. So the Framework passes your view model in the url to the action.
I'd suggest you to do this:
[HttpPost]
public ActionResult ViewExample1(Models.RegisterModel model)
{
if (ModelState.IsValid)
{
// Do the work you want to do in the ViewExample2 action here!
// ... and then return the ViewExample2 view
return View("ViewExample2", model);
}
return View(model);
}
// This action is not needed anymore
/*public ActionResult ViewExample2(Models.RegisterModel model)
{
return View(model);
}*/
My guess is that you're using a form tag (rather than BeginForm) and you aren't specifying a method, so it defaults to using a GET rather than a POST.
Convert to using a BeginForm, or add the method.
So I have a simple action in my controller. The project is a MVC Mobile Application.
public ActionResult Index()
{
return View();
}
this gives an form to enter data. I then handle the data in the post back.
[HttpPost]
public ActionResult Index(ScanViewModel model)
{
if (ModelState.IsValid)
{
Scan ns = new Scan();
ns.Location = model.Location;
ns.Quantity = model.Quantity;
ns.ScanCode = model.ScanCode;
ns.Scanner = User.Identity.Name;
ns.ScanTime = DateTime.Now;
_db.Scans.Add(ns);
_db.SaveChanges();
}
return View(model);
}
I want to clear the fields in the form and allow users to enter data again. However I get the exact same values back into my inputs. How can I clear them in the controller.
Just call this.ModelState.Clear()
You should follow the PRG pattern.
Just redirect to the Action method which is meant for the Create Screen. You can use the RedirectToAction method to do so.
RedirectToAction returns an HTTP 302 response to the browser, which causes the browser to make a GET request to the specified action.
[HttpPost]
public ActionResult Index(ScanViewModel model)
{
if(ModelState.IsValid)
{
//Code for save here
//..............
_db.SaveChanges();
return RedirectToAction("Index","User");
}
return View(model);
}
public ActionResult Index()
{
return View();
}
Assuming your controller name is UserController.
I am redirecting the view from [HttpPost] method to [HttpGet] method. I have gotten it to work, but want to know if this is the best way to do this.
Here is my code:
[HttpPost]
public ActionResult SubmitStudent()
{
StudentViewModel model = TempData["model"] as StudentResponseViewModel;
TempData["id"] = model.Id;
TempData["name"] = model.Name;
return RedirectToAction("DisplayStudent");
}
[HttpGet]
public ActionResult DisplayStudent()
{
ViewData["id"] = TempData["id"];
ViewData["name"] = TempData["name"];
return View();
}
View:
<%# Page
Language="C#"
Inherits="System.Web.Mvc.ViewPage"
%>
<html>
<head runat="server">
<title>DisplayStudent</title>
</head>
<body>
<div>
<%= ViewData["id"]%> <br />
<%= ViewData["name"]%>
</div>
</body>
</html>
There are basically 3 techniques in ASP.NET MVC to implement the PRG pattern.
TempData
Using TempData is indeed one way of passing information for a single redirect. The drawback I see with this approach is that if the user hits F5 on the final redirected page he will no longer be able to fetch the data as it will be removed from TempData for subsequent requests:
[HttpPost]
public ActionResult SubmitStudent(StudentResponseViewModel model)
{
if (!ModelState.IsValid)
{
// The user did some mistakes when filling the form => redisplay it
return View(model);
}
// TODO: the model is valid => do some processing on it
TempData["model"] = model;
return RedirectToAction("DisplayStudent");
}
[HttpGet]
public ActionResult DisplayStudent()
{
var model = TempData["model"] as StudentResponseViewModel;
return View(model);
}
Query string parameters
Another approach if you don't have many data to send is to send them as query string parameters, like this:
[HttpPost]
public ActionResult SubmitStudent(StudentResponseViewModel model)
{
if (!ModelState.IsValid)
{
// The user did some mistakes when filling the form => redisplay it
return View(model);
}
// TODO: the model is valid => do some processing on it
// redirect by passing the properties of the model as query string parameters
return RedirectToAction("DisplayStudent", new
{
Id = model.Id,
Name = model.Name
});
}
[HttpGet]
public ActionResult DisplayStudent(StudentResponseViewModel model)
{
return View(model);
}
Persistence
Yet another approach and IMHO the best consists into persisting this model into some data store (like a database or something and then when you want to redirect to the GET action send only an id allowing for it to fetch the model from wherever you persisted it). Here's the pattern:
[HttpPost]
public ActionResult SubmitStudent(StudentResponseViewModel model)
{
if (!ModelState.IsValid)
{
// The user did some mistakes when filling the form => redisplay it
return View(model);
}
// TODO: the model is valid => do some processing on it
// persist the model
int id = PersistTheModel(model);
// redirect by passing the properties of the model as query string parameters
return RedirectToAction("DisplayStudent", new { Id = id });
}
[HttpGet]
public ActionResult DisplayStudent(int id)
{
StudentResponseViewModel model = FetchTheModelFromSomewhere(id);
return View(model);
}
Each method has its pros and cons. Up to you to choose which one suits best to your scenario.
If you are inserting this data into a database then you should redirect them to a controller action that has this data in the route:
/Students/View/1
You can then write code in the controller to retrieve the data back from the database for display:
public ActionResult View(int id) {
// retrieve from the database
// create your view model
return View(model);
}
One of the overrides of RedirectToAction() looks like that:
RedirectToAction(string actionName, object routeValues)
You can use this one as:
[HttpPost]
public ActionResult SubmitStudent()
{
StudentViewModel model = TempData["model"] as StudentResponseViewModel;
return RedirectToAction("DisplayStudent", new {id = model.ID, name = model.Name});
}
[HttpGet]
public ActionResult DisplayStudent(string id, string name)
{
ViewData["id"] = TempData["id"];
ViewData["name"] = TempData["name"];
return View();
}
Hope that works.
This is the classic Post-Redirect-Get pattern (PRG) and it looks fine but I would add one bit of code. In the DisplayStudent method check if your TempData variables are not null otherwise do a redirect to some default Index action. This is in case a user presses F5 to refresh the page.
public ActionResult DisplayStudent()
{
if(TempData["model"] == null)
{
return RedirectToAction("Index");
}
var model = (StudentResponseViewModel)TempData["model"];
return View(model);
}
public ViewResult Index()
{
IEnumerable<StudentResponseViewModel> students = GetAllStudents();
return View(students);
}
Image the following controller method:
public ActionResult ShipmentDetails(Order order)
{
return View(new OrderViewModel { Order = order });
}
The incoming order parameter is filled from a custom model binder, that either creates a new order for this session and stores it in the session, or reuses an existing order from the current session. This order instace is now used to fill a shipment details form, where users can enter their address and so on.
When using #using(Html.BeginForm()) in the view. I cannot use the same signature for the post method again (because this would result in ambigious method names) and I found me adding a dummy parameter just to make this work.
[HttpPost]
public ActionResult ShipmentDetails(Order order, object dummy)
{
if (!ModelState.IsValid)
return RedirectToAction("ShipmentDetails");
return RedirectToAction("Initialize", order.PaymentProcessorTyped + "Checkout");
}
What are the best practices for this? Would you simply rename the method to something like PostShipmentDetails() and use one of the overloads of BeginForm? Or does the problem originate from the point, that the first method has the order parameter?
You could use the ActionName attribuite:
[HttpPost]
[ActionName("ShipmentDetails")]
public ActionResult UpdateShipmentDetails(Order order) { ... }
or a more classic pattern:
public ActionResult ShipmentDetails(int orderId)
{
var order = Repository.GetOrder(orderId);
return View(new OrderViewModel { Order = order });
}
[HttpPost]
public ActionResult ShipmentDetails(Order order)
{
if (!ModelState.IsValid)
return RedirectToAction("ShipmentDetails");
return RedirectToAction("Initialize", order.PaymentProcessorTyped + "Checkout");
}