Response.Redirect into a new window (with Extension Methods)
This question comes up from time to time, to time. If you understand how redirects work, then you also know it is "not possible" to redirect into a new window, because a redirect on the server causes a special HTTP response to be sent to the users browser, the client. The browsers native implementation interprets the special response code and sends the user off to the destination. There's no built-in mechanism or standard for specifying a new window.
The only way to open a new window is for it to be initiated on the client side, whether it be through script or clicking on a link.
So the solution always proposed to this problem is to instead write out some script that opens the window, rather than using Response.Redirect:
<script type="text/javascript">
window.open("foo.aspx");
</script>
Ok... so first you lecture me about how it is "not possible", and then you give me the code that makes it possible. Why can't I just redirect to a new window -- I don't care how HTTP works or client this or server that. There's obviously a solution, so why do I have to worry about it?
(The make-believe developers in my head are always quite temperamental)
It's easy enough to write a little helper that abstracts the details away from us... while we're at it, we might as well add 'target' and 'windowFeatures' parameters. If we're going to open the new window with script, why not let you use all of the window.open parameters? For example, with 'windowFeatures' you can specify whether the new window should have a menu bar, and what its width and height are.
public static class ResponseHelper {
public static void Redirect(string url, string target, string windowFeatures) {
HttpContext context = HttpContext.Current;
if ((String.IsNullOrEmpty(target) ||
target.Equals("_self", StringComparison.OrdinalIgnoreCase)) &&
String.IsNullOrEmpty(windowFeatures)) {
context.Response.Redirect(url);
}
else {
Page page = (Page)context.Handler;
if (page == null) {
throw new InvalidOperationException(
"Cannot redirect to new window outside Page context.");
}
url = page.ResolveClientUrl(url);
string script;
if (!String.IsNullOrEmpty(windowFeatures)) {
script = @"window.open(""{0}"", ""{1}"", ""{2}"");";
}
else {
script = @"window.open(""{0}"", ""{1}"");";
}
script = String.Format(script, url, target, windowFeatures);
ScriptManager.RegisterStartupScript(page,
typeof(Page),
"Redirect",
script,
true);
}
}
}
Now you just call ResponseHelper.Redirect, and it figures out how to honor your wishes. If you don't specify a target or you specify the target to be "_self", then you must mean to redirect within the current window, so a regular Response.Redirect occurs. If you specify a different target, like "_blank", or if you specify window features, then you want to redirect to a new window, and we write out the appropriate script.
One nice side effect of this "do you really need a new window?" detection is that it's dynamic. Say the destination you redirect to is configurable by some administrator. Now they can decide whether it opens in a new window or not. If they don't want it to they can specify blank or _self as the target.
Disclaimers:
Note: If you use it outside the context of a Page request, you can't redirect to a new window. The reason is the need to call the ResolveClientUrl method on Page, which I can't do if there is no Page. I could have just built my own version of that method, but it's more involved than you might think to do it right. So if you need to use this from an HttpHandler other than a Page, you are on your own.
Note: Beware of popup blockers.
Note: Obviously when you are redirecting to a new window, the current window will still be hanging around. Normally redirects abort the current request -- no further processing occurs. But for these redirects, processing continues, since we still have to serve the response for the current window (which also happens to contain the script to open the new window, so it is important that it completes).
Extension Methods
Recently, Eilon and Bertrand blogged about a novel use of some C# 3.0 features. Eilon posed the question, "Have you come up with a novel way to use a new language feature that you'd like to share?". Well here you go.
Extension Methods are a new feature in C# 3.0 (you'll need it for the rest of the article). They allow you to add methods to existing types, imported via a 'using' statement. I've seen a lot of debate over their use -- whether they are bad or good. Well -- I don't know, I don't really want to be involved in that debate. But I do know that in some scenarios they seem to fit perfectly. Like all language tools, you should use it sparingly and only when appropriate. I believe even the dreaded GOTO statement, which yes, exists in C#, has its place (I wasn't a believer originally, but some old coworkers of mine convinced me (Bob!)).
In this case, an extension method seems to work well. In general, whenever you find yourself writing a static Helper class whose only purpose in life is to help use the APIs of another type, it's probably a great candidate for extension methods. Especially if the first parameter to all those methods is the type you're trying to help with -- or if the methods always grabs the instance through some static API (like HttpContext.Current) or instantiates a new one.
By rewriting our ResponseHelper to use extension methods...
public static class ResponseHelper {
public static void Redirect(this HttpResponse response,
string url,
string target,
string windowFeatures) {
if ((String.IsNullOrEmpty(target) ||
target.Equals("_self", StringComparison.OrdinalIgnoreCase)) &&
String.IsNullOrEmpty(windowFeatures)) {
response.Redirect(url);
}
else {
Page page = (Page)HttpContext.Current.Handler;
if (page == null) {
throw new InvalidOperationException(
"Cannot redirect to new window outside Page context.");
}
url = page.ResolveClientUrl(url);
string script;
if (!String.IsNullOrEmpty(windowFeatures)) {
script = @"window.open(""{0}"", ""{1}"", ""{2}"");";
}
else {
script = @"window.open(""{0}"", ""{1}"");";
}
script = String.Format(script, url, target, windowFeatures);
ScriptManager.RegisterStartupScript(page,
typeof(Page),
"Redirect",
script,
true);
}
}
}
Note the 'this' keyword in the first parameter. Now whenever we include the namespace this class is defined within, we get a nice override on the actual Response object.
Simply including a 'using' to a namespace is what gets extensions methods to show up. So it's probably a good idea to keep extension methods isolated to their own namespaces, lest someone get more than they bargained for when they use your namespace.
Also worth noting is that this is still a static API, so you can use it the traditional way, too. You just have to pass in the Response object as the first parameter.
And to see it in action...
Response.Redirect("popup.aspx", "_blank", "menubar=0,width=100,height=100");