Using TempData with Post Redirect Get pattern - c#

There are a number of folks that advocate using TempData with the PRG pattern in .NET Core 2.2.x.
From what I understand, this line of code stores data:
TempData["foo"] = JsonConvert.SerializeObject(model);
And the following reconstitutes the model and then deletes it from the TempData construct:
string s = (string)TempData["Model"];
var model = JsonConvert.DeserializeObject<ModelType>(s);
So given this transient nature of TempData, imagine the following PRG construct. The user POSTs to the UserInfo action, which packages the model into TempData and redirects to a UserInfo GET. The GET UserInfo reconstitutes the model and displays the view.
[HttpPost]
public IActionResult UserInfo(DataCollectionModel model) {
TempData["Model"] = JsonConvert.SerializeObject(model);
return RedirectToAction("UserInfo");
}
[HttpGet]
public IActionResult UserInfo() {
string s = (string)TempData["Model"];
DataCollectionModel model = JsonConvert.DeserializeObject<DataCollectionModel>(s);
return View(model);
}
The user is now on the /Controller/UserInfo page. If the user pressed F5 to refresh the page, the TempData["Model"] would no longer be there and the GET on UserInfo would fail. The fix might be to store the model in TempData after reading it, but wouldn't that result in leaking memory?
Am I missing something?

TempData can be used for storing transient data . It is useful for redirection, when data is needed for more than a single request.When an object in a TempDataDictionary is read, it will be marked for deletion at the end of that request.
That means if you put something on TempData like
TempData["value"] = "someValueForNextRequest";
And on another request you access it, the value will be there but as soon as you read it, the value will be marked for deletion:
//second request, read value and is marked for deletion
object value = TempData["value"];
//third request, value is not there as it was deleted at the end of the second request
TempData["value"] == null
The Peek and Keep methods allow you to read the value without marking it for deletion. Say we get back to the first request where the value was saved to TempData.
With Peek you get the value without marking it for deletion with a single call, see msdn:
//second request, PEEK value so it is not deleted at the end of the request
object value = TempData.Peek("value");
//third request, read value and mark it for deletion
object value = TempData["value"];
With Keep you specify a key that was marked for deletion that you want to keep. Retrieving the object and later on saving it from deletion are 2 different calls. See msdn
//second request, get value marking it from deletion
object value = TempData["value"];
//later on decide to keep it
TempData.Keep("value");
//third request, read value and mark it for deletion
object value = TempData["value"];
You can use Peek when you always want to retain the value for another request. Use Keep when retaining the value depends on additional logic.
Make the following changes in your Get action
public IActionResult UserInfo()
{
//The First method ,
string s = (string)TempData.Peek("Model");
//The Second method
string s = (string)TempData["Model"];
TempData.Keep("Model");
if(s==null)
{
return View();
}
else
{
User model = JsonConvert.DeserializeObject<User>(s);
return View(model);
}
}

Related

Pass created data to another controller

I am using ASP.NET MVC Entity Framework and I have a page to insert data
public ActionResult Create()
{
return View();
}
// POST: /Home/Create
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include="id,firstname,lastname,email,guests,guestfirstname,guestlastname,productInterest,occupancyTimeline,isInvestment,timeSlot,dateSlot")] CP_LC_Preview cp_lc_preview)
{
if (ModelState.IsValid)
{
db.Data.Add(cp_lc_preview);
db.SaveChanges();
return RedirectToAction("Confirm", new { info = cp_lc_preview });
}
return View(cp_lc_preview);
}
What I am trying to do is take that data that was just entered and pass it to another controller to display. like a confirmation page.
Here is my method for the confirm page
public ActionResult Confirm()
{
return View();
}
You may consider following the PRG pattern.
PRG stands for POST - REDIRECT - GET. With this approach,After you successfully save the data, you will issue a redirect response with a unique id in the querystring, using which the second GET action method can query the resource again and return something to the view.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include="id,firstname,lastname,email,guests,guestfirstname,guestlastname,productInterest,occupancyTimeline,isInvestment,timeSlot,dateSlot")] CP_LC_Preview cp_lc_preview)
{
if (ModelState.IsValid)
{
db.Data.Add(cp_lc_preview);
db.SaveChanges();
var id = cp_lc_preview.Id;
return RedirectToAction("Confirm", new { id = id });
}
return View(cp_lc_preview);
}
and in your Confirm action method, have id parameter and using the value of that read the record from the db again and use as needed.
public ActionResult Confirm(int id)
{
var d = db.Data.FirstOrDefault(g=>g.Id==id);
// Use d as needed
// to do : Return something
}
TempData
If you do not prefer to have this id in the url, consider using TempData to pass the data. But TempData has a short life span. Once read, the data is gone. TempData uses Session behind the scene to store the data.
TempData["NewItem"] = cp_lc_preview;
return RedirectToAction("Confirm", "controllerName");
and in the Confirm method
public ActionResult actionname()
{
var model=TempData["NewItem"] as CP_LC_Preview
// to do : Return something
}
For your reference
How do I include a model with a RedirectToAction?
You can use TempData for that.
TempData["YourData"] = YourData;//persist data for next request
var myModel=TempData["YourData"] as YourData //consume it on the next request
What is TempData ?
TempData is meant to be a very short-lived instance, and you should only use it
during the current and the subsequent requests only.
Since TempData works this way, you need to know for sure what the next request will be, and
redirecting to another view is the only time you can guarantee this.
Therefore, the only scenario where using TempData will reliably work is when
you are redirecting.This is because a redirect kills the current request , then creates a
new request on the server to serve the redirected view.
Simply said, Asp.Net MVC TempData dictionary is used to share data between
controller actions.
The value of TempData persists until it is read or until the current user’s session times out.
By default, the TempData saves its content to the session state.
TempData values are marked for deletion when you read them. At the end of the request,
any marked values are deleted.
The benefit is that if you have a chain of multiple redirections it won’t cause TempData to
be emptied the values will still be there until you actually use them, then they clear up after
themselves automatically.

