Is using TempData to pass details between controller actions bad practice? - c#

I have certain situations where I need to pass a value between controller actions.
When passing a returnUrl from a view to all nested views. In the
view I have
#{
TempData["returnURL"] = Request.Url.AbsoluteUri;
}
and then access it in a similar way to this (in my real version I
check that the key is in TempData and that the returnURL is a real
URL):
return Redirect(TempData["returnURL"].ToString());
If it needs to continue on past the first page change (i.e. Search
page -> Edit page -> Edit Section page) I'm adding it again
TempData["returnURL"] = TempData["returnURL"];
When I need to pass a value from one controller action through a
view to another controller action that is called by ajax such as
here:
public ViewResult Index(FormCollection form)
{
var model = new GridColumnChooserViewModel();
//Select deleted/not deleted rows
if (form.HasKeys())
model.ShowRows = (form["deletedDropDown"] == null) ?
"Active" :
GetOptionByName(form["deletedDropDown"]);
TempData["ShowRows"] = model.ShowRows;
...
}
and then in my other ajax-called action controller I access it:
public JsonResult GetData()
{
//Select deleted/not deleted rows
var showRows = (TempData.ContainsKey("ShowRows") && TempData["ShowRows"] == null) ?
"Active" :
GetOptionByName(TempData["ShowRows"].ToString());
//refresh tempdata showrows so it is there for next call
TempData["ShowRows"] = model.ShowRows;
return this.GetDataSource(showRows);
}
My question is, is this really bad practice? From my understanding of it, I'm essentially using TempData like a session cookie. Is there a better way to do this, like using an actual cookie?

Yes, I would say that this in general is bad practice. While the ViewData dictionary approach is fast and fairly easy to implement it can leads to typo's and errors that are not caught at compile time. An alternative would be to use the ViewModel pattern which allows you to use strongly-typed classes for the specific view you need to expose values or content within. Ultimately giving you type safe and compile time checking along with intellisense.
My first choice would be to use a view model. If that doesn't fit then using session state may be just fine.

It seems like you are using TempData to flow state through the various pages of your site; in general, I'd say this is a bad practice.
Ideally, you would flow whatever upcoming state you would need to the client, and the client would store it (in some sort of JSON or whatever). Then, the client would return it to you as part of their action, and then you'd pass back the appropriate state, etc.; it speaks more to the stateless nature of HTTP applications.

I changed both situations to use Session so that I didn't have to keep pushing the TempData value on.
public ActionResult Create()
{
Session["returnURL"] = Request.UrlReferrer.AbsoluteUri;
...
}
Then I access it like this
var returnURL = (Session["returnURL"] != null) ? Session["returnURL"].ToString()
: Url.Action("Index", "Home");
Seems a bit better.

Related

asp.net mvc ViewState implementation

I working on a web project where I first get data from the database and bind to the Html control. If there is a validation error I will send the same view back for rendering with the displayed validation errors. When the page comes up, there is an exception. I stepped through the code and found that the model was passed will null collection. Basically any property that was not binded to a textbox was changed to null. I was told not to use session or viewdata to keep temp storage. So I call a method SaveViewState where it save all the property value of the ViewModel property to a static variable like so
private static MyViewModel _viewModel;
private MyViewModel SaveViewModel(MyViewModel viewModel)
{
if (_viewModel == null)
{
_viewModel = new MyViewModel ();
}
if (!string.IsNullOrEmpty(viewModel.MyName))
_viewModel.MyName= viewModel.MyName;
if (!string.IsNullOrEmpty(viewModel.Number))
_viewModel.Number= viewModel.Number;
if (!string.IsNullOrEmpty(viewModel.Address))
_viewModel.Address= viewModel.Address;
if (!string.IsNullOrEmpty(viewModel.State))
_viewModel.State= viewModel.State;
}
It works but I think it is very inefficient and there must be a better way to implement ViewState in MVC with Session or ViewData or HiddenFields? By the way, I was told not to use those three.
Any help is appreciated. Thanks.
I am not sure if this solution is worse than using a session or hidden fields. In your action you should return the corresponding view with the same model that was posted. The ActionResult should be something like this:
public ActionResult SomePost(SomeModel model)
{
if (!ModelState.IsValid())
{
//error in validation
return View(model);
}
//post save redirect and stuff
return ... redirect?
}
The ModelState.IsValid() will test according to the DataAnnotations. Standard attributes like [Required], [MaxLength] etc. are available.
In this configuration, the use of a SaveViewModel function is not required. If your collection is null after post: re-query it, post it or fetch it from a ViewData like object.
There are good reasons not to use those three you mentioned, but if you know that reason you might want to consider it:
1) Use of session: will make scalability difficult because every request in a session must hit that specific server.
2) Hidden fields: Not really a problem IFF you realize the hidden field can be manipulated in a browser. So don't store ID's there
3) ViewData: basically breaks the MVC pattern; you can use it to store data but that's what a model is for. It totally legitimate to use ViewData from a filter. To provide some general functionality for example.

