I'm reading how to programmatically invalidate cached pages on the server in ASP.NET, and the book ("MCTS Self-Paced Traing Kit (Exam 70-515)") says:
To directly control whether a cached
version of a page is used or whether
the page is dynamically generated,
response to the ValidateCacheOutput
event and set a valid value for the
HttpValidationStatus attribute.
The code segments look like the following:
public static void ValidateCacheOutput(HttpContext context, Object data,
ref HttpValidationStatus status)
{
if (context.Request.QueryString["Status"] != null)
{
string pageStatus = context.Request.QueryString["Status"];
if (pageStatus == "invalid")
status = HttpValidationStatus.Invalid;
else if (pageStatus == "ignore")
status = HttpValidationStatus.IgnoreThisRequest;
else
status = HttpValidationStatus.Valid;
}
else
status = HttpValidationStatus.Valid;
}
protected void Page_Load(object sender, EventArgs e)
{
Response.Cache.AddValidationCallback(
new HttpCacheValidateHandler(ValidateCacheOutput),
null);
}
Could someone please explain to me what this code is doing? Also, the main question I have is that I thought Cached pages were simply returned from the server, but the code below indicates that the page life-cycle is being invoked (Page_Load event); I'm confused because the page life-cycle isn't invoked if a cached page is returned, so how would the code in the Page_Load event even fire?
Note: Here's the same example that the book has
I also came across this question. It is too bad that almost every blog post and article I find about this subject dutifully replicates the MSDN example without really explaining how it works.
I don't have a definite answer but I think this works because the page life cycle is invoked at least once. Namely when the page is requested for the first time and thus isn't cached yet.
During that first request the Page_Load is called and the HttpCacheValidateHandler is registered with the Cache object. During all subsequent request for that page, the Cache object is able to call your ValidateCacheOutput() method. And because this method is static the page life-cycle doesn't have to be invoked.
I hope that someone who knows more about this can comment on it but in my opinion this also implies the following:
In the given example the HttpCacheValidateHandler doesn't need to be a static method of the page because it doesn't use any properties of the Page object. It can be a static method on any other object you like.
The ValidateCacheOutput() method will probably be called for every page request, not just for the page which is (ab)used to call Response.Cache.AddValidationCallback(). Maybe i'm missing something obvious but I don't see how the Cache "knows" which HttpCacheValidateHandler belongs to which page.
You are right in that typically cached pages are just returned frm the server but this changes when you use AddValidationCallback. In this case, ASP.NET will call this method to determine whether to returned a cached copy or not. This method should therefore be very light or you will negate the effect of caching.
The code you have listed just checks the querystring for a Status variable and uses the value of this to determine whether to either (1) pull the page from the cache, (2) clear the cached page, re-render it and cache it or (3) just ignore the cache and re-render the page.
See http://msdn.microsoft.com/en-us/library/system.web.httpvalidationstatus.aspx for the status options.
Related
In my application there are sometimes in that I want to get a user request, save it in some place and then, in a next request, simulate saved request instead of real request, is that possible?
Use any web debugger. I use Fiddler 2.6.2 myself.
Find it here, https://www.telerik.com/download/fiddler
Be warned - there is no robust way to do it (there are a lot of edge cases to consider) but hopefully this will get you started:
Take Request.Form.ToString() and save it against the user somewhere. For testing I suggest putting it in a hidden field, but at some point you would probably need to move that to a database and associate that with the user somehow.
You will need to have a field that will tell you whether the previous request should be replayed. Using a query string parameter like ?replay=true will work. Then pull it into your page:
protected bool IsReplayRequest
{
get
{
return bool.Parse(this.GetValue(p => p.Request["replay"], "false"));
}
}
To simulate the request you will need to override the page's DeterminePostBackMode. This is one of the first methods ASP.NET calls to start the postback process. Use the saved request form converted into a NameValueCollection. Something like this:
protected override NameValueCollection DeterminePostBackMode()
{
if (IsReplayRequest && SavedFormData != null)
{
if (!string.IsNullOrWhiteSpace(SavedFormData.Data))
{
return HttpUtility.ParseQueryString(SavedFormData.Data ?? string.Empty);
}
}
return base.DeterminePostBackMode();
}
Where SavedFormData is just a class which holds the request form and the user information.
With those 3 steps, you should be able to succesfully simulate a previous request for a simple page
Now for the potential problems:
First, the viewstate might get in the way. If you have any listboxes or data grids or authentication etc, you will then need to save the viewstate with the form data. for that you will need to override the page GetStatePersister() method (Is it possible to make a custom implementation of ViewState?).
Second, once you have simulated a request, you need to make sure a) no data is corrupted and b) you remove the "replay" query string. Otherwise the request will play over and over again.
Third, depending on how you store the request form, you will have to think about how to associate it with the user/browser/session/window. Dealing with each one of those has it's own problems.
Once you've got everything above solved, you will successfully simulate a saved request!
I am re-writing an ASP.NET application and noticed a difference in behaviour ...
In my Page_Load event I have some code such as:
string id = Request["id"]
which gets the id param from the URL. On page load (ie a HTTP GET), this works as expected in both versions. I also have a button onclick event handler. Clearly, this performs a POST to the server, and also invokes the Page_Load handler. The difference is, that in the original version of the app, the id is successfully loaded from the request. In the new version of the app, id comes back as null. I have discovered that I need to use Request.Params["id"] instead, but am totally puzzled as to why Request["id"] works for POST requests in one app but not the other.
The only difference between the apps is that the first was created as File -> New Website and the second File -> New Web Application. I think this is what is causing the difference in behaviour, but am wondering why this subtle difference, and also if there is anything else I should be aware of between the 2.
Any advice greatly appreciated.
As you have mentioned, you have the id parameter coming through twice. This will be because you have one in the query string parameters and one in the form parameters. I'm not sure why this would be occurring in one web app and not the other, but you can make changes to your code to account for it in a more correct way.
If you view the source of the HTML in your browser, you will see that the action value for the form will be current pages URL, including the query string. This is why the first id is being sent through. Evidently, the second id is coming through via the form itself:
HTML Source of basic web form
<form method="post" action="Default.aspx?id=3" id="ctl01">
<input type="text" name="id">
</div>
There are a couple of things you can do here:
first off, I wouldn't use Request.Params["id"] for this, as it combines the query string, form, cookies and server variables into one collection. You should use Request.Querystring and Request.Form properties, based on what you require and when
In your Page_Load handler, use the Page.IsPostBack property to determine whether the page is loading for a GET or POST and use the Request properties described above.
Example of Page.IsPostBack usage:
protected void Page_Load(object sender, EventArgs e)
{
string id = string.Empty;
if (Page.IsPostBack)
{
id = Request.Form["id"];
}
else
{
id = Request.QueryString["id"];
}
}
I always use web applications project but the difference is compilation. Website has a dynamic compilation, which means that the first request will be slower and web app has pre-compiled release dlls.
Check this for pro's and con's : http://maordavid.blogspot.ca/2007/06/aspnet-20-web-site-vs-web-application.html
I could see, myself and many people are having trouble with this two items in ASP.NET... Refresh Button, Back Button... (I see many people went to the exent of asking how to disable these two buttons in the browser..)
Now what are the problems in Implementing two more boolean variables in Page (IsRefresh, IsPostBack)... If those problems can be circumvened and implemented, it would be a great asset to developers...
When you are answering, if you could also include the steps you are taking in your web app in order to avoid reposting (at times in DB) in such scenarios that would be helpful.
Thanks
The problem with implement the two additional boolean properties is, that there really is no (reliable) way to distinguish Back / Refresh triggered requests. When hitting on of these buttons, the browser will either:
display the page from the cache if allowed, or
execute the exact same request again (possibly asking the user to confirm first)
You are experiencing case #2. When the second request occurs, the server will recieve the exact same request data as it did with the original request. Thus, ASP.NET (and most other frameworks) will handle the request just like the original was handled. There are several ways to work around this problem:
Change your caching policy to allow the browser to redisplay the results of previous requests (might solve the Back problem, but not Refresh).
Add a hidden field (or data in the ViewState) containing a unique value on the page from where postbacks are expected. Then add some data structure to keep a list of "used" values, along with logic to test for duplicates before doing anything critical.
Use the Post/Redirect/Get pattern to ensure that the POST request never ends up in the browser history.
Solution #3 is dead easy, and works in almost every case. Basically, rather then returning the results/confirmation as a reply to the "critical" postback, do a redirect to a new URL:
protected void btnCritical_Click(object sender, EventArgs e)
{
DoSomethingThatShouldNotBeDoneTwice();
Response.Redirect("confirmation.aspx");
}
aspnet_isapi recognizes a postback from the form content, specifically the ViewState field. It has no intrinsic way to distinguish a postback from a refresh.
i have seen a few strategies for working with this in the past. One is to assign each request a guid and use that as a unique key or index in the table you are writing to.
You can create a base page that all of your pages inherit from.
public partial class PageBase : System.Web.UI.Page
{
private Guid _requestId;
protected Guid RequestId
{
get
{
return _requestId;
}
}
protected virtual void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
_requestId = Guid.NewGuid();
ViewState.Add("requestId", _requestId);
}
else
{
_requestId = (Guid)ViewState["requestId"];
}
}
}
public partial class _Default : PageBase
{
protected override void Page_Load(object sender, EventArgs e)
{
base.Page_Load(sender, e);
// now do stuff
}
}
It's not quite that clear cut. The only way the server can identify a "PostBack" is based on the headers passed to the page by your request. When you POST a form, "IsPostBack" is true, as you would expect. When you refresh a page or press Back the exact same request data is sent. There's no way for the application to detect that the user issued a non-standard request (Back or Refresh) without changing the behavior of the browser that sends the request.
As you know, most browsers will indicate to you that "Clicking Back will resend the form data...", but that is merely a warning issued by the browser to let you know that it is going to send the exact same request over again. The server does not know this and has no (native) way to interpret the information.
Double Postback Prevention Tips
One way to prevent data getting posted twice is to ensure that each PostBack contains some unique data that you can validate. The process is fairly simple.
On each page load you will want to create a unique identifier for that page's PostBack events. It doesn't matter what the unique id is, so long as it isn't the same on sequential page loads. Place that unique identifier in a hidden field on your page AND in the user's session (or a cookie). Then, on each PostBack validate that the cookie in the hidden field matches the value in the session. If the values match the PostBack is an original post to the page which can be processed accordingly. After performing the necessary operations you will then need to change the unique identifier in both locations again. This way, if the user should hit back and choose to "Resend Data", the hidden field will not match the session key and you can throw out the duplicate post data.
Why is "_requestValueCollection" empty on PostBack?
I have a really strange problem with post backs. In some cases on post backs (this.Request.RequestType == "POST") have null "_requestValueCollection" member. And for ASP.NET that means this.IsPostBack == false.
So I have modified the Page_Load in the following way:
protected void Page_Load(object sender, EventArgs e)
{
if (!this.IsPostBack && this.Request.RequestType != "POST")
{
//REGULAR INIT STUFF
}
else
{
//REGULAR SITE POSTBACK STUFF
}
}
What is possible danger of this approach? So far everything is doing OK (and is pretty rich and complicated page).
It isn't clear from your example what you are attempting to do with this code, so this is mostly a short in the dark.
You probably don't need the second part of the if statement. Checking IsPostBack alone should be sufficient.
_requestValueCollection is not a property, it is a field and probably isn't a good place to get at the data submitted by the client. I suggest instead that you consider using the Form property (this.Request.Form) or the Headers property (this.Request.Headers) depending on what you are looking for. Keep in mind that most of the time you can just get form values from the asp.net controls on the form directly.
You may also want to look at the Request.HttpMethod property if you need to determine the exact http method used to invoke the page.
Edit: Adding info about _requestValueCollection
The mechanics behind the _requestValueCollection being loaded are quite complex, but I took a look at the MS source and from what I can determine the page calls on every control on the page that implements the IPostBackDataHandler interface. For each of these it will call the LoadPostData method which adds the data for that control to the collection.
The main things that I can think of off the top of my head that might cause the collection to be null would be:
no server controls on the page implement IPostBackDataHandler
there is no server form, or the contents of the form weren't sent by the client
Alternately, the page may be using query strings to convey the data to the server, and the query string doesn't contain anything
As I said, this is a bit fuzzy. The Page class is very complex internally and so there could be other ways data gets put into that collection too, but this was all I could find on a casual examination.
i want a handler to redirect to a web-forms page, pre-filling in the values of some controls on the form.
i tried setting my current Request.Form data:
if (theyWantToDoSomething)
{
//pre-fill form values
context.Request.Form["TextBox1"] = "test";
context.Request.Form["ComboBox1"] = "test 2";
context.Request.Form["TextBox2"] = GetTheTextForTheThing();
//tell the client to go there
context.Response.Redirect("~/SomeWebForm.aspx");
return;
}
But i get an exception that Form values are read only.
What would be a way to send the client to another page, pre-filling form data?
Answer
i used the Session state to store values. It's important to note that by default a Handler doesn't have access to Session (the Session object will be null). You have to tell IIS to give you the Session object by adding the IRequiresSessionState marker interface to your handler class:
public class Handler : IHttpHandler, System.Web.SessionState.IRequiresSessionState
{
public void ProcessRequest(HttpContext context)
{
...
if (theyWantToDoSomething)
{
//pre-fill form values
context.Session["thing1"] = "test";
context.Session["thing2"] = "test 2";
context.Session["thing3"] = GetTheTextForTheThing();
//tell the client to go there
context.Response.Redirect("~/SomeWebForm.aspx");
return; //not strictly needed, since Redirect ends processing
}
...
}
}
You can only populate your Response, the Request is input data and is indeed read-only.
If you are using ASP.NET, there are a variety of ways you could accomplish what you need:
The best way would probably be to pass the data you need to be pre-populated to SomeWebForm.aspx via the Session object, and on that pages Load method, populate your form. Keep in mind that when you do Response.Redirect, a 302 response is sent to the client with the URL the client should redirect to. The process is transparent to the user...but there is a full round trip involved.
Another alternative to populating the users Session would be to add GET parameters via a query string to the redirect to SomeWebForm.aspx.
If you need to transfer processing to the SomeWebForm.aspx page without round tripping, you could use Server.Transfer. This will transfer execution from the current page to the page you choose...however, this can cause some odd behavior on the client end because the URL does not update. As far as the user is concerned, it will still appear as though they are on the same page they started on.
A few ideas that might get you started:
Pass the values in the query string
Store the values in the session state or in a seperate cookie
Store the values in HttpContext.Items and use Server.Transfer instead of Response.Redirect
Another approach that hasn't been mentioned yet is using Server.Transfer makes it possible to use the Page.PreviousPage property, allowing access to the controls on the page that transferred control.
As jrista mentioned though, using Transfer doesn't update the URL shown to the user, which may or may not be an issue. For example, a user can't precisely bookmark a page they got transferred to since the URL will be that of the original transferring page.