Return to second to last URL in MVC (return View with previous filter conditions applied)?

I'm working on an MVC5 application. On the home screen is a grid allowing users to view Data and be transferred to a number of Views for various actions on each record. One of these is an [EDIT].
The issue I'm encountering is as follows: due to the amount of data it is convenient to Filter the data down (say to a specific location) and then Edit records from there. The filter on this grid (Grid.MVC from CodePlex) performs filtering partially by modifying the URL (http://homeURL/?grid-filter=Location.DEPT__1__accounting) such as 1 being Equals, 2 being Cotains, 3 being StartsWith, and 4 being EndsWith and then after the next 2 underscores being the search criteria.
This functions fine, however upon [POST] return from the Edit the user currently is returned to main Index view without the filtering criteria still set (forcing them to go in over and over and add filtering criteria before performing the similar EDIT on records of the same criteria).
My POST-EDIT method is currently setup to include:
if (ModelState.IsValid)
{
collection.MODIFIED_DATE = DateTime.Now;
collection.MODIFIED_BY = System.Environment.UserName;
db.Entry(collection).State = EntityState.Modified;
await db.SaveChangesAsync();
return RedirectToAction("Index", "Home");
}
For my attempts I had first thought to return the View with the updated collection (return View(collection)) but this of course just takes me back to the EDIT view, not the home view with the data grid filtered down as previously specified. I considered adding a field in the database, something like LAST_FILTERED_URL, but this just feels like an overgrown band-aid.
Does anyone know of a clean way to go about this?
EDIT:
I had thought to do something similar to Andrea's suggestion early on, but had not thought of doing an explicit redirect with the Parameter of the url-filter passed in the Redirect. Below is my current code for the GET/POST Edit:
// GET: ENITTY_Collection/Edit/5
public async Task<ActionResult> Edit(int id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
ENTITY_COLLECTION entity_Collection = await db.ENTITY_COLLECTION.FindAsync(id);
if (entity_Collection == null)
{
return HttpNotFound();
}
// Other code for Controls on the View
return View(entity_Collection);
}
// POST: ENTITY_Collection/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Edit([Bind(Include = "Id,One_Id,Two_Id,Three_Id,Four_Id,Five_Id,Six_Id,field7,field8,field9,...field18,created_date,created_by,modified_date,modified_by")] ENTITY_COLLECTION entity_Collection)
{
if (ModelState.IsValid)
{
entity_Collection.MODIFIED_DATE = DateTime.Now;
entity_Collection.MODIFIED_BY = System.Environment.UserName;
db.Entry(entity_Collection).State = EntityState.Modified;
await db.SaveChangesAsync();
//return RedirectToAction("Index", "Home");
return View(entity_Collection);
}
// Other code for if Model is Invalid before returning to View.
return View(entity_Collection);
}
I like Andrea's suggestion, but I still need a good way to store the URL the user has when they first navigate to the GET-Edit View, and then use that filtered URL value to return the user to that previous location & filter option when the POST-Edit completes and changes have saved.
Any thoughts?
I'm not sure if this is the most correct way of going about what I'm after, but what appears to be working for me is the use of a Session value.
In my GET method I store the URL:
Session["returnURL"] = Request.UrlReferrer.AbsoluteUri;
Then in my POST I use this value in a Redirect() after saving changes to the record:
var returnURL = (Session["returnURL"] != null) ? Session["returnURL"].ToString() : Url.Action("Index", "Home");
return Redirect(returnURL);
So far all initial testing is resulting in a return to the main view with all sorting/filtering criteria in place before the record was entered into for update.
Have you tried changing passing the current filter to redirect to action as follows?
Note: I am assuming that:
you are redirecting to the same controller
you have a controller parameter called currentFilterValue
RedirectToAction("Index", "Home",new { grid-filter = currentFilterValue });
The default LoginController for an MVC project from Microsoft includes a bunch of methods that use a returnUrl parameter. Using this practice, you could include a return URL when opening the editor, that when editing is done, returns the user back to the prior screen with the filters intact (assuming they are in the URL).
My base class for view models has a property for storing the ReturnURL, which is then stored as a hidden if set.
#Html.HiddenFor(model => model.ReturnUrl)
My action that posts from edit then has this logic:
if (viewModel.ReturnUrl.IsNotNullOrEmptyTrimmed())
{
return RedirectToLocal(viewModel.ReturnUrl, "ActionName", "ControllerName");
}
else
{
// Default hard coded redirect
}
In order to prevent some injections, you will want a validation method (called above) like this for ensuring the URL is valid:
protected ActionResult RedirectToLocal(string returnUrl, string defaultAction, string defaultController)
{
try
{
if (returnUrl.IsNullOrEmptyTrimmed())
return RedirectToAction(defaultAction, defaultController);
if (Url.IsLocalUrl(returnUrl))
return Redirect(returnUrl);
Uri returnUri = new Uri(returnUrl);
if (Url.IsLocalUrl(returnUri.AbsolutePath))
{
return Redirect(returnUrl);
}
}
catch
{
}
return RedirectToAction(defaultAction, defaultController);
}
Hopefully this gets you some ideas and on the right track.