Pass data between Views in MVC

I'm working on a Web Application project using C# and MVC that will take in two URLs in a form and use them to create an instance of a class I have created called "ImageSwap." This model has some data (a username of the person performing the swap; two variables which hold the URLs of two images to be swapped; two variables which save the actual names of these files without all of the rest of the URL information; and two arrays which represent the file locations to check for these files). Right now, I have it so that the initial index view creates the instance of the class and passes it to the same view with the information put in through the form, submitted via POST, like so:
public ActionResult Index()
{
Ops.Operations.Models.ImageSwapModel newImageSwap = new Models.ImageSwapModel();
return View(newImageSwap);
}
[HttpPost]
public ActionResult Index(ImageSwapModel imageSwap)
{
var oldFileFound = false;
var newFileFound = false;
if (ModelState.IsValid)
{
//Perform data manipulation and set needed values
}
}
It then performs some functions on the data, such as parsing out the filename at the end of the URL, and a directory number (which is the first part of this filename, i.e. directory#_fileName.jpg). All of this works fine.
My problem is that I would like to pass this model to another view once it has data populated in all of its fields by this initial ActionResult so that I can have a verification view, which would allow the user to preview the two files side by side so that they can ensure they are swapping the appropriate images. They should then be able to hit another submit button which will initiate the actual moving/replacing of the images and be taken to a page confirming.
Is there a way to pass data from this controller to a different view? My confusion arises because I cannot create another version of an ActionResult of Index with the same input, but I do not want to have the actual swapping of the images occur without a preview and a prompt. Should I re-write my Index view so that it utilizes partial views in order to accomplish this? What is the easiest way to have data persist through multiple steps and views?
What is the easiest way to have data persist through multiple steps
and views?
Your question sounds like you're trying to achieve what you can easily do with sessions. The session object allows you to persist data between requests simply by adding it to the Session object on the HttpContext that exists within the base class that your controller extends, like so:
(Note the Serializable attribute. This allows your object to be serialized into the session object).
[Serializable]
public class ImageSwapModel {
// Your class's properties
}
Then in your controller you can do the following:
[HttpPost]
public ActionResult Index(ImageSwapModel imageSwap)
{
var oldFileFound = false;
var newFileFound = false;
if (ModelState.IsValid)
{
this.HttpContext.Session["ImageSwap"] = imageSwap;
}
}
When you want to retrieve the model you can grab it from the session like so:
var imageSwap = (ImageSwapModel)this.HttpContext.Session["ImageSwap"];
Taking it one step further:
Whilst the above will work fine, generally it's not a good practice to reference the HttpContext object directly in your code as it creates unnecessary coupling to the HttpContext object that can easily be avoided. Instead you should opt to inject an instance of the session object via Dependency Injection. Here is a similar answer that provides a basic idea as to how you can do this.
You can return different views with Models being passed to them in your one Index action like
if(some condition)
{
Return View("ViewVersion1", MyModelVersion1);
}
else
{
Return View("ViewVersion2", MyModelVersion2);
}

passing in an additional parameter with the model to a View

Still learning Mvc so sorry if this question is a bit weird.
I am passing the model in my Controller method and want to pass an additional parameter.
what I want to achieve is something like this but the parameter overload does not allow for the 'additional' parameter.
#using (Html.BeginForm("SubmitCall", "Home", new { Id = Model.ProductDetail.ProductId }))
and then in the controller
public ActionResult SubmitCall(ViewModel model, int Id)
{
return RedirectToAction("DetailViewById", model, new {productId = Id});//not allowed
}
Is there a better way of achieving this?
Kind regards
If your only intention is to redirect the client to another URL then the above scenario is not the right way to achieve this.
RedirectToAction will send a HTTP 302 with the Location information generated from your route information (again, in your case this would be the URL to DetailViewById with productId as a parameter or part of the URL - depends on your route configuration).
If you need to persist the ViewModel that is submitted by the client and operate on it once the user is requesting your redirected action, then i would suggest to use TempData which is designed for between-request persistence of such data.
Usually, the ViewModel is a combination of what you have coming back from database (DTO) + extra bits and pieces you need TO and FROM View.
So in my opinion, you should add this extra property to your Viewmodel.
Create an anonymous object for parameters:
return RedirectToAction("DetailViewById",
new {
productId = Id,
model.Property1,
model.Property2,
...
});

How to properly resolve RedirectToAction from throwing Object Not Set To Instance

There is probably a very simple answer to this question but I'd like to know the best practice for handling this type of scenario. Running MVC4, I have a page that performs a redirects to a Controller action that performs a lookup and dynamically renders content on another page.
The page makes a call to the following controller for the lookup:
public ActionResult GetMembershipLevel(string userid)
{
var memberDetails = MemberDetails.GetMembershipDetails(userid);
TempData["MemberDetails"] = memberDetails;
var memberLevel = memberDetails.MemberLevel;
switch (memberLevel)
{
case 0:
return RedirectToAction("Basic");
case 1:
return RedirectToAction("Gold");
case 2:
return RedirectToAction("Platinum");
}
return null;
}
After the lookup, based on the Members details the data is stored in the TempData object and Redirected to an ActionResult within the same controller to provide the model data that get sent to 1 of potentially 3 views. I did this because each view will render a different set of the Model based on Member classifications:
public ActionResult Basic()
{
var details = (MembershipDetail)TempData["MemberDetails"];
ViewBag.Member = details.IndividualName;
return View(tuple);
}
In this code snippet, the Model is returned to a View called Basic and all this works fine. Except in the case that a manual refresh is made within the Browser. Then all Hell breaks out with the notorious "Object Not Set To an Instance Of an Object" at the line "ViewBag.Artist = details.IndividualName;"
I know the reason it's happening is because a refresh of the same page cannot and does not repeat the entire cycle that landed the user on the page. It was a Redirect with the user's selected Id initially and a refresh simply refreshes the existing page without the Id parameter causing the TempData to be null on subsequent refreshes.
I did some research and found a couple of suggestions.
1. Add modules runAllManagedModulesForAllRequests="true" to the WebConfig. (I ruled this out as a potential fix immediately as I don't believe it is relevant to my problem.
Decorate my Controller with the [HandleError] attribute. (This actually worked and alleviated the problem)
But my question, "Is this the correct and best method of handling the issue?"
I read concerning the attribute, "When you provide only the HandleError attribute to your class (or to your action method for that matter), then when an unhandled exception occurs MVC will look for a corresponding View named "Error" first in the Controller's View folder. If it can't find it there then it will proceed to look in the Shared View folder (which should have an Error.aspx file in it by default)"
So it leads me to believe that implementation of the [HandleError] attribute should require a corresponding View.
If this is correct; It seems what I've accomplished is resolution of the issue but not much more than a comparable try catch block that doesn't implement the catch.
Right or Wrong? Is this method acceptable under the circumstances?
I believe you should design your application in a different way.
Instead of having different actions that you redirect to (what stops a user from simply typing in a url with Gold instead of Basic?) you should instead have a single page that renders the correct view based on their membership status. This essentially solves your entire problem. Plus it makes it impossible for someone to type in a membership level they don't have.
Essentially, you would do this:
public ActionResult Membership()
{
var memberDetails = MemberDetails.GetMembershipDetails(userid);
var memberLevel = memberDetails.MemberLevel;
switch (memberLevel)
{
default:
case 0:
return View("Basic", memberDetails);
case 1:
return View("Gold", memberDetails);
case 2:
return View("Platinum", memberDetails);
}
}

Need to access a variable from all Views

On the site I'm working on, there are users with different permissions on the site.
Given the schedule ID and employee ID that we're currently looking at, we can get their role-specific permissions.
Right now, our BaseModel has a property that properly accesses the DB and grabs this info.
For all views that pass a model to the view, everything runs fine.
The problem lies in Controller Methods where no model is passed. In a few views, all they're supplied is a few ViewBag entries, and work fine.
However, I /need/ the CurrentPermissions property in those pages nonetheless, for the layout. Whether or not the permissions have one boolean value set true/false, something may/may not be displayed/populated.
So, my option seem to be:
Somehow throw my CurrentPermissions into a ViewBag entry for all views, and access them through that instead of the base model.
I'm not sure how to do this. I've seen people using OnActionExecuting, but that fails since my connection to TransactionManager is not yet set up at that point.
Somehow throw just the BaseModel into those views that don't currently pass a model. I'm refraining from this as much as possible. I'm not sure how I would go about doing such, but it seems like that would over-complicate the situation.
How can I go about pushing this CurrentPermissions object (generated from a call to my TransactionManager) to every view (specifically, the Layouts!)
Your approach is what we use in out projects... and we use this approach to systematically remove the use of ViewBag changing it to ViewModels.
Other approach we have used (for UserPreferences in my case) is adding an ActionFilter that ends including the preference in the ViewBag. You decorate the actions needing it with [IncludePreferences] in my case (that is the name of my filter attribute.
EDIT ActionFilter:
public class IncludePreferencesAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var controller = filterContext.Controller as BaseController;
// IController is not necessarily a Controller
if (controller != null)
{
//I have my preferences in the BaseController
//and cached but here you can query the DB
controller.ViewBag.MyPreferences = controller.TenantPreferences;
}
}
}
In you action you decorate it using [IncludePreferences]
As a temporary solution, I'm doing the following at the top of my Layout:
#{ OurModel.SupervisorRestriction CurrentSupervisorRestrictions = ViewBag.CurrentSupervisorRestrictions ?? Model.CurrentSupervisorRestrictions; }
This way, if we're passing in an object then it works just fine. Otherwise, I'll directly pass in a ViewBag.CurrentSupervisorRestrictions from the controller. There are only a few cases, so it's not that bad.
Better suggestions would be great, though.

Categories

Resources