I'm working on an application using ASP.NET 4.0 and MVC 2.0. If it's in any way relevant, I'm using VS2010.
I'm running into complications with TempData. I did not write the original code, but it isn't working correctly and I'm attempting to fix it. I don't have a lot of experience working with TempData and ViewData.
I have an Index action as follows (pseudocode):
public virtual ActionResult Index()
{
var vm = new IndexViewModel();
// some code here to set up the ViewModel
if (TempData.ContainsKey("Success"))
vm.Success = true;
return View(MVC.Controller.Views.Index, vm);
}
And I have a POST action as follows (pseudocode):
[HttpPost]
public virtual ActionResult Index(IndexViewModel vm, List<int> formData)
{
if (DoSomethingWithData(formData))
{
TempData["Success"] = true;
return RedirectToAction(MVC.Controller.ActionNames.Index);
}
TempData["Message"] = "Failed to use formData";
return View(MVC.Controller.Views.Index, vm);
}
The view emits a form and prefaces it with a success message if vm.Success is true. It will also emit the message in TempData["Message"] if it is present.
The first time I come to the page I get just the form. I enter INVALID form data and submit it... and I get the form prefaced by the error message as expected. (I know there's poor design here since it doesn't redirect... and you get poor user experience with refresh, etc. but I'm not worried about that yet) This is all great.
The problem manifests when I use VALID form data. If I submit valid form data, I get the page back prefaced with a success message as expected, but if I refresh the page the success message is still there. Indeed if I go to a completely different part of the site and navigate back, the success message is still there. For some reason after a redirect and read, the tempdata is still there. There has been both a redirect and a read... shouldn't the temp data now be clear?
I'm reasonably certain that the other places I navigate to aren't setting TempData["Success"] for any reason, but to be sure I've navigated to things like Google, and come back directly to the URL for this page, and it still seems as though TempData["Success"] is populated.
It's very clear that either I don't clearly understand how TempData is supposed to function (unsurprising) or something unusual is happening that I simply don't have the experience to see.
Any advice is welcome!
Thanks,
Dave
[EDIT]
The view doesn't actually emit the form when there's a success message... it only emits the success message.
The view looks more or less like this:
<% if (TempData.ContainsKey("Message")) { %>
Emit message...
<% } %>
<% using (Html.BeginForm(MVC.Controller.ActionNames.Index,
MVC.Controller.Name,
FormMethod.Post,
new { id = "form"})) { %>
<% if (!Model.Success) { %>
Emit form...
<% } else { %>
Emit confirmation message...
<% } %>
<% } %>
Francisco pointed me towards something I hadn't considered... but it turns out the constructor for the viewmodel sets Success to false... so it's not something odd with that. I know for sure that TempData["Success"] is still set (rather than something like foolishly reusing a viewmodel with success set to true) because I've stepped through the code and it continually steps into that if statement where it sets vm.success = true, even after a refresh.
Microsoft has made a change to the behavior of TempData that we need to be aware of in MVC 2 and 3. TempData now no longer completely clears at the end of a controller action cycle. TempData can now (automatically and without you changing anything) persist through to other pages. TempData keys are now only cleared if they’ve been read. Furthermore, if you use RedirectResult or RedirectToRouteResult, they will persist even if they are read.
Here are more details: warning-mvc-nets-tempdata-now-persists-across-screens
Just adding this as I said in my comment. I suggest to do
if (TempData["Success"] != null)
vm.Success = true;
Instead of
if (TempData.ContainsKey("Success"))
vm.Success = true;
... so it counts as a TempData read. Glad it worked.
Regards
Related
Im trying to style my login page. My login url is website/Security/login. Im trying to locate the 'login' piece of the url. What have i done wrong below?
public function DisplayPageType() {
$param = $this->request->param('Action');
if ($param === 'login')
{
return 'Login';
}
Thanks
I think that won't work since the controller during render is the Page_Controller and not the Security controller. So the $Action param is not equal to login. It could be index, I'm not sure.
If you just want to check if you're in the login page, you can add this to your Page_Controller:
public function getIsLoginPage()
{
return $_REQUEST['url'] == '/Security/login';
}
Then in your template:
<body class="<%if $IsLoginPage %>login-page<% end_if %>">
A bit dirty but it's the quickest way I know.
Another way is to leverage SilverStripe's legacy support. You can add a css file called tabs.css at mysite/css/tabs.css. If this file exists, SilverStripe will include this in the page.
You can also create templates that SilverStripe will automatically use if they exist:
themes/<theme_name>/Security.ss - If you want your login page to use an entirely different layout.
themes/<theme_name>/Layout/Security_login.ss - If you want to change just the content part (the $Layout section)
I hope this helps.
#gpbnz is right, the $Action param is not equal to login, it actually returns null as accessing $this->request from the Page_Controller when accessing the Security/login returns a NullHTTPRequest.
To get the action, you will want to get the current controller using Controller::curr(). It is then as simple as calling getAction on this controller.
To confirm that the action isn't from a random controller that happens to have an action called login, you can check the instanceof the controller like so: Controller::curr() instanceof Security
This check will still allow it to work for any controller that extends Security though which may/may not happen depending on the project.
I would stick away from actually reading the URL for the information manually though as that can create issues with maintainability in the future.
To bring this to a nice little function:
public function isLoginPage()
{
$controller = Controller::curr();
return $controller instanceof Security && $controller->getAction() == 'login';
}
Otherwise #gpbnz had a good suggestion of using the template system to your advantage for overriding not only the styles but the HTML around it.
I'm trying to implement a user login form, I want to achieve:
when username doesn't exist in DB, return a flash message with previous
form data prefilled.
if any server side validation errors happened, return back to previous page, with old data pre-filled, display the errors
messages along side.
The problem now is, if I use flash scope, I need to use Redirect after post, but this will lose the pre-filled data.
If I use any status other than Redirect, I can't put data into flash scope.
What did I missing?
Don't use a Redirect for a failed login. You can return the same Form back to the login view with extra errors attached to it.
Something like this:
loginForm.bindFromRequest.fold(
formWithErrors => views.html.login(formWithErrors),
credentials => {
if(authenticate(credentials)) // dummy implementation
Redirect(controllers.Application.index)
else
BadRequest(views.html.login(loginForm.fill(credentials).withGlobalError("Incorrect login credentials")))
}
)
Then your view signature would look something like this:
#(loginForm: Form[Credentials])
#* Displays the first global error from the form, if any. *#
#loginForm.globalError.map{error =>
<h3>#error.message</h3>
}
And you'd pre-fill the form with values as before (I hope).
If there are multiple global errors, you can access them with globalErrors as it will access a Seq[FormError] instead of Option[FormError].
You can also attach errors to specific Form keys.
loginForm.withError("email", "I don't like your email.")
And would access them similarly:
#loginForm.error("email").map{ error =>
#error.message
}
You are using wrong concept, take a look to the Handling form submission doc, section: Validating a form in an Action
If form contains errors you are returning BadRequest (not Redirect)
If form IS valid anyway next check (i.e. DB query) returns an error you should do exactly the same thing as in formWithErrors so render the view passing incoming form to it (userData)
Finally if everything's OK, you can make your operation and redirect the user i.e. to main page or something...
Pseudo code (basing on doc):
def userPost = Action { implicit request =>
userForm.bindFromRequest.fold(
formWithErrors => {
BadRequest(views.html.user(formWithErrors))
},
userData => {
// Check whatever you need...
if (afterCheckSomethingIsWrong){
// if something's wrong fill the `userForm` with `userData` and render the same view again...
// You can use flash scope here i.e. for placing error message
BadRequest(views.html.user(userForm.fill(userData))).flashing("error" -> "The account doesn't exist")
} else {
// if everything is OK, redirect to some page, outside the form handling process, i.e. main page
Redirect(routes.Application.index).flashing("success" -> "Fine you're logged now")
}
}
)
}
We have a number of forms on our site that are shown with jquery .dialog and we submit them using an ajax post request.
I've done a fair bit of research and from what I can tell there isn't any definite patterns on how to return validation errors from the server side as a result of an ajax request.
Pretty much the only pattern I could find was to post the form and then do validation server side and in either case return a json object that encapsulated a result and if the result was incorrect the form html
ie.
{result: true}
{success: false, html = "<form>....</form>"}
So in the case the result was false you would need to rewire up any events attached to items on the form as you would be replacing the old form with the new form that has the validation errors.
This seems like an ok approach but you also end up potentially returning a lot more data to the client that you need to when they only really need to validation messages and you are also forced to rewire the form up which is a bit annoying.
I did find one other mention of somehow getting the validation errors and returning them in a json object from the action and somehow telling the client to display them against the correct fields.
Is there any frameworks out there that make this easier or should I write my own or just stick to returning the entire partial for the form and rewiring it when validation is incorrect?
I don't know of any frameworks that handle this particular case–and I don't know that there's a clear best practice–but it's easy enough to serialize validation errors and return them as a JSON object. You could try this extension method on ModelStateDictionary:
public static IEnumerable<ValidationResult> GetValidationResults(this ModelStateDictionary dictionary)
{
foreach (var key in dictionary.Keys)
foreach (var error in dictionary[key].Errors)
if (error != null)
yield return new ValidationResult(error.ErrorMessage, new string[] { key });
}
And in the controller:
if (!ModelState.IsValid)
{
return new JsonResult(ModelState.GetValidationResults());
}
But you're right, you would then have to loop through the object and append the errors to the correct fields. If you have ClientValidationEnabled and UnobtrusiveJavaScriptEnabled set to true, the loop would look something like this:
$.each(errors, function(i, item) {
$('span[data-valmsg-for=' + item.MemberNames[0] + ']').html(item.ErrorMessage);
})
If not, it wouldn't be that difficult to match up the error messages to their respective fields as the object contains the field name. This would definitely save you some data across the wire, but it moves a larger share of the validation responsibility into Javascript. Like I said, I don't know that there's a clear best practice, but I have used this method in the past with success.
I'm using TempDate["Message"] to show little update banners as the user does things on my site like this:
[AcceptVerbs(HttpVerbs.Post), Authorize(Roles = "Admins")]
public ActionResult Delete(int id)
{
_Repo.DeletePage(id); // soft-delete
TempData["Message"] = "Page deleted!";
return RedirectToAction("Revisions", "Page", new { id = id });
}
Then in my master page I have this:
<%-- message box (show it only if it contains a message) --%>
<% string Message = (TempData["Message"] ?? ViewData["Message"]) as string;
if(!string.IsNullOrEmpty(Message)){
%>
<div id="message"><%:Message %></div>
<% }
TempData["Message"] = null; ViewData["Message"] = null; %>
I hit both TempData and ViewData because I read somewhere that TempData should be used for redirects and ViewData should be used otherwise.
The issue is: often the message won't show up right away. Sometimes it takes a click or two to different parts of the site for the message to show up. It's very strange.
Any ideas?
You should verify all places where you use TempData["Message"] in your code. Corresponds to ASP.NET MVC does browser refresh make TempData useless? you can read TempData["Message"] only once (see also http://forums.asp.net/p/1528070/3694325.aspx). During the first uage of TempData["Message"], the TempData["Message"] will be deleted from the internal TempDataDictionary.
Probably it would be better to use TempData["Message"] only inside of Revisions action of the Page controller and not inside of master page or inside a View.
TempData is not intended to pass data to views, hence the name ViewData for that purpose. In fact, I can't think of a reason to use TempData from within a view definition at all...
One very common usage of TempData is the passing of information between controller actions when you do a redirect (the Revisions action in your example above, for instance, would be able to make use of your TempData["Message"] variable).
This is common practice in the PRG means of coding MVC interactions (Post-Redirect-Get) since you often need to pass information from the initial target action when doing the Redirect to the Get. An example of how this might be useful in a Get is below where I often just default to a new viewmodel UNLESS there is one already passed from a redirect in TempData:
public ActionResult System() {
SystemAdminVM model = (SystemAdminVM)TempData["screenData"] ?? new SystemAdminVM();
One more thing; I see you explicitly clearing your TempData and ViewData dictionary entries in your view. You don't need to do that as by that point they are at the end of their life spans anyway...
Happy coding!
Your app's behavior is the one you'd expect if you're using TempData where you should be using ViewData.
You want to double-check that you're storing your status feedbacks in TempData only when the controller does a re-direct. Otherwise, you should use ViewData.
This smells like you need a couple of unit tests to confirm the behavior you're seeing. Try writing up a couple using this example as a starting point:
http://weblogs.asp.net/leftslipper/archive/2008/04/13/mvc-unit-testing-controller-actions-that-use-tempdata.aspx
If you have configured multiple worker process for your application, but session state mode is "InProc", then you can't use default TempData implementation, as session state becomes unusable. (see ASP.NET session state and multiple worker processes)
You could try to use MvcFutures CookieTempDataProvider instead.
Then I'm trying to use Zend_Form_Element_Hash it regenerates a hash every request.
In my code:
// form
$this->addElement('hash', 'hihacker', array('salt' => 'thesal'));
Then I dumping $_SESSION I see a new value each page reload.
Then I send a form it reports an error "The token '28a5e0e2a50a3d4afaa654468fd29420' does not match the given token 'a64407cc11376dac1916d2101de90d29'", each time - new pair of tokens
$form = new Form();
$form->addElement('hash', 'hihacker',
array('salt' => 'YOUR TOO MUCH SALTY TEXT !!##'));
if ($this->_request->isPost() && $form->isValid($this->_request->getPost())) {
// Valid ! you are safe do what ever you want .
} else if (count($form->getErrors('request_token')) > 0) {
///get him to the error controller
$this->_forward('csrf-forbidden', 'error');
return;
}
its working very well for me but double check your session setting
"
Internally, the element stores a unique identifier using Zend_Session_Namespace, and checks for it at submission (checking that the TTL has not expired). The 'Identical' validator is then used to ensure the submitted hash matches the stored hash.
The 'formHidden' view helper is used to render the element in the form.
"
form ZF docs
Zend_Form_Element_Hash is supposed to regenerate every request. What you're describing is your tokens going out of synch. This generally happens with multiple forms or with redirects/forwards.
If you're using ajax somewhere on the page you can put this in the controller action (near the end)
$form->hash->initCsrfToken();
$this->view->hash = $form->hash->getValue();
Then when you do the ajax call, just pull the token and replace the token on the form using a selector and .replaceWith(). This is how you deal with multiple forms as well
Otherwise you're probably either redirecting something or loading something twice and you should change the hop in the Zend library. The hop is how many times a token can be requested before it expires
Check that there is not a hidden redirect or forward somewhere in your script... the hash has a hop count of 1 so any redirect will make it expire.
FWIW i think there was a subtle bug in the hash a few versions of ZF ago. I got stuck on exactly the same problem, and hacked the code to make the hop count = 2. When I upgraded ZF this problem went away.