TempData[] getting wiped despite no requests taking place

TempData is supposed to persist for a subsequent request. However when I check the TempData in a subsequent controller I get a null. Why is TempData[] getting wiped off. Here are my two controllers. I am calling them in succession. So TempData[] should persist from one request to the next. Here is code from controller 1:
[HttpGet]
public ActionResult EditCampaign(int? Id)
{
ViewBag.Banner = bannerText.Replace(" ", " ");
// This is passed in case we need to add a new product or edit one. Then we'll
// need the campaign id for reference purposes.
TempData["campaign_id"] = Id;
TempData["campaignBanner"] = bannerText.Replace(" ", " ");
ViewBag.StartDate = debugger.startDate.ToShortDateString();
ViewBag.EndDate = debugger.endDate.ToShortDateString();
int Id1 = Convert.ToInt32(TempData["campaign_id"]);
int convertedId1 = Id1
Here is where I try to get the value in Controller 2:
[HttpPost]
public ActionResult EditCampaign(CampaignDTO camps)
{
// Do this up top. i.e. get the campaign ID
camps.Id = Convert.ToInt32(TempData["campaign_id"]);
string success = "";
I suspect that assigning the value of TempData in the penultimate line of controller 1 might be wiping off the data. If thats the case that is something new and not documented. Please help.
Reading the data is enough to remove it. Here's the relevant source:
public object this[string key]
{
get
{
object value;
if (TryGetValue(key, out value))
{
_initialKeys.Remove(key);
return value;
}
return null;
}
set
{
_data[key] = value;
_initialKeys.Add(key);
}
}
So in controller 1, when you say:
int Id1 = Convert.ToInt32(TempData["campaign_id"]);
you are actually removing the data you just inserted.
There are many ways to work around this, but if you must read it from TempData, use Peek() instead.
You have some flawed info. The data stored in the TempData property persists for only one request.
http://msdn.microsoft.com/en-us/library/system.web.mvc.viewpage.tempdata%28v=vs.118%29.aspx
TempData is useful when you are redirecting (HTTP 302 or 303, i.e. RedirectToAction in MVC) to pass along some context. It does not persist across separate HTTP requests. Based on your code sample, it suggests your client code first makes a GET to controller one, then issues a second request to controller two.
Your best bet is probably to return the necessary data as part of your response in controller one, then make that part of the incoming request to controller. You could potentially also use Session variables, https://code.msdn.microsoft.com/How-to-create-and-access-447ada98.

RedirectToAction() destroys string data

Inside my controller I have string variable
private string notificationMessage = "";
which I want to use to copy it's content to ViewBag.Message and display that content on the view.
So inside my edit action I populate it's (notificationMessage) content like this
notificationMessage = "data is succ. updated!";
return RedirectToAction();
But after redirection to Index action this string variable is empty;
How can solve this?
Use TempData instead of ViewBag. It persists between requests.
It is because RedirectToAction returns an HTTP 302 response to the browser, which causes the browser to make a GET request to the specified action. Since HTTP is stateless, you can't simply set something in one action and get it in another(when it is another GET request).
What you can do is
1) pass a querystring to your new action and check that in the next action method and show a message according to that.
return RedirectToAction("ThankYou","Account",new {msg="success"});
and in your ThankYou action
public ActionResult ThankYou(string msg)
{
var vm=YourSuccessViewModel();
if(msg="success") // you may do a null checking before accessing this value
{
vm.Message="Saved Successfully";
}
return View(vm);
}
2) Store in a temporary place like Session / TempData. TempData is meant to be a very short-lived instance, and you should only use it during the current and the subsequent requests only!
TempData["UserMsg"] = "Saved Successfully";
return RedirectToAction("ThankYou","Account");
and in your ThankYou action you can read it and show to user as needed.
public ActionResult ThankYou(string msg)
{
var msg = TempData["UserMsg"] as string;
//to do : do what you want with the message and return the view.
}
Session object is the backing store for the TempData object, and it is destroyed more quickly than a regular session, i.e., immediately after the subsequent request.

ASP.NET MVC 3 - Detect Whether Current Page Was Redirected To in a Post Redirect Get Workflow

In my C# .NET 4 MVC 3 application I have a delete controller for a set of CRUD pages which uses the Post Redirect Get pattern to redirect to an Index controller after a successful delete. I would like to render a button on the Index page only if this page was NOT redirected to by such an action. Is there a simple way to detect if the current page was redirected to (i.e. was reached as the result of a PRG redirect)?
After reading http://blog.simonlovely.com/archive/2008/11/26/post-redirect-get-pattern-in-mvc.aspx my current approach is to set this in my delete controller with TempData after the DeleteMyEntity method has succeeded:
try {
MyService.DeleteMyEntity(MyViewModel.MyEntity);
TempData["Redirected"] = true;
args = new RouteValueDictionary(new { Foo = 1, Baa = 2 });
return RedirectToAction("Index", args);
} catch (Exception e)
{
//Logging etc. - redirect should never be reached on exception (and TempData item not set)
throw(e);
}
then in my Index controller I check to see if this value exists and is true:
if (TempData["Redirected"] != null)
{
//we can then do something useful with this
}
Another opportunity I see would be to add another item to args and check for this in the controller, but in this case I may as well just use TempData. Is there a way to do this using a HTTP Response code on the request without needing to pass this data through with TempData or a similar mechanism?
another route would be to set up a global actionfilter that "injects" that flag for you...
public class RedirectDetect: ActionFilterAttribute{
public override void OnActionExecuted(ActionExecutedContext filterContext){
if (filterContext.Result is RedirectToRouteResult ||
filterContext.Result is RedirectResult)
{
TempData["Redirected"] = true;
//or what ever other indicator you want to set
}
}
}
And then you can just call redirectToAction("Index") and then check in your receiving handler
sidenote: I challenge you to say RedirectDetect aloud and not smirk.
I use TempData in a similar fashion - for instance, to show a status message (after redirecting to) my view when a record has been added / updated / deleted. This is the kind of simple, throw-away stuff that TempData is used for, so I say what you have is appropriate.
Personally I wouldn't mess with HTTP status codes unless I had an absolute need for it. And you could probably do something with the referrer http header, but again, that would be much messier and more complicated than just using TempData. You have a clean, simple solution that works, I say go with what you have.
I am not aware of any simpler mechanism and have been using TempData for quite some time to implement Post-Redirect-Get features. As far as I know, this is specifically one of the reasons for TempData's existence. I would continue using it.
There is no way to tell the difference between requests as a result of a 3xx redirection or a straightforward user-initiated GET. The best way is to provide a querystring argument that is only appended by the redirection for the initial POST request, but there is nothing stopping the user from reloading the page with the same querystring.
Hmmm, or you could send a cookie with the redirection from the POST, then remove the cookie in the response of the subsequent GET, like so:
public ActionResult PostHandler(ViewModel model) {
SetCookie("redirected", "true"); // psuedocode
return Redirect("GetHandler2");
}
public ActionResult GetHandler2() {
if( GetCookie("redirected") == "true" ) {
// do something
}
DeleteCookie("redirected");
}
Building off of George's answer:
public class MarkRedirects : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext context)
{
if (context.Result is RedirectToActionResult ||
context.Result is RedirectResult)
{
// Obtain and verify the underlying IController
var controller = context.Controller as Controller;
if (controller != null)
controller.TempData["Redirected"] = true; // or set other dictionary data here
}
}
}
The conditional checks of context.Result can vary based on what method you used to redirect, for instance if you redirected the user via the RedirectToAction() method, context.Result is RedirectToActionResult will return true but context.Result is RedirectToRouteResult will not.
Because of this, you will want to change up that conditional based on your personal taste of how you redirect users. The current code would work for OP's situation.
If you're going to be using this everywhere, it may be wise to modify a base controller.

Categories

Resources