January 25, 2010 @ 12:42 am
- from Maui, Hawaii
Ran into a small problem today with my client side jQuery library after switching to jQuery 1.4. I ran into a problem with a shadow plugin that I use to provide drop shadows for absolute elements – for Mozilla WebKit browsers the –moz-box-shadow and –webkit-box-shadow CSS attributes are used but for IE a manual element is created to provide the shadow that underlays the original element along with a blur filter to provide the fuzziness in the shadow. Some of the key pieces are:
var vis = el.is(":visible");
if (!vis)
el.show(); // must be visible to get .position
var pos = el.position();
if (typeof shEl.style.filter == "string")
sh.css("filter", 'progid:DXImageTransform.Microsoft.Blur(makeShadow=true, pixelradius=3, shadowOpacity=' + opt.opacity.toString() + ')');
sh.show()
.css({ position: "absolute",
width: el.outerWidth(),
height: el.outerHeight(),
opacity: opt.opacity,
background: opt.color,
left: pos.left + opt.offset,
top: pos.top + opt.offset
});
This has always worked in previous versions of jQuery, but with 1.4 the original filter no longer works. It appears that applying the opacity after the original filter wipes out the original filter. IOW, the opacity filter is not applied incrementally, but absolutely which is a real bummer.
Luckily the workaround is relatively easy by just switching the order in which the opacity and filter are applied. If I apply the blur after the opacity I get my correct behavior back with both opacity:
sh.show()
.css({ position: "absolute",
width: el.outerWidth(),
height: el.outerHeight(),
opacity: opt.opacity,
background: opt.color,
left: pos.left + opt.offset,
top: pos.top + opt.offset
});
if (typeof shEl.style.filter == "string")
sh.css("filter", 'progid:DXImageTransform.Microsoft.Blur(makeShadow=true, pixelradius=3, shadowOpacity=' + opt.opacity.toString() + ')');
While this works this still causes problems in other areas where opacity is implicitly set in code such as for fade operations or in the case of my shadow component the style/property watcher that keeps the shadow and main object linked. Both of these may set the opacity explicitly and that is still broken as it will effectively kill the blur filter.
This seems like a really strange design decision by the jQuery team, since clearly the jquery css function does the right thing for setting filters. Internally however, the opacity setting doesn’t use .css instead hardcoding the filter which given jQuery’s usual flexibility and smart code seems really inappropriate.
The following is from jQuery.js 1.4:
var style = elem.style || elem, set = value !== undefined;
// IE uses filters for opacity
if ( !jQuery.support.opacity && name === "opacity" ) {
if ( set ) {
// IE has trouble with opacity if it does not have layout
// Force it by setting the zoom level
style.zoom = 1;
// Set the alpha filter to set the opacity
var opacity = parseInt( value, 10 ) + "" === "NaN" ? "" : "alpha(opacity=" + value * 100 + ")";
var filter = style.filter || jQuery.curCSS( elem, "filter" ) || "";
style.filter = ralpha.test(filter) ? filter.replace(ralpha, opacity) : opacity;
}
return style.filter && style.filter.indexOf("opacity=") >= 0 ?
(parseFloat( ropacity.exec(style.filter)[1] ) / 100) + "":
"";
}
You can see here that the style is explicitly set in code rather than relying on $.css() to assign the value resulting in the old filter getting wiped out.
jQuery 1.32 looks a little different:
// IE uses filters for opacity
if ( !jQuery.support.opacity && name == "opacity" ) {
if ( set ) {
// IE has trouble with opacity if it does not have layout
// Force it by setting the zoom level
elem.zoom = 1;
// Set the alpha filter to set the opacity
elem.filter = (elem.filter || "").replace( /alpha\([^)]*\)/, "" ) +
(parseInt( value ) + '' == "NaN" ? "" : "alpha(opacity=" + value * 100 + ")");
}
return elem.filter && elem.filter.indexOf("opacity=") >= 0 ?
(parseFloat( elem.filter.match(/opacity=([^)]*)/)[1] ) / 100) + '':
"";
}
Offhand I’m not sure why the latter works better since it too is assigning the filter. However, when checking with the IE script debugger I can see that there are actually a couple of filter tags assigned when using jQuery 1.32 but only one when I use jQuery 1.4.
Note also that the jQuery 1.3 compatibility plugin for jQUery 1.4 doesn’t address this issue either.
Resources
January 20, 2010 @ 4:32 pm
- from Maui, Hawaii
A few days ago my buddy Ben Jones pointed out that he ran into a bug in the ScriptContainer control in the West Wind Web and Ajax Toolkit. The problem was basically that when a Server.Transfer call was applied the script container (and also various ClientScriptProxy script embedding routines) would potentially fail to load up the specified scripts.
It turns out the problem is due to the fact that the various components in the toolkit use request specific singletons via a Current property. I use a static Current property tied to a Context.Items[] entry to handle this type of operation which looks something like this:
/// <summary>
/// Current instance of this class which should always be used to
/// access this object. There are no public constructors to
/// ensure the reference is used as a Singleton to further
/// ensure that all scripts are written to the same clientscript
/// manager.
/// </summary>
public static ClientScriptProxy Current
{
get
{
if (HttpContext.Current == null)
return new ClientScriptProxy();
ClientScriptProxy proxy = null;
if (HttpContext.Current.Items.Contains(STR_CONTEXTID))
proxy = HttpContext.Current.Items[STR_CONTEXTID] as ClientScriptProxy;
else
{
proxy = new ClientScriptProxy();
HttpContext.Current.Items[STR_CONTEXTID] = proxy;
}
return proxy;
}
}
The proxy is attached to a Context.Items[] item which makes the instance Request specific. This works perfectly fine in most situations EXCEPT when you’re dealing with Server.Transfer/Execute requests. Server.Transfer doesn’t cause Context.Items to be cleared so both the current transferred request and the original request’s Context.Items collection apply.
For the ClientScriptProxy this causes a problem because script references are tracked on a per request basis in Context.Items to check for script duplication. Once a script is rendered an ID is written into the Context collection and so considered ‘rendered’:
// No dupes - ref script include only once
if (HttpContext.Current.Items.Contains( STR_SCRIPTITEM_IDENTITIFIER + fileId ) )
return;
HttpContext.Current.Items.Add(STR_SCRIPTITEM_IDENTITIFIER + fileId, string.Empty);
where the fileId is the script name or unique identifier. The problem is on the Transferred page the item will already exist in Context and so fail to render because it thinks the script has already rendered based on the Context item. Bummer.
The workaround for this is simple once you know what’s going on, but in this case it was a bitch to track down because the context items are used in many places throughout this class. The trick is to determine when a request is transferred and then removing the specific keys.
The first issue is to determine if a script is in a Trransfer or Execute call:
if (HttpContext.Current.CurrentHandler != HttpContext.Current.Handler)
Context.Handler is the original handler and CurrentHandler is the actual currently executing handler that is running when a Transfer/Execute is active. You can also use Context.PreviousHandler to get the last handler and chain through the whole list of handlers applied if Transfer calls are nested (dog help us all for the person debugging that).
For the ClientScriptProxy the full logic to check for a transfer and remove the code looks like this:
/// <summary>
/// Clears all the request specific context items which are script references
/// and the script placement index.
/// </summary>
public void ClearContextItemsOnTransfer()
{
if (HttpContext.Current != null)
{
// Check for Server.Transfer/Execute calls - we need to clear out Context.Items
if (HttpContext.Current.CurrentHandler != HttpContext.Current.Handler)
{
List<string> Keys = HttpContext.Current.Items.Keys.Cast<string>().Where(s => s.StartsWith(STR_SCRIPTITEM_IDENTITIFIER) || s == STR_ScriptResourceIndex).ToList();
foreach (string key in Keys)
{
HttpContext.Current.Items.Remove(key);
}
}
}
}
along with a small update to the Current property getter that sets a global flag to indicate whether the request was transferred:
if (!proxy.IsTransferred && HttpContext.Current.Handler != HttpContext.Current.CurrentHandler)
{
proxy.ClearContextItemsOnTransfer();
proxy.IsTransferred = true;
}
return proxy;
I know this is pretty ugly, but it works and it’s actually minimal fuss without affecting the behavior of the rest of the class. Ben had a different solution that involved explicitly clearing out the Context items and replacing the collection with a manually maintained list of items which also works, but required changes through the code to make this work.
In hindsight, it would have been better to use a single object that encapsulates all the ‘persisted’ values and store that object in Context instead of all these individual small morsels. Hindsight is always 20/20 though :-}.
If possible use Page.Items
ClientScriptProxy is a generic component that can be used from anywhere in ASP.NET, so there are various methods that are not Page specific on this component which is why I used Context.Items, rather than the Page.Items collection.Page.Items would be a better choice since it will sidestep the above Server.Transfer nightmares as the Page is reloaded completely and so any new Page gets a new Items collection. No fuss there.
So for the ScriptContainer control, which has to live on the page the behavior is a little different. It is attached to Page.Items (since it’s a control):
/// <summary>
/// Returns a current instance of this control if an instance
/// is already loaded on the page. Otherwise a new instance is
/// created, added to the Form and returned.
///
/// It's important this function is not called too early in the
/// page cycle - it should not be called before Page.OnInit().
///
/// This property is the preferred way to get a reference to a
/// ScriptContainer control that is either already on a page
/// or needs to be created. Controls in particular should always
/// use this property.
/// </summary>
public static ScriptContainer Current
{
get
{
// We need a context for this to work!
if (HttpContext.Current == null)
return null;
Page page = HttpContext.Current.CurrentHandler as Page;
if (page == null)
throw new InvalidOperationException(Resources.ERROR_ScriptContainer_OnlyWorks_With_PageBasedHandlers);
ScriptContainer ctl = null;
// Retrieve the current instance
ctl = page.Items[STR_CONTEXTID] as ScriptContainer;
if (ctl != null)
return ctl;
ctl = new ScriptContainer();
page.Form.Controls.Add(ctl);
return ctl;
}
}
The biggest issue with this approach is that you have to explicitly retrieve the page in the static Current property. Notice again the use of CurrentHandler (rather than Handler which was my original implementation) to ensure you get the latest page including the one that Server.Transfer fired.
Server.Transfer and Server.Execute are Evil
All that said – this fix is probably for the 2 people who are crazy enough to rely on Server.Transfer/Execute. :-} There are so many weird behavior problems with these commands that I avoid them at all costs. I don’t think I have a single application that uses either of these commands…
Related Resources
January 02, 2010 @ 3:26 am
- from Maui, Hawaii
One thing that I’ve come to appreciate in control development in ASP.NET that use JavaScript is the ability to have more control over script and script include placement than ASP.NET provides natively. Specifically in ASP.NET you can use either the ClientScriptManager or ScriptManager to embed scripts and script references into pages via code.
This works reasonably well, but the script references that get generated are generated into the HTML body and there’s very little operational control for placement of scripts. If you have multiple controls or several of the same control that need to place the same scripts onto the page it’s not difficult to end up with scripts that render in the wrong order and stop working correctly. This is especially critical if you load script libraries with dependencies either via resources or even if you are rendering referenced to CDN resources.
Natively ASP.NET provides a host of methods that help embedding scripts into the page via either Page.ClientScript or the ASP.NET ScriptManager control (both with slightly different syntax):
- RegisterClientScriptBlock
Renders a script block at the top of the HTML body and should be used for embedding callable functions/classes.
- RegisterStartupScript
Renders a script block just prior to the </form> tag and should be used to for embedding code that should execute when the page is first loaded. Not recommended – use jQuery.ready() or equivalent load time routines.
- RegisterClientScriptInclude
Embeds a reference to a script from a url into the page.
- RegisterClientScriptResource
Embeds a reference to a Script from a resource file generating a long resource file string
All 4 of these methods render their <script> tags into the HTML body. The script blocks give you a little bit of control by having a ‘top’ and ‘bottom’ of the document location which gives you some flexibility over script placement and precedence. Script includes and resource url unfortunately do not even get that much control – references are simply rendered into the page in the order of declaration.
The ASP.NET ScriptManager control facilitates this task a little bit with the abililty to specify scripts in code and the ability to programmatically check what scripts have already been registered, but it doesn’t provide any more control over the script rendering process itself. Further the ScriptManager is a bear to deal with generically because generic code has to always check and see if it is actually present.
Some time ago I posted a ClientScriptProxy class that helps with managing the latter process of sending script references either to ClientScript or ScriptManager if it’s available. Since I last posted about this there have been a number of improvements in this API, one of which is the ability to control placement of scripts and script includes in the page which I think is rather important and a missing feature in the ASP.NET native functionality.
Handling ScriptRenderModes
One of the big enhancements that I’ve come to rely on is the ability of the various script rendering functions described above to support rendering in multiple locations:
/// <summary>
/// Determines how scripts are included into the page
/// </summary>
public enum ScriptRenderModes
{
/// <summary>
/// Inherits the setting from the control or from the ClientScript.DefaultScriptRenderMode
/// </summary>
Inherit,
/// Renders the script include at the location of the control
/// </summary>
Inline,
/// <summary>
/// Renders the script include into the bottom of the header of the page
/// </summary>
Header,
/// <summary>
/// Renders the script include into the top of the header of the page
/// </summary>
HeaderTop,
/// <summary>
/// Uses ClientScript or ScriptManager to embed the script include to
/// provide standard ASP.NET style rendering in the HTML body.
/// </summary>
Script,
/// <summary>
/// Renders script at the bottom of the page before the last Page.Controls
/// literal control. Note this may result in unexpected behavior
/// if /body and /html are not the last thing in the markup page.
/// </summary>
BottomOfPage
}
This enum is then applied to the various Register functions to allow more control over where scripts actually show up. Why is this useful? For me I often render scripts out of control resources and these scripts often include things like a JavaScript Library (jquery) and a few plug-ins. The order in which these can be loaded is critical so that jQuery.js always loads before any plug-in for example.
Typically I end up with a general script layout like this:
- Core Libraries- HeaderTop
- Plug-ins: Header
- ScriptBlocks: Header or Script depending on other dependencies
There’s also an option to render scripts and CSS at the very bottom of the page before the last Page control on the page which can be useful for speeding up page load when lots of scripts are loaded.
The API syntax of the ClientScriptProxy methods is closely compatible with ScriptManager’s using static methods and control references to gain access to the page and embedding scripts.
For example, to render some script into the current page in the header:
// Create script block in header
ClientScriptProxy.Current.RegisterClientScriptBlock(this, typeof(ControlResources),
"hello_function", "function helloWorld() { alert('hello'); }", true,
ScriptRenderModes.Header);
// Same again - shouldn't be rendered because it's the same id
ClientScriptProxy.Current.RegisterClientScriptBlock(this, typeof(ControlResources),
"hello_function", "function helloWorld() { alert('hello'); }", true,
ScriptRenderModes.Header);
// Create a second script block in header
ClientScriptProxy.Current.RegisterClientScriptBlock(this, typeof(ControlResources),
"hello_function2", "function helloWorld2() { alert('hello2'); }", true,
ScriptRenderModes.Header);
// This just calls ClientScript and renders into bottom of document
ClientScriptProxy.Current.RegisterStartupScript(this,typeof(ControlResources),
"call_hello", "helloWorld();helloWorld2();", true);
which generates:
<html xmlns="http://www.w3.org/1999/xhtml" >
<head><title>
</title>
<script type="text/javascript">
function helloWorld() { alert('hello'); }
</script>
<script type="text/javascript">
function helloWorld2() { alert('hello2'); }
</script>
</head>
<body>
…
<script type="text/javascript">
//<![CDATA[
helloWorld();helloWorld2();//]]>
</script>
</form>
</body>
</html>
Note that the scripts are generated into the header rather than the body except for the last script block which is the call to RegisterStartupScript. In general I wouldn’t recommend using RegisterStartupScript – ever. It’s a much better practice to use a script base load event to handle ‘startup’ code that should fire when the page first loads. So instead of the code above I’d actually recommend doing:
ClientScriptProxy.Current.RegisterClientScriptBlock(this, typeof(ControlResources),
"call_hello", "$().ready( function() { alert('hello2'); });", true,
ScriptRenderModes.Header);
assuming you’re using jQuery on the page.
For script includes from a Url the following demonstrates how to embed scripts into the header. This example injects a jQuery and jQuery.UI script reference from the Google CDN then checks each with a script block to ensure that it has loaded and if not loads it from a server local location:
// load jquery from CDN
ClientScriptProxy.Current.RegisterClientScriptInclude(this, typeof(ControlResources),
"http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js",
ScriptRenderModes.HeaderTop);
// check if jquery loaded - if it didn't we're not online
string scriptCheck =
@"if (typeof jQuery != 'object')
document.write(unescape(""%3Cscript src='{0}' type='text/javascript'%3E%3C/script%3E""));";
string jQueryUrl = ClientScriptProxy.Current.GetWebResourceUrl(this, typeof(ControlResources),
ControlResources.JQUERY_SCRIPT_RESOURCE);
ClientScriptProxy.Current.RegisterClientScriptBlock(this, typeof(ControlResources),
"jquery_register", string.Format(scriptCheck,jQueryUrl),true,
ScriptRenderModes.HeaderTop);
// Load jquery-ui from cdn
ClientScriptProxy.Current.RegisterClientScriptInclude(this, typeof(ControlResources),
"http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/jquery-ui.min.js",
ScriptRenderModes.Header);
// check if we need to load from local
string jQueryUiUrl = ResolveUrl("~/scripts/jquery-ui-custom.min.js");
ClientScriptProxy.Current.RegisterClientScriptBlock(this, typeof(ControlResources),
"jqueryui_register", string.Format(scriptCheck, jQueryUiUrl), true,
ScriptRenderModes.Header);
// Create script block in header
ClientScriptProxy.Current.RegisterClientScriptBlock(this, typeof(ControlResources),
"hello_function", "$().ready( function() { alert('hello'); });", true,
ScriptRenderModes.Header);
which in turn generates this HTML:
<html xmlns="http://www.w3.org/1999/xhtml" >
<head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js" type="text/javascript"></script>
<script type="text/javascript">
if (typeof jQuery != 'object')
document.write(unescape("%3Cscript src='/WestWindWebToolkitWeb/WebResource.axd?d=DIykvYhJ_oXCr-TA_dr35i4AayJoV1mgnQAQGPaZsoPM2LCdvoD3cIsRRitHKlKJfV5K_jQvylK7tsqO3lQIFw2&t=633979863959332352' type='text/javascript'%3E%3C/script%3E"));
</script>
<title>
</title>
<script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/jquery-ui.min.js" type="text/javascript"></script>
<script type="text/javascript">
if (typeof jQuery != 'object')
document.write(unescape("%3Cscript src='/WestWindWebToolkitWeb/scripts/jquery-ui-custom.min.js' type='text/javascript'%3E%3C/script%3E"));
</script>
<script type="text/javascript">
$().ready(function() { alert('hello'); });
</script>
</head>
<body>
…
</body>
</html>
As you can see there’s a bit more control in this process as you can inject both script includes and script blocks into the document at the top or bottom of the header, plus if necessary at the usual body locations. This is quite useful especially if you create custom server controls that interoperate with script and have certain dependencies. The above is a good example of a useful switchable routine where you can switch where scripts load from by default – the above pulls from Google CDN but a configuration switch may automatically switch to pull from the local development copies if your doing development for example.
How does it work?
As mentioned the ClientScriptProxy object mimicks many of the ScriptManager script related methods and so provides close API compatibility with it although it contains many additional overloads that enhance functionality. It does however work against ScriptManager if it’s available on the page, or Page.ClientScript if it’s not so it provides a single unified frontend to script access. There are however many overloads of the original SM methods like the above to provide additional functionality.
The implementation of script header rendering is pretty straight forward – as long as a server header (ie. it has to have runat=”server” set) is available. Otherwise these routines fall back to using the default document level insertions of ScriptManager/ClientScript. Given that there is a server header it’s relatively easy to generate the script tags and code and append them to the header either at the top or bottom. I suspect Microsoft didn’t provide header rendering functionality precisely because a runat=”server” header is not required by ASP.NET so behavior would be slightly unpredictable. That’s not really a problem for a custom implementation however.
Here’s the RegisterClientScriptBlock implementation that takes a ScriptRenderModes parameter to allow header rendering:
/// <summary>
/// Renders client script block with the option of rendering the script block in
/// the Html header
///
/// For this to work Header must be defined as runat="server"
/// </summary>
/// <param name="control">any control that instance typically page</param>
/// <param name="type">Type that identifies this rendering</param>
/// <param name="key">unique script block id</param>
/// <param name="script">The script code to render</param>
/// <param name="addScriptTags">Ignored for header rendering used for all other insertions</param>
/// <param name="renderMode">Where the block is rendered</param>
public void RegisterClientScriptBlock(Control control, Type type, string key, string script, bool addScriptTags, ScriptRenderModes renderMode)
{
if (renderMode == ScriptRenderModes.Inherit)
renderMode = DefaultScriptRenderMode;
if (control.Page.Header == null ||
renderMode != ScriptRenderModes.HeaderTop &&
renderMode != ScriptRenderModes.Header &&
renderMode != ScriptRenderModes.BottomOfPage)
{
RegisterClientScriptBlock(control, type, key, script, addScriptTags);
return;
}
// No dupes - ref script include only once
const string identifier = "scriptblock_";
if (HttpContext.Current.Items.Contains(identifier + key))
return;
HttpContext.Current.Items.Add(identifier + key, string.Empty);
StringBuilder sb = new StringBuilder();
// Embed in header
sb.AppendLine("\r\n<script type=\"text/javascript\">");
sb.AppendLine(script);
sb.AppendLine("</script>");
int? index = HttpContext.Current.Items["__ScriptResourceIndex"] as int?;
if (index == null)
index = 0;
if (renderMode == ScriptRenderModes.HeaderTop)
{
control.Page.Header.Controls.AddAt(index.Value, new LiteralControl(sb.ToString()));
index++;
}
else if(renderMode == ScriptRenderModes.Header)
control.Page.Header.Controls.Add(new LiteralControl(sb.ToString()));
else if (renderMode == ScriptRenderModes.BottomOfPage)
control.Page.Controls.AddAt(control.Page.Controls.Count-1,new LiteralControl(sb.ToString()));
HttpContext.Current.Items["__ScriptResourceIndex"] = index;
}
Note that the routine has to keep track of items inserted by id so that if the same item is added again with the same key it won’t generate two script entries. Additionally the code has to keep track of how many insertions have been made at the top of the document so that entries are added in the proper order.
The RegisterScriptInclude method is similar but there’s some additional logic in here to deal with script file references and ClientScriptProxy’s (optional) custom resource handler that provides script compression
/// <summary>
/// Registers a client script reference into the page with the option to specify
/// the script location in the page
/// </summary>
/// <param name="control">Any control instance - typically page</param>
/// <param name="type">Type that acts as qualifier (uniqueness)</param>
/// <param name="url">the Url to the script resource</param>
/// <param name="ScriptRenderModes">Determines where the script is rendered</param>
public void RegisterClientScriptInclude(Control control, Type type, string url, ScriptRenderModes renderMode)
{
const string STR_ScriptResourceIndex = "__ScriptResourceIndex";
if (string.IsNullOrEmpty(url))
return;
if (renderMode == ScriptRenderModes.Inherit)
renderMode = DefaultScriptRenderMode;
// Extract just the script filename
string fileId = null;
// Check resource IDs and try to match to mapped file resources
// Used to allow scripts not to be loaded more than once whether
// embedded manually (script tag) or via resources with ClientScriptProxy
if (url.Contains(".axd?r="))
{
string res = HttpUtility.UrlDecode( StringUtils.ExtractString(url, "?r=", "&", false, true) );
foreach (ScriptResourceAlias item in ScriptResourceAliases)
{
if (item.Resource == res)
{
fileId = item.Alias + ".js";
break;
}
}
if (fileId == null)
fileId = url.ToLower();
}
else
fileId = Path.GetFileName(url).ToLower();
// No dupes - ref script include only once
const string identifier = "script_";
if (HttpContext.Current.Items.Contains( identifier + fileId ) )
return;
HttpContext.Current.Items.Add(identifier + fileId, string.Empty);
// just use script manager or ClientScriptManager
if (control.Page.Header == null || renderMode == ScriptRenderModes.Script || renderMode == ScriptRenderModes.Inline)
{
RegisterClientScriptInclude(control, type,url, url);
return;
}
// Retrieve script index in header
int? index = HttpContext.Current.Items[STR_ScriptResourceIndex] as int?;
if (index == null)
index = 0;
StringBuilder sb = new StringBuilder(256);
url = WebUtils.ResolveUrl(url);
// Embed in header
sb.AppendLine("\r\n<script src=\"" + url + "\" type=\"text/javascript\"></script>");
if (renderMode == ScriptRenderModes.HeaderTop)
{
control.Page.Header.Controls.AddAt(index.Value, new LiteralControl(sb.ToString()));
index++;
}
else if (renderMode == ScriptRenderModes.Header)
control.Page.Header.Controls.Add(new LiteralControl(sb.ToString()));
else if (renderMode == ScriptRenderModes.BottomOfPage)
control.Page.Controls.AddAt(control.Page.Controls.Count-1, new LiteralControl(sb.ToString()));
HttpContext.Current.Items[STR_ScriptResourceIndex] = index;
}
There’s a little more code here that deals with cleaning up the passed in Url and also some custom handling of script resources that run through the ScriptCompressionModule – any script resources loaded in this fashion are automatically cached based on the resource id. Raw urls extract just the filename from the URL and cache based on that. All of this to avoid doubling up of scripts if called multiple times by multiple instances of the same control for example or several controls that all load the same resources/includes.
Finally RegisterClientScriptResource utilizes the previous method to wrap the WebResourceUrl as well as some custom functionality for the resource compression module:
/// <summary>
/// Returns a WebResource or ScriptResource URL for script resources that are to be
/// embedded as script includes.
/// </summary>
/// <param name="control">Any control</param>
/// <param name="type">A type in assembly where resources are located</param>
/// <param name="resourceName">Name of the resource to load</param>
/// <param name="renderMode">Determines where in the document the link is rendered</param>
public void RegisterClientScriptResource(Control control, Type type,
string resourceName,
ScriptRenderModes renderMode)
{
string resourceUrl = GetClientScriptResourceUrl(control, type, resourceName);
RegisterClientScriptInclude(control, type, resourceUrl, renderMode);
}
/// <summary>
/// Works like GetWebResourceUrl but can be used with javascript resources
/// to allow using of resource compression (if the module is loaded).
/// </summary>
/// <param name="control"></param>
/// <param name="type"></param>
/// <param name="resourceName"></param>
/// <returns></returns>
public string GetClientScriptResourceUrl(Control control, Type type, string resourceName)
{
#if IncludeScriptCompressionModuleSupport
// If wwScriptCompression Module through Web.config is loaded use it to compress
// script resources by using wcSC.axd Url the module intercepts
if (ScriptCompressionModule.ScriptCompressionModuleActive)
{
string url = "~/wwSC.axd?r=" + HttpUtility.UrlEncode(resourceName);
if (type.Assembly != GetType().Assembly)
url += "&t=" + HttpUtility.UrlEncode(type.FullName);
return WebUtils.ResolveUrl(url);
}
#endif
return control.Page.ClientScript.GetWebResourceUrl(type, resourceName);
}
This code merely retrieves the resource URL and then simply calls back to RegisterClientScriptInclude with the URL to be embedded which means there’s nothing specific to deal with other than the custom compression module logic which is nice and easy.
What else is there in ClientScriptProxy?
ClientscriptProxy also provides a few other useful services beyond what I’ve already covered here:
Transparent ScriptManager and ClientScript calls
ClientScriptProxy includes a host of routines that help figure out whether a script manager is available or not and all functions in this class call the appropriate object – ScriptManager or ClientScript – that is available in the current page to ensure that scripts get embedded into pages properly. This is especially useful for control development where controls have no control over the scripting environment in place on the page.
RegisterCssLink and RegisterCssResource
Much like the script embedding functions these two methods allow embedding of CSS links. CSS links are appended to the header or to a form declared with runat=”server”.
LoadControlScript
Is a high level resource loading routine that can be used to easily switch between different script linking modes. It supports loading from a WebResource, a url or not loading anything at all. This is very useful if you build controls that deal with specification of resource urls/ids in a standard way.
Check out the full Code
You can check out the full code to the ClientScriptProxyClass here:
ClientScriptProxy.cs
ClientScriptProxy Documentation (class reference)
Note that the ClientScriptProxy has a few dependencies in the West Wind Web Toolkit of which it is part of. ControlResources holds a few standard constants and script resource links and the ScriptCompressionModule which is referenced in a few of the script inclusion methods.
There’s also another useful ScriptContainer companion control to the ClientScriptProxy that allows scripts to be placed onto the page’s markup including the ability to specify the script location and script minification options.
You can find all the dependencies in the West Wind Web Toolkit repository:
West Wind Web Toolkit Repository
West Wind Web Toolkit Home Page
December 24, 2009 @ 2:05 am
- from Maui, Hawaii
Ran into an odd behavior today with a many to many mapping of one of my tables in LINQ to SQL. Many to many mappings aren’t transparent in LINQ to SQL and it maps the link table the same way the SQL schema has it when creating one. In other words LINQ to SQL isn’t smart about many to many mappings and just treats it like the 3 underlying tables that make up the many to many relationship. Iain Galloway has a nice blog entry about Many to Many relationships in LINQ to SQL.
I can live with that – it’s not really difficult to deal with this arrangement once mapped, especially when reading data back. Writing is a little more difficult as you do have to insert into two entities for new records, but nothing that can’t be handled in a small business object method with a few lines of code.
When I created a database I’ve been using to experiment around with various different OR/Ms recently I found that for some reason LINQ to SQL was completely failing to map even to the linking table. As it turns out there’s a good reason why it fails, can you spot it below? (read on :-})
Here is the original database layout:
There’s an items table, a category table and a link table that holds only the foreign keys to the Items and Category tables for a typical M->M relationship.
When these three tables are imported into the model the *look* correct – I do get the relationships added (after modifying the entity names to strip the prefix):
The relationship looks perfectly fine, both in the designer as well as in the XML document:
<Table Name="dbo.wws_Item_Categories" Member="ItemCategories">
<Type Name="ItemCategory">
<Column Name="ItemId" Type="System.Guid" DbType="uniqueidentifier NOT NULL" CanBeNull="false" />
<Column Name="CategoryId" Type="System.Guid" DbType="uniqueidentifier NOT NULL" CanBeNull="false" />
<Association Name="ItemCategory_Category" Member="Categories" ThisKey="CategoryId" OtherKey="Id" Type="Category" />
<Association Name="Item_ItemCategory" Member="Item" ThisKey="ItemId" OtherKey="Id" Type="Item" IsForeignKey="true" />
</Type>
</Table>
<Table Name="dbo.wws_Categories" Member="Categories">
<Type Name="Category">
<Column Name="Id" Type="System.Guid" DbType="UniqueIdentifier NOT NULL" IsPrimaryKey="true" IsDbGenerated="true" CanBeNull="false" />
<Column Name="ParentId" Type="System.Guid" DbType="UniqueIdentifier" CanBeNull="true" />
<Column Name="CategoryName" Type="System.String" DbType="NVarChar(150)" CanBeNull="true" />
<Column Name="CategoryDescription" Type="System.String" DbType="NVarChar(MAX)" CanBeNull="true" />
<Column Name="tstamp" AccessModifier="Internal" Type="System.Data.Linq.Binary" DbType="rowversion" CanBeNull="true" IsVersion="true" />
<Association Name="ItemCategory_Category" Member="ItemCategory" ThisKey="Id" OtherKey="CategoryId" Type="ItemCategory" IsForeignKey="true" />
</Type>
</Table>
However when looking at the code generated these navigation properties (also on Item) are completely missing:
[global::System.Data.Linq.Mapping.TableAttribute(Name="dbo.wws_Item_Categories")]
[global::System.Runtime.Serialization.DataContractAttribute()]
public partial class ItemCategory : Westwind.BusinessFramework.EntityBase
{
private System.Guid _ItemId;
private System.Guid _CategoryId;
public ItemCategory()
{
}
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_ItemId", DbType="uniqueidentifier NOT NULL")]
[global::System.Runtime.Serialization.DataMemberAttribute(Order=1)]
public System.Guid ItemId
{
get
{
return this._ItemId;
}
set
{
if ((this._ItemId != value))
{
this._ItemId = value;
}
}
}
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_CategoryId", DbType="uniqueidentifier NOT NULL")]
[global::System.Runtime.Serialization.DataMemberAttribute(Order=2)]
public System.Guid CategoryId
{
get
{
return this._CategoryId;
}
set
{
if ((this._CategoryId != value))
{
this._CategoryId = value;
}
}
}
}
Notice that the Item and Category association properties which should be EntityRef properties are completely missing. They’re there in the model, but the generated code – not so much.
So what’s the problem here?
The problem – it appears – is that LINQ to SQL requires primary keys on all entities it tracks. In order to support tracking – even of the link table entity – the link table requires a primary key. Real obvious ain’t it, especially since the designer happily lets you import the table and even shows the relationship and implicitly the related properties.
Adding an Id field as a Pk to the database and then importing results in this model layout:
which properly generates the Item and Category properties into the link entity.
It’s ironic that LINQ to SQL *requires* the PK in the middle – the Entity Framework requires that a link table have *only* the two foreign key fields in a table in order to recognize a many to many relation. EF actually handles the M->M relation directly without the intermediate link entity unlike LINQ to SQL.
[updated from comments – 12/24/2009]
Another approach is to set up both ItemId and CategoryId in the database which shows up in LINQ to SQL like this:
This also work in creating the Category and Item fields in the ItemCategory entity. Ultimately this is probably the best approach as it also guarantees uniqueness of the keys and so helps in database integrity.
It took me a while to figure out WTF was going on here – lulled by the designer to think that the properties should be when they were not. It’s actually a well documented feature of L2S that each entity in the model requires a Pk but of course that’s easy to miss when the model viewer shows it to you and even the underlying XML model shows the Associations properly.
This is one of the issue with L2S of course – you have to play by its rules and once you hit one of those rules there’s no way around them – you’re stuck with what it requires which in this case meant changing the database.
December 21, 2009 @ 1:20 am
- from Maui, Hawaii
ASP.Net includes quite a plethora of properties to retrieve path information about the current request, control and application. There's a ton of information available about paths on the Request object, some of it appearing to overlap and some of it buried several levels down, and it can be confusing to find just the right path that you are looking for.
To keep things straight I thought it a good idea to summarize the path options along with descriptions and example paths. I wrote a post about this a long time ago in 2004 and I find myself frequently going back to that page to quickly figure out which path I’m looking for in processing the current URL. Apparently a lot of people must be doing the same, because the original post is the second most visited even to this date on this blog to the tune of nearly 500 hits per day. So, I decided to update and expand a bit on the original post with a little more information and clarification based on the original comments.
Request Object Paths Available
Here's a list of the Path related properties on the Request object (and the Page object). Assume a path like http://www.west-wind.com/webstore/admin/paths.aspx for the paths below where webstore is the name of the virtual.
| ApplicationPath | Returns the web root-relative logical path to the virtual root of this app. /webstore/ |
| PhysicalApplicationPath | Returns local file system path of the virtual root for this app. c:\inetpub\wwwroot\webstore |
| PhysicalPath | Returns the local file system path to the current script or path. c:\inetpub\wwwroot\webstore\admin\paths.aspx |
Path FilePath CurrentExecutionFilePath
| All of these return the full root relative logical path to the script page including path and scriptname. CurrentExcecutionFilePath will return the ‘current’ request path after a Transfer/Execute call while FilePath will always return the original request’s path. /webstore/admin/paths.aspx |
| AppRelativeCurrentExecutionFilePath | Returns an ASP.NET root relative virtual path to the script or path for the current request. If in a Transfer/Execute call the transferred Path is returned. ~/admin/paths.aspx |
| PathInfo | Returns any extra path following the script name. If no extra path is provided returns the root-relative path (returns text in red below). string.Empty if no PathInfo is available. /webstore/admin/paths.aspx/ExtraPathInfo |
| RawUrl | Returns the full root relative relative URL including querystring and extra path as a string. /webstore/admin/paths.aspx?sku=wwhelp40 |
| Url | Returns a fully qualified URL including querystring and extra path. Note this is a Uri instance rather than string. http://www.west-wind.com/webstore/admin/paths.aspx?sku=wwhelp40 |
| UrlReferrer | The fully qualified URL of the page that sent the request. This is also a Uri instance and this value is null if the page was directly accessed by typing into the address bar or using an HttpClient. Based Referrer client Http header. http://www.west-wind.com/webstore/default.aspx?Info |
| Control.TemplateSourceDirectory | Returns the logical path to the folder of the page, master or user control on which it is called. This is useful if you need to know the path only to a Page or control from within the control. For non-file controls this returns the Page path. /webstore/admin/ |
As you can see there’s a ton of information available there for each of the three common path formats:
- Physical Path
is an OS type path that points to a path or file on disk.
- Logical Path
is a Web path that is relative to the Web server’s root. It includes the virtual plus the application relative path.
- ~/ (Root-relative) Path
is an ASP.NET specific path that includes ~/ to indicate the virtual root Web path. ASP.NET can convert virtual paths into either logical paths using Control.ResolveUrl(), or physical paths using Server.MapPath(). Root relative paths are useful for specifying portable URLs that don’t rely on relative directory structures and very useful from within control or component code.
You should be able to get any necessary format from ASP.NET from just about any path or script using these mechanisms.
~/ Root Relative Paths and ResolveUrl() and ResolveClientUrl()
ASP.NET supports root-relative virtual path syntax in most of its URL properties in Web Forms. So you can easily specify a root relative path in a control rather than a location relative path:
<asp:Image runat="server" ID="imgHelp" ImageUrl="~/images/help.gif" />
ASP.NET internally resolves this URL by using ResolveUrl("~/images/help.gif") to arrive at the root-relative URL of /webstore/images/help.gif which uses the Request.ApplicationPath as the basepath to replace the ~. By convention any custom Web controls also should use ResolveUrl() on URL properties to provide the same functionality.
In your own code you can use Page.ResolveUrl() or Control.ResolveUrl() to accomplish the same thing:
string imgPath = this.ResolveUrl("~/images/help.gif");
imgHelp.ImageUrl = imgPath;
Unfortunately ResolveUrl() is limited to WebForm pages, so if you’re in an HttpHandler or Module it’s not available.
ASP.NET Mvc also has it’s own more generic version of ResolveUrl in Url.Decode:
<script src="<%= Url.Content("~/scripts/new.js") %>" type="text/javascript"></script>
which is part of the UrlHelper class. In ASP.NET MVC the above sort of syntax is actually even more crucial than in WebForms due to the fact that views are not referencing specific pages but rather are often path based which can lead to various variations on how a particular view is referenced.
In a Module or Handler code Control.ResolveUrl() unfortunately is not available which in retrospect seems like an odd design choice – URL resolution really should happen on a Request basis not as part of the Page framework. Luckily you can also rely on the static VirtualPathUtility class:
string path = VirtualPathUtility.ToAbsolute("~/admin/paths.aspx");
VirtualPathUtility also many other quite useful methods for dealing with paths and converting between the various kinds of paths supported. One thing to watch out for is that ToAbsolute() will throw an exception if a query string is provided and doesn’t work on fully qualified URLs. I wrote about this topic with a custom solution that works fully qualified URLs and query strings here (check comments for some interesting discussions too).
Similar to ResolveUrl() is ResolveClientUrl() which creates a fully qualified HTTP path that includes the protocol and domain name. It’s rare that this full resolution is needed but can be useful in some scenarios.
Mapping Virtual Paths to Physical Paths with Server.MapPath()
If you need to map root relative or current folder relative URLs to physical URLs or you can use HttpContext.Current.Server.MapPath(). Inside of a Page you can do the following:
string physicalPath = Server.MapPath("~/scripts/ww.jquery.js"));
MapPath is pretty flexible and it understands both ASP.NET style virtual paths as well as plain relative paths, so the following also works.
string physicalPath = Server.MapPath("scripts/silverlight.js");
as well as dot relative syntax:
string physicalPath = Server.MapPath("../scripts/jquery.js");
Once you have the physical path you can perform standard System.IO Path and File operations on the file. Remember with physical paths and IO or copy operations you need to make sure you have permissions to access files and folders based on the Web server user account that is active (NETWORK SERVICE, ASPNET typically).
Server and Host Information
Between these settings you can get all the information you may need to figure out where you are at and to build new Url if necessary. If you need to build a URL completely from scratch you can get access to information about the server you are accessing:
| SERVER_NAME |
The of the domain or IP Address
wwww.west-wind.com or 127.0.0.1 |
| SERVER_PORT |
The port that the request runs under.
80 |
| SERVER_PORT_SECURE |
Determines whether https: was used.
0 or 1 |
| APPL_MD_PATH |
ADSI DirectoryServices path to the virtual root directory. Note that LM typically doesn’t work for ADSI access so you should replace that with LOCALHOST or the machine’s NetBios name.
/LM/W3SVC/1/ROOT/webstore |
Request.Url and Uri Parsing
If you still need more control over the current request URL or you need to create new URLs from an existing one, the current Request.Url Uri property offers a lot of control. Using the Uri class and UriBuilder makes it easy to retrieve parts of a URL and create new URLs based on existing URL. The UriBuilder class is the preferred way to create URLs – much preferable over creating URIs via string concatenation.
| Scheme |
The URL scheme or protocol prefix.
http or https |
| Port |
The port if specifically specified. |
| DnsSafeHost |
The domain name or local host NetBios machine name
www.west-wind.com or rasnote |
| LocalPath |
The full path of the URL including script name and extra PathInfo.
/webstore/admin/paths.aspx |
| Query |
The query string if any
?id=1 |
The Uri class itself is great for retrieving Uri parts, but most of the properties are read only if you need to modify a URL in order to change it you can use the UriBuilder class to load up an existing URL and modify it to create a new one.
Here are a few common operations I’ve needed to do to get specific URLs:
Convert the Request URL to an SSL/HTTPS link
For example to take the current request URL and converted it to a secure URL can be done like this:
UriBuilder build = new UriBuilder(Request.Url);
build.Scheme = "https";
build.Port = -1; // don't inject port
Uri newUri = build.Uri;
string newUrl = build.ToString();
Retrieve the fully qualified URL without a QueryString
AFAIK, there’s no native routine to retrieve the current request URL without the query string. It’s easy to do with UriBuilder however:
UriBuilder builder = newUriBuilder(Request.Url);
builder.Query = "";
stringlogicalPathWithoutQuery = builder.ToString();
What else?
I took a look through the old post’s comments and addressed as many of the questions and comments that came up in there. With a few small and silly exceptions this update post handles most of these.
But I’m sure there are a more things that go in here. What else would be useful to put onto this post so it serves as a nice all in one place to go for path references? If you think of something leave a comment and I’ll try to update the post with it in the future.
December 18, 2009 @ 3:00 pm
- from Maui, Hawaii
Got a note a couple of days ago from a client using one of my generic routines that wraps SmtpClient. Apparently whenever a file has been attached to a message and emailed with SmtpClient the file remains locked after the message has been sent. Oddly this particular issue hasn’t cropped up before for me although these routines are in use in a number of applications I’ve built.
The wrapper I use was built mainly to backfit an old pre-.NET 2.0 email client I built using Sockets to avoid the CDO nightmares of the .NET 1.x mail client. The current class retained the same class interface but now internally uses SmtpClient which holds a flat property interface that makes it less verbose to send off email messages.
File attachments in this interface are handled by providing a comma delimited list for files in an Attachments string property which is then collected along with the other flat property settings and eventually passed on to SmtpClient in the form of a MailMessage structure.
The jist of the code is something like this:
/// <summary>
/// Fully self contained mail sending method. Sends an email message by connecting
/// and disconnecting from the email server.
/// </summary>
/// <returns>true or false</returns>
public bool SendMail()
{
if (!this.Connect())
return false;
try
{
// Create and configure the message
MailMessage msg = this.GetMessage();
smtp.Send(msg);
this.OnSendComplete(this);
}
catch (Exception ex)
{
string msg = ex.Message;
if (ex.InnerException != null)
msg = ex.InnerException.Message;
this.SetError(msg);
this.OnSendError(this);
return false;
}
finally
{
// close connection and clear out headers
// SmtpClient instance nulled out
this.Close();
}
return true;
}
/// <summary>
/// Configures the message interface
/// </summary>
/// <param name="msg"></param>
protected virtual MailMessage GetMessage()
{
MailMessage msg = new MailMessage();
msg.Body = this.Message;
msg.Subject = this.Subject;
msg.From = new MailAddress(this.SenderEmail, this.SenderName);
if (!string.IsNullOrEmpty(this.ReplyTo))
msg.ReplyTo = new MailAddress(this.ReplyTo);
// Send all the different recipients
this.AssignMailAddresses(msg.To, this.Recipient);
this.AssignMailAddresses(msg.CC, this.CC);
this.AssignMailAddresses(msg.Bcc, this.BCC);
if (!string.IsNullOrEmpty(this.Attachments))
{
string[] files = this.Attachments.Split(new char[2] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries);
foreach (string file in files)
{
msg.Attachments.Add(new Attachment(file));
}
}
if (this.ContentType.StartsWith("text/html"))
msg.IsBodyHtml = true;
else
msg.IsBodyHtml = false;
msg.BodyEncoding = this.Encoding;
… additional code omitted
return msg;
}
Basically this code collects all the property settings of the wrapper object and applies them to the SmtpClient and in GetMessage() to an individual MailMessage properties. Specifically notice that attachment filenames are converted from a comma-delimited string to filenames from which new attachments are created.
The code as it’s written however, will cause the problem with file attachments not being released properly. Internally .NET opens up stream handles and reads the files from disk to dump them into the email send stream. The attachments are always sent correctly but the local files are not immediately closed.
As you probably guessed the issue is simply that some resources are not automatcially disposed when sending is complete and sure enough the following code change fixes the problem:
// Create and configure the message
using (MailMessage msg = this.GetMessage())
{
smtp.Send(msg);
if (this.SendComplete != null)
this.OnSendComplete(this);
// or use an explicit msg.Dispose() here
}
The Message object requires an explicit call to Dispose() (or a using() block as I have here) to force the attachment files to get closed.
I think this is rather odd behavior for this scenario however. The code I use passes in filenames and my expectation of an API that accepts file names is that it uses the files by opening and streaming them and then closing them when done. Why keep the streams open and require an explicit .Dispose() by the calling code which is bound to lead to unexpected behavior just as my customer ran into? Any API level code should clean up as much as possible and this is clearly not happening here resulting in unexpected behavior. Apparently lots of other folks have run into this before as I found based on a few Twitter comments on this topic.
Odd to me too is that SmtpClient() doesn’t implement IDisposable – it’s only the MailMessage (and Attachments) that implement it and require it to clean up for left over resources like open file handles. This means that you couldn’t even use a using() statement around the SmtpClient code to resolve this – instead you’d have to wrap it around the message object which again is rather unexpected.
Well, chalk that one up to another small unexpected behavior that wasted a half an hour of my time – hopefully this post will help someone avoid this same half an hour of hunting and searching.
Resources:
December 14, 2009 @ 12:15 am
- from Maui, Hawaii
Paging in ASP.NET has been relatively easy with stock controls supporting basic paging functionality. However, recently I built an MVC application and one of the things I ran into was that I HAD TO build manual paging support into a few of my pages. Dealing with list controls and rendering markup is easy enough, but doing paging is a little more involved. I ended up with a small but flexible component that can be dropped anywhere. As it turns out the task of creating a semi-generic Pager control for MVC was fairly easily.
Now I’m back to working in Web Forms and thought to myself that the way I created the pager in MVC actually would also work in ASP.NET – in fact quite a bit easier since the whole thing can be conveniently wrapped up into an easily reusable control. A standalone pager would provider easier reuse in various pages and a more consistent pager display regardless of what kind of 'control’ the pager is associated with.
Why a Pager Control?
At first blush it might sound silly to create a new pager control – after all Web Forms has pretty decent paging support, doesn’t it? Well, sort of. Yes the GridView control has automatic paging built in and the ListView control has the related DataPager control.
The built in ASP.NET paging has several issues though:
- Postback and JavaScript requirements
If you look at paging links in ASP.NET they are always postback links with javascript:__doPostback() calls that go back to the server. While that works fine and actually has some benefit like the fact that paging saves changes to the page and post them back, it’s not very SEO friendly. Basically if you use javascript based navigation nosearch engine will follow the paging links which effectively cuts off list content on the first page. The DataPager control does support GET based links via the QueryStringParameter property, but the control is effectively tied to the ListView control (which is the only control that implements IPageableItemContainer).
- DataSource Controls required for Efficient Data Paging Retrieval
The only way you can get paging to work efficiently where only the few records you display on the page are queried for and retrieved from the database you have to use a DataSource control - only the Linq and Entity DataSource controls support this natively. While you can retrieve this data yourself manually, there’s no way to just assign the page number and render the pager based on this custom subset. Other than that default paging requires a full resultset for ASP.NET to filter the data and display only a subset which can be very resource intensive and wasteful if you’re dealing with largish resultsets (although I’m a firm believer in returning actually usable sets :-}). If you use your own business layer that doesn’t fit an ObjectDataSource you’re SOL. That’s a real shame too because with LINQ based querying it’s real easy to retrieve a subset of data that is just the data you want to display but the native Pager functionality doesn’t support just setting properties to display just the subset AFAIK.
- DataPager is not Free Standing
The DataPager control is the closest thing to a decent Pager implementation that ASP.NET has, but alas it’s not a free standing component – it works off a related control and the only one that it effectively supports from the stock ASP.NET controls is the ListView control. This means you can’t use the same data pager formatting for a grid and a list view or vice versa and you’re always tied to the control.
- Paging Events
In order to handle paging you have to deal with paging events. The events fire at specific time instances in the page pipeline and because of this you often have to handle data binding in a way to work around the paging events or else end up double binding your data sources based on paging. Yuk.
- Styling
The GridView pager is a royal pain to beat into submission for styled rendering. The DataPager control has many more options and template layout and it renders somewhat cleaner, but it too is not exactly easy to get a decent display for.
- Not a Generic Solution
The problem with the ASP.NET controls too is that it’s not generic. GridView, DataGrid use their own internal paging, ListView can use a DataPager and if you want to manually create data layout – well you’re on your own. IOW, depending on what you use you likely have very different looking Paging experiences.
So, I figured I’ve struggled with this once too many and finally sat down and built a Pager control.
The Pager Control
My goal was to create a totally free standing control that has no dependencies on other controls and certainly no requirements for using DataSource controls. The idea is that you should be able to use this pager control without any sort of data requirements at all – you should just be able to set properties and be able to display a pager.
The Pager control I ended up with has the following features:
- Completely free standing Pager control – no control or data dependencies
- Complete manual control – Pager can render without any data dependency
- Easy to use: Only need to set PageSize, ActivePage and TotalItems
- Supports optional filtering of IQueryable for efficient queries and Pager rendering
- Supports optional full set filtering of IEnumerable<T> and DataTable
- Page links are plain HTTP GET href Links
- Control automatically picks up Page links on the URL and assigns them
(automatic page detection no page index changing events to hookup) - Full CSS Styling support
On the downside there’s no templating support for the control so the layout of the pager is relatively fixed. All elements however are stylable and there are options to control the text, and layout options such as whether to display first and last pages and the previous/next buttons and so on.
To give you an idea what the pager looks like, here are two differently styled examples (all via CSS):
The markup for these two pagers looks like this:
<ww:Pager runat="server" id="ItemPager"
PageSize="5"
PageLinkCssClass="gridpagerbutton"
SelectedPageCssClass="gridpagerbutton-selected"
PagesTextCssClass="gridpagertext"
CssClass="gridpager"
RenderContainerDiv="true"
ContainerDivCssClass="gridpagercontainer"
MaxPagesToDisplay="6"
PagesText="Item Pages:"
NextText="next"
PreviousText="previous"
/>
<ww:Pager runat="server" id="ItemPager2"
PageSize="5"
RenderContainerDiv="true"
MaxPagesToDisplay="6" />
The latter example uses default style settings so it there’s not much to set. The first example on the other hand explicitly assigns custom styles and overrides a few of the formatting options.
Styling
The styling is based on a number of CSS classes of which the the main pager, pagerbutton and pagerbutton-selected classes are the important ones. Other styles like pagerbutton-next/prev/first/last are based on the pagerbutton style.
The default styling shown for the red outlined pager looks like this:
.pagercontainer
{
margin: 20px 0;
background: whitesmoke;
padding: 5px;
}
.pager
{
float: right;
font-size: 10pt;
text-align: left;
}
.pagerbutton,.pagerbutton-selected,.pagertext
{
display: block;
float: left;
text-align: center;
border: solid 2px maroon;
min-width: 18px;
margin-left: 3px;
text-decoration: none;
padding: 4px;
}
.pagerbutton-selected
{
font-size: 130%;
font-weight: bold;
color: maroon;
border-width: 0px;
background: khaki;
}
.pagerbutton-first
{
margin-right: 12px;
}
.pagerbutton-last,.pagerbutton-prev
{
margin-left: 12px;
}
.pagertext
{
border: none;
margin-left: 30px;
font-weight: bold;
}
.pagerbutton a
{
text-decoration: none;
}
.pagerbutton:hover
{
background-color: maroon;
color: cornsilk;
}
.pagerbutton-prev
{
background-image: url(images/prev.png);
background-position: 2px center;
background-repeat: no-repeat;
width: 35px;
padding-left: 20px;
}
.pagerbutton-next
{
background-image: url(images/next.png);
background-position: 40px center;
background-repeat: no-repeat;
width: 35px;
padding-right: 20px;
margin-right: 0px;
}
Yup that’s a lot of styling settings although not all of them are required. The key ones are pagerbutton, pager and pager selection. The others (which are implicitly created by the control based on the pagerbutton style) are for custom markup of the ‘special’ buttons.
In my apps I tend to have two kinds of pages: Those that are associated with typical ‘grid’ displays that display purely tabular data and those that have a more looser list like layout. The two pagers shown above represent these two views and the pager and gridpager styles in my standard style sheet reflect these two styles.
Configuring the Pager with Code
Finally lets look at what it takes to hook up the pager. As mentioned in the highlights the Pager control is completely independent of other controls so if you just want to display a pager on its own it’s as simple as dropping the control and assigning the PageSize, ActivePage and either TotalPages or TotalItems.
So for this markup:
<ww:Pager runat="server" id="ItemPagerManual"
PageSize="5"
MaxPagesToDisplay="6"
/>
I can use code as simple as:
ItemPagerManual.PageSize = 3;
ItemPagerManual.ActivePage = 4;
ItemPagerManual.TotalItems = 20;
Note that ActivePage is not required - it will automatically use any Page=x query string value and assign it, although you can override it as I did above. TotalItems can be any value that you retrieve from a result set or manually assign as I did above.
A more realistic scenario based on a LINQ to SQL IQueryable result is even easier. In this example, I have a UserControl that contains a ListView control that renders IQueryable data. I use a User Control here because there are different views the user can choose from with each view being a different user control. This incidentally also highlights one of the nice features of the pager: Because the pager is independent of the control I can put the pager on the host page instead of into each of the user controls. IOW, there’s only one Pager control, but there are potentially many user controls/listviews that hold the actual display data.
The following code demonstrates how to use the Pager with an IQueryable that loads only the records it displays: protected voidPage_Load(objectsender, EventArgs e)
{
Category = Request.Params["Category"] ?? string.Empty;
IQueryable<wws_Item> ItemList = ItemRepository.GetItemsByCategory(Category);
// Update the page and filter the list down
ItemList = ItemPager.FilterIQueryable<wws_Item>(ItemList);
// Render user control with a list view
Control ulItemList = LoadControl("~/usercontrols/" + App.Configuration.ItemListType + ".ascx");
((IInventoryItemListControl)ulItemList).InventoryItemList = ItemList;
phItemList.Controls.Add(ulItemList); // placeholder
}
The code uses a business object to retrieve Items by category as an IQueryable which means that the result is only an expression tree that hasn’t execute SQL yet and can be further filtered. I then pass this IQueryable to the FilterIQueryable() helper method of the control which does two main things:
- Filters the IQueryable to retrieve only the data displayed on the active page
- Sets the Totaltems property and calculates TotalPages on the Pager
and that’s it! When the Pager renders it uses those values, plus the PageSize and ActivePage properties to render the Pager.
In addition to IQueryable there are also filter methods for IEnumerable<T> and DataTable, but these versions just filter the data by removing rows/items from the entire already retrieved data.
Output Generated and Paging Links
The output generated creates pager links as plain href links. Here’s what the output looks like:
<div id="ItemPager" class="pagercontainer">
<div class="pager">
<span class="pagertext">Pages: </span><a href="http://localhost/WestWindWebStore/itemlist.aspx?Page=1" class="pagerbutton" />1</a>
<a href="http://localhost/WestWindWebStore/itemlist.aspx?Page=2" class="pagerbutton" />2</a>
<a href="http://localhost/WestWindWebStore/itemlist.aspx?Page=3" class="pagerbutton" />3</a>
<span class="pagerbutton-selected">4</span>
<a href="http://localhost/WestWindWebStore/itemlist.aspx?Page=5" class="pagerbutton" />5</a>
<a href="http://localhost/WestWindWebStore/itemlist.aspx?Page=6" class="pagerbutton" />6</a>
<a href="http://localhost/WestWindWebStore/itemlist.aspx?Page=20" class="pagerbutton pagerbutton-last" />20</a> <a href="http://localhost/WestWindWebStore/itemlist.aspx?Page=3" class="pagerbutton pagerbutton-prev" />Prev</a> <a href="http://localhost/WestWindWebStore/itemlist.aspx?Page=5" class="pagerbutton pagerbutton-next" />Next</a></div>
<br clear="all" />
</div>
</div>
The links point back to the current page and simply append a Page= page link into the page. When the page gets reloaded with the new page number the pager automatically detects the page number and automatically assigns the ActivePage property which results in the appropriate page to be displayed. The code shown in the previous section is all that’s needed to handle paging.
Note that HTTP GET based paging is different than the Postback paging ASP.NET uses by default. Postback paging preserves modified page content when clicking on pager buttons, but this control will simply load a new page – no page preservation at this time.
The advantage of not using Postback paging is that the URLs generated are plain HTML links that a search engine can follow where __doPostback() links are not.
Pager with a Grid
The pager also works in combination with grid controls so it’s easy to bypass the grid control’s paging features if desired. In the following example I use a gridView control and binds it to a DataTable result which is also filterable by the Pager control.
The very basic plain vanilla ASP.NET grid markup looks like this:
<div style="width: 600px; margin: 0 auto;padding: 20px; ">
<asp:DataGrid runat="server" AutoGenerateColumns="True"
ID="gdItems" CssClass="blackborder" style="width: 600px;">
<AlternatingItemStyle CssClass="gridalternate" />
<HeaderStyle CssClass="gridheader" />
</asp:DataGrid>
<ww:Pager runat="server" ID="Pager"
CssClass="gridpager"
ContainerDivCssClass="gridpagercontainer"
PageLinkCssClass="gridpagerbutton"
SelectedPageCssClass="gridpagerbutton-selected"
PageSize="8"
RenderContainerDiv="true"
MaxPagesToDisplay="6" />
</div>
and looks like this when rendered:
using custom set of CSS styles. The code behind for this code is also very simple:
protected void Page_Load(object sender, EventArgs e)
{
string category = Request.Params["category"] ?? "";
busItem itemRep = WebStoreFactory.GetItem();
var items = itemRep.GetItemsByCategory(category)
.Select(itm => new {Sku = itm.Sku, Description = itm.Description});
// run query into a DataTable for demonstration
DataTable dt = itemRep.Converter.ToDataTable(items,"TItems");
// Remove all items not on the current page
dt = Pager.FilterDataTable(dt,0);
// bind and display
gdItems.DataSource = dt;
gdItems.DataBind();
}
A little contrived I suppose since the list could already be bound from the list of elements, but this is to demonstrate that you can also bind against a DataTable if your business layer returns those.
Unfortunately there’s no way to filter a DataReader as it’s a one way forward only reader and the reader is required by the DataSource to perform the bindings. However, you can still use a DataReader as long as your business logic filters the data prior to rendering and provides a total item count (most likely as a second query).
Control Creation
The control itself is a pretty brute force ASP.NET control. Nothing clever about this other than some basic rendering logic and some simple calculations and update routines to determine which buttons need to be shown. You can take a look at the full code from the West Wind Web Toolkit’s Repository (note there are a few dependencies).
To give you an idea how the control works here is the Render() method:
/// <summary>
/// overridden to handle custom pager rendering for runtime and design time
/// </summary>
/// <param name="writer"></param>
protected override void Render(HtmlTextWriter writer)
{
base.Render(writer);
if (TotalPages == 0 && TotalItems > 0)
TotalPages = CalculateTotalPagesFromTotalItems();
if (DesignMode)
TotalPages = 10;
// don't render pager if there's only one page
if (TotalPages < 2)
return;
if (RenderContainerDiv)
{
if (!string.IsNullOrEmpty(ContainerDivCssClass))
writer.AddAttribute("class", ContainerDivCssClass);
writer.RenderBeginTag("div");
}
// main pager wrapper
writer.WriteBeginTag("div");
writer.AddAttribute("id", this.ClientID);
if (!string.IsNullOrEmpty(CssClass))
writer.WriteAttribute("class", this.CssClass);
writer.Write(HtmlTextWriter.TagRightChar + "\r\n");
// Pages Text
writer.WriteBeginTag("span");
if (!string.IsNullOrEmpty(PagesTextCssClass))
writer.WriteAttribute("class", PagesTextCssClass);
writer.Write(HtmlTextWriter.TagRightChar);
writer.Write(this.PagesText);
writer.WriteEndTag("span");
// if the base url is empty use the current URL
FixupBaseUrl();
// set _startPage and _endPage
ConfigurePagesToRender();
// write out first page link
if (ShowFirstAndLastPageLinks && _startPage != 1)
{
writer.WriteBeginTag("a");
string pageUrl = StringUtils.SetUrlEncodedKey(BaseUrl, QueryStringPageField, (1).ToString());
writer.WriteAttribute("href", pageUrl);
if (!string.IsNullOrEmpty(PageLinkCssClass))
writer.WriteAttribute("class", PageLinkCssClass + " " + PageLinkCssClass + "-first");
writer.Write(HtmlTextWriter.SelfClosingTagEnd);
writer.Write("1");
writer.WriteEndTag("a");
writer.Write(" ");
}
// write out all the page links
for (int i = _startPage; i < _endPage + 1; i++)
{
if (i == ActivePage)
{
writer.WriteBeginTag("span");
if (!string.IsNullOrEmpty(SelectedPageCssClass))
writer.WriteAttribute("class", SelectedPageCssClass);
writer.Write(HtmlTextWriter.TagRightChar);
writer.Write(i.ToString());
writer.WriteEndTag("span");
}
else
{
writer.WriteBeginTag("a");
string pageUrl = StringUtils.SetUrlEncodedKey(BaseUrl, QueryStringPageField, i.ToString()).TrimEnd('&');
writer.WriteAttribute("href", pageUrl);
if (!string.IsNullOrEmpty(PageLinkCssClass))
writer.WriteAttribute("class", PageLinkCssClass);
writer.Write(HtmlTextWriter.SelfClosingTagEnd);
writer.Write(i.ToString());
writer.WriteEndTag("a");
}
writer.Write("\r\n");
}
// write out last page link
if (ShowFirstAndLastPageLinks && _endPage < TotalPages)
{
writer.WriteBeginTag("a");
string pageUrl = StringUtils.SetUrlEncodedKey(BaseUrl, QueryStringPageField, TotalPages.ToString());
writer.WriteAttribute("href", pageUrl);
if (!string.IsNullOrEmpty(PageLinkCssClass))
writer.WriteAttribute("class", PageLinkCssClass + " " + PageLinkCssClass + "-last");
writer.Write(HtmlTextWriter.SelfClosingTagEnd);
writer.Write(TotalPages.ToString());
writer.WriteEndTag("a");
}
// Previous link
if (ShowPreviousNextLinks && !string.IsNullOrEmpty(PreviousText) && ActivePage > 1)
{
writer.Write(" ");
writer.WriteBeginTag("a");
string pageUrl = StringUtils.SetUrlEncodedKey(BaseUrl, QueryStringPageField, (ActivePage - 1).ToString());
writer.WriteAttribute("href", pageUrl);
if (!string.IsNullOrEmpty(PageLinkCssClass))
writer.WriteAttribute("class", PageLinkCssClass + " " + PageLinkCssClass + "-prev");
writer.Write(HtmlTextWriter.SelfClosingTagEnd);
writer.Write(PreviousText);
writer.WriteEndTag("a");
}
// Next link
if (ShowPreviousNextLinks && !string.IsNullOrEmpty(NextText) && ActivePage < TotalPages)
{
writer.Write(" ");
writer.WriteBeginTag("a");
string pageUrl = StringUtils.SetUrlEncodedKey(BaseUrl, QueryStringPageField, (ActivePage + 1).ToString());
writer.WriteAttribute("href", pageUrl);
if (!string.IsNullOrEmpty(PageLinkCssClass))
writer.WriteAttribute("class", PageLinkCssClass + " " + PageLinkCssClass + "-next");
writer.Write(HtmlTextWriter.SelfClosingTagEnd);
writer.Write(NextText);
writer.WriteEndTag("a");
}
writer.WriteEndTag("div");
if (RenderContainerDiv)
{
if (RenderContainerDivBreak)
writer.Write("<br clear=\"all\" />\r\n");
writer.WriteEndTag("div");
}
}
As I said pretty much brute force rendering based on the control’s property settings of which there are quite a few:
You can also see the pager in the designer above. unfortunately the VS designer (both 2010 and 2008) fails to render the float: left CSS styles properly and starts wrapping after margins are applied in the special buttons. Not a big deal since VS does at least respect the spacing (the floated elements overlay). Then again I’m not using the designer anyway :-}.
Filtering Data
What makes the Pager easy to use is the filter methods built into the control. While this functionality is clearly not the most politically correct design choice as it violates separation of concerns, it’s very useful for typical pager operation. While I actually have filter methods that do something similar in my business layer, having it exposed on the control makes the control a lot more useful for typical databinding scenarios. Of course these methods are optional – if you have a business layer that can provide filtered page queries for you can use that instead and assign the TotalItems property manually.
There are three filter method types available for IQueryable, IEnumerable and for DataTable which tend to be the most common use cases in my apps old and new. The IQueryable version is pretty simple as it can simply rely on on .Skip() and .Take() with LINQ:
/// <summary>
/// <summary>
/// Queries the database for the ActivePage applied manually
/// or from the Request["page"] variable. This routine
/// figures out and sets TotalPages, ActivePage and
/// returns a filtered subset IQueryable that contains
/// only the items from the ActivePage.
/// </summary>
/// <param name="query"></param>
/// <param name="activePage">
/// The page you want to display. Sets the ActivePage property when passed.
/// Pass 0 or smaller to use ActivePage setting.
/// </param>
/// <returns></returns>
public IQueryable<T> FilterIQueryable<T>(IQueryable<T> query, int activePage)
where T : class, new()
{
ActivePage = activePage < 1 ? ActivePage : activePage;
if (ActivePage < 1)
ActivePage = 1;
TotalItems = query.Count();
if (TotalItems <= PageSize)
{
ActivePage = 1;
TotalPages = 1;
return query;
}
int skip = ActivePage - 1;
if (skip > 0)
query = query.Skip(skip * PageSize);
_TotalPages = CalculateTotalPagesFromTotalItems();
return query.Take(PageSize);
}
The IEnumerable<T> version simply converts the IEnumerable to an IQuerable and calls back into this method for filtering.
The DataTable version requires a little more work to manually parse and filter records (I didn’t want to add the Linq DataSetExtensions assembly just for this):
/// <summary>
/// Filters a data table for an ActivePage.
///
/// Note: Modifies the data set permanently by remove DataRows
/// </summary>
/// <param name="dt">Full result DataTable</param>
/// <param name="activePage">Page to display. 0 to use ActivePage property </param>
/// <returns></returns>
public DataTable FilterDataTable(DataTable dt, int activePage)
{
ActivePage = activePage < 1 ? ActivePage : activePage;
if (ActivePage < 1)
ActivePage = 1;
TotalItems = dt.Rows.Count;
if (TotalItems <= PageSize)
{
ActivePage = 1;
TotalPages = 1;
return dt;
}
int skip = ActivePage - 1;
if (skip > 0)
{
for (int i = 0; i < skip * PageSize; i++ )
dt.Rows.RemoveAt(0);
}
while(dt.Rows.Count > PageSize)
dt.Rows.RemoveAt(PageSize);
return dt;
}
Using the Pager Control
The pager as it is is a first cut I built a couple of weeks ago and since then have been tweaking a little as part of an internal project I’m working on. I’ve replaced a bunch of pagers on various older pages with this pager without any issues and have what now feels like a more consistent user interface where paging looks and feels the same across different controls. As a bonus I’m only loading the data from the database that I need to display a single page. With the preset class tags applied too adding a pager is now as easy as dropping the control and adding the style sheet for styling to be consistent – no fuss, no muss. Schweet.
Hopefully some of you may find this as useful as I have or at least as a baseline to build ontop of…
Resources
December 05, 2009 @ 6:25 pm
- from Maui, Hawaii
I’ve been working with VS 2010 Beta 2 for a while now and while it works Ok most of the time it seems the environment is very, very fragile when it comes to crashes and installed packages. Specifically I’ve been working just fine for days, then when VS 2010 crashes it will not re-start. Instead I get the good old Application cannot start dialog:
Other failures I’ve seen bring forth other just as useful dialogs with information overload like Operation cannot be performed which for me specifically happens when trying to compile any project.
After a bit of digging around and a post to Microsoft Connect the solution boils down to resetting the VS.NET environment. The Application Cannot Start issue stems from a package load failure of some sort, so the work around for this is typically:
c:\program files\Visual Studio 2010\Common7\IDE\devenv.exe /ResetSkipPkgs
In most cases that should do the trick. If it doesn’t and the error doesn’t go away the more drastic:
c:\program files\Visual Studio 2010\Common7\IDE\devenv.exe /ResetSettings
is required which resets all settings in VS to its installation defaults. Between these two I’ve always been able to get VS to startup and run properly.
BTW it’s handy to keep a list of command line options for Visual Studio around:
http://msdn.microsoft.com/en-us/library/xee0c8y7%28VS.100%29.aspx
Note that the /? option in VS 2010 doesn’t display all the options available but rather displays the ‘demo version’ message instead, so the above should be helpful. Also note that unless you install Visual C++ the Visual Studio Command Prompt icon is not automatically installed so you may have to navigate manually to the appropriate folder above.
Cannot Build Failures
If you get the Cannot compile error dialog, there is another thing that have worked for me: Change your project build target from Debug to Release (or whatever – just change it) and compile again. If that doesn’t work doing the reset steps above will do it for me.
It appears this failure comes from some sort of interference of other versions of Visual Studio installed on the system and running another version first. Resetting the build target explicitly seems to reset the build providers to a normalized state so that things work in many cases. But not all. Worst case – resetting settings will do it.
The bottom line for working in VS 2010 has been – don’t get too attached to your custom settings as they will get blown away quite a bit. I’ve probably been through 20 or more of these VS resets although I’ve been working with it quite a bit on an internal project.
It’s kind of frustrating to see this kind of high level instability in a Beta 2 product which is supposedly the last public beta they will put out. On the other hand this beta has been otherwise rather stable and performance is roughly equivalent to VS 2008. Although I mention the crash above – crashes I’ve seen have been relatively rare and no more frequent than in VS 2008 it seems. Given the drastic UI changes in VS 2010 (using WPF for the shell and editor) I’m actually impressed that the product is as stable as it is at this point. Also I was seriously worried about text quality going to a WPF model, but thankfully WPF 4.0 addresses the blurry text issue with native font rendering to render text on non-cleartype enabled systems crisply.
Anyway I hope that these notes are helpful to some of you playing around with the beta and running into problems. Hopefully you won’t need them :-}
November 18, 2009 @ 4:54 am
- from Maui, Hawaii
I’ve posted the slides and samples to my DevConnections Sessions for anyone interested. I had a lot of fun with my sessions this time around mainly because the sessions picked were a little off the beaten track (well, the handlers/modules and e-commerce sessions anyway). For those of you that attended I hope you found the sessions useful. For the rest of you – you can check out the slides and samples if you like. Here’s what was covered:
Introduction to jQuery with ASP.NET
This session covered mostly the client side of jQuery demonstrated on a small sample page with a variety of incrementally built up examples of selection and page manipulation. This session also introduces some of the basics of AJAX communication talking to ASP.NET. When I do this session it never turns out exactly the same way and this time around the examples were on the more basic side and purely done with hands on demonstrations rather than walk throughs of more complex examples. Alas this session always feels like it needs another half an hour to get through the full sortiment of functionality. The slides and samples cover a wider variety of topics and there are many examples that demonstrate more advanced operations like interacting with WCF REST services, using client templating and building rich client only windowed interfaces.
Download
Low Level ASP.NET: Handlers and Modules
This session was a look at the ASP.NET pipeline and it discusses some of the ASP.NET base architecture and key components from HttpRuntime on up through the various modules and handlers that make up the ASP.NET/IIS pipeline. This session is fun as there are a number of cool examples that demonstrate the power and flexibility of ASP.NET, but some of the examples were external and interfacing with other technologies so they’re not actually included in the downloadable samples. However, there are still a few cool ones in there – there’s an image resizing handler, an image overlay module that stamps images with Sample if loaded from a certain folder, an OpenID authentication module (which failed during the demo due to the crappy internet connection at DevConnections this year :-}), Response filtering using a generic filter stream component, a generic error handler and a few others. The slides cover a lot of the ASP.NET pipeline flow and various HttpRuntime components.
Download
Electronic Payment Processing in ASP.NET Applications
This session covered the business end and integration of electronic credit card processing and PayPal. A good part of this session deals with what’s involved in payment processing, getting signed up and who you have to deal with for your merchant account. We then took a look at integration of credit card processing via some generic components provided with the session that allow processing using a unified class interface with specific implementations for several of the most common gateway providers including Authorize.NET, PayFlowPro, LinkPoint, BluePay etc. We also briefly looked at PayPal Classic implementation which provides a quick and cheap if not quite as professional mechanism for taking payments online. The samples provide the Credit Card processing wrappers for the various gateway providers as well as a PayPal helper class to generate the PayPal redirect urls as well as helper code for dealing with IPN callbacks.
Download
Hope some of you will find the material useful. Enjoy.
November 13, 2009 @ 9:47 am
- from Las Vegas, Nevada
During one of my Handlers and Modules session at DevConnections this week one of the attendees asked a question that I didn’t have an immediate answer for. Basically he wanted to capture response output completely and then apply some filtering to the output – effectively injecting some additional content into the page AFTER the page had completely rendered. Specifically the output should be captured from anywhere – not just a page and have this code injected into the page.
Some time ago I posted some code that allows you to capture ASP.NET Page output by overriding the Render() method, capturing the HtmlTextWriter() and reading its content, modifying the rendered data as text then writing it back out. I’ve actually used this approach on a few occasions and it works fine for ASP.NET pages. But this obviously won’t work outside of the Page class environment and it’s not really generic – you have to create a custom page class in order to handle the output capture.
[updated 11/16/2009 – updated ResponseFilterStream implementation and a few additional notes based on comments]
Enter Response.Filter
However, ASP.NET includes a Response.Filter which can be used – well to filter output. Basically Response.Filter is a stream through which the OutputStream is piped back to the Web Server (indirectly). As content is written into the Response object, the filter stream receives the appropriate Stream commands like Write, Flush and Close as well as read operations although for a Response.Filter that’s uncommon to be hit. The Response.Filter can be programmatically replaced at runtime which allows you to effectively intercept all output generation that runs through ASP.NET.
A common Example: Dynamic GZip Encoding
A rather common use of Response.Filter hooking up code based, dynamic GZip compression for requests which is dead simple by applying a GZipStream (or DeflateStream) to Response.Filter. The following generic routines can be used very easily to detect GZip capability of the client and compress response output with a single line of code and a couple of library helper routines:
WebUtils.GZipEncodePage();
which is handled with a few lines of reusable code and a couple of static helper methods:
/// <summary>
///Sets up the current page or handler to use GZip through a Response.Filter
///IMPORTANT:
///You have to call this method before any output is generated!
/// </summary>
public static void GZipEncodePage()
{
HttpResponse Response = HttpContext.Current.Response;
if(IsGZipSupported())
{
stringAcceptEncoding = HttpContext.Current.Request.Headers["Accept-Encoding"];
if(AcceptEncoding.Contains("deflate"))
{
Response.Filter = newSystem.IO.Compression.DeflateStream(Response.Filter,
System.IO.Compression.CompressionMode.Compress);
Response.AppendHeader("Content-Encoding", "deflate");
}
else
{
Response.Filter = newSystem.IO.Compression.GZipStream(Response.Filter,
System.IO.Compression.CompressionMode.Compress);
Response.AppendHeader("Content-Encoding", "gzip");
}
}
// Allow proxy servers to cache encoded and unencoded versions separately
Response.AppendHeader("Vary", "Content-Encoding");
}
/// <summary>
/// Determines if GZip is supported
/// </summary>
/// <returns></returns>
public static bool IsGZipSupported()
{
string AcceptEncoding = HttpContext.Current.Request.Headers["Accept-Encoding"];
if (!string.IsNullOrEmpty(AcceptEncoding) &&
(AcceptEncoding.Contains("gzip") || AcceptEncoding.Contains("deflate")))
return true;
return false;
}
GZipStream and DeflateStream are streams that are assigned to Response.Filter and by doing so apply the appropriate compression on the active Response.
Response.Filter content is chunked
So to implement a Response.Filter effectively requires only that you implement a custom stream and handle the Write() method to capture Response output as it’s written. At first blush this seems very simple – you capture the output in Write, transform it and write out the transformed content in one pass. And that indeed works for small amounts of content. But you see, the problem is that output is written in small buffer chunks (a little less than 16k it appears) rather than just a single Write() statement into the stream, which makes perfect sense for ASP.NET to stream data back to IIS in smaller chunks to minimize memory usage en route.
Unfortunately this also makes it a more difficult to implement any filtering routines since you don’t directly get access to all of the response content which is problematic especially if those filtering routines require you to look at the ENTIRE response in order to transform or capture the output as is needed for the solution the gentleman in my session asked for.
So in order to address this a slightly different approach is required that basically captures all the Write() buffers passed into a cached stream and then making the stream available only when it’s complete and ready to be flushed.
As I was thinking about the implementation I also started thinking about the few instances when I’ve used Response.Filter implementations. Each time I had to create a new Stream subclass and create my custom functionality but in the end each implementation did the same thing – capturing output and transforming it. I thought there should be an easier way to do this by creating a re-usable Stream class that can handle stream transformations that are common to Response.Filter implementations.
Creating a semi-generic Response Filter Stream Class
What I ended up with is a ResponseFilterStream class that provides a handful of Events that allow you to capture and/or transform Response content. The class implements a subclass of Stream and then overrides Write() and Flush() to handle capturing and transformation operations. By exposing events it’s easy to hook up capture or transformation operations via single focused methods.
ResponseFilterStream exposes the following events:
- CaptureStream, CaptureString
Captures the output only and provides either a MemoryStream or String with the final page output. Capture is hooked to the Flush() operation of the stream.
- TransformStream, TransformString
Allows you to transform the complete response output with events that receive a MemoryStream or String respectively and can you modify the output then return it back as a return value. The transformed output is then written back out in a single chunk to the response output stream. These events capture all output internally first then write the entire buffer into the response.
- TransformWrite, TransformWriteString
Allows you to transform the Response data as it is written in its original chunk size in the Stream’s Write() method. Unlike TransformStream/TransformString which operate on the complete output, these events only see the current chunk of data written. This is more efficient as there’s no caching involved, but can cause problems due to searched content splitting over multiple chunks.
Using this implementation, creating a custom Response.Filter transformation becomes as simple as the following code.
To hook up the Response.Filter using the MemoryStream version event:
ResponseFilterStream filter = new ResponseFilterStream(Response.Filter);
filter.TransformStream += filter_TransformStream;
Response.Filter = filter;
and the event handler to do the transformation:
MemoryStream filter_TransformStream(MemoryStream ms)
{
Encoding encoding = HttpContext.Current.Response.ContentEncoding;
string output = encoding.GetString(ms.ToArray());
output = FixPaths(output);
ms = new MemoryStream(output.Length);
byte[] buffer = encoding.GetBytes(output);
ms.Write(buffer,0,buffer.Length);
return ms;
}
private string FixPaths(string output)
{
string path = HttpContext.Current.Request.ApplicationPath;
// override root path wonkiness
if (path == "/")
path = "";
output = output.Replace("\"~/", "\"" + path + "/").Replace("'~/", "'" + path + "/");
return output;
}
The idea of the event handler is that you can do whatever you want to the stream and return back a stream – either the same one that’s been modified or a brand new one – which is then sent back to as the final response.
The above code can be simplified even more by using the string version events which handle the stream to string conversions for you:
ResponseFilterStream filter = new ResponseFilterStream(Response.Filter);
filter.TransformString += filter_TransformString;
Response.Filter = filter;
and the event handler to do the transformation calling the same FixPaths method shown above:
string filter_TransformString(string output)
{
return FixPaths(output);
}
The events for capturing output and capturing and transforming chunks work in a very similar way. By using events to handle the transformations ResponseFilterStream becomes a reusable component and we don’t have to create a new stream class or subclass an existing Stream based classed.
By the way, the example used here is kind of a cool trick which transforms “~/” expressions inside of the final generated HTML output – even in plain HTML controls not HTML controls – and transforms them into the appropriate application relative path in the same way that ResolveUrl would do.
So you can write plain old HTML like this:
<a href=”~/default.aspx”>Home</a>
and have it turned into:
<a href=”/myVirtual/default.aspx”>Home</a>
without having to use an ASP.NET control like Hyperlink or Image or having to constantly use:
<img src=”<%= ResolveUrl(“~/images/home.gif”) %>” />
in MVC applications (which frankly is one of the most annoying things about MVC especially given the path hell that extension-less and endpoint-less URLs impose).
I can’t take credit for this idea. While discussing the Response.Filter issues on Twitter a hint from Dylan Beattie who pointed me at one of his examples which does something similar. I thought the idea was cool enough to use an example for future demos of Response.Filter functionality in ASP.NET next I time I do the Modules and Handlers talk (which was great fun BTW).
How practical this is is debatable however since there’s definitely some overhead to using a Response.Filter in general and especially on one that caches the output and the re-writes it later. Make sure to test for performance anytime you use Response.Filter hookup and make sure it' doesn’t end up killing perf on you. You’ve been warned :-}.
How does ResponseFilterStream work?
The big win of this implementation IMHO is that it’s a reusable component – so for implementation there’s no new class, no subclassing – you simply attach to an event to implement an event handler method with a straight forward signature to retrieve the stream or string you’re interested in.
The implementation is based on a subclass of Stream as is required in order to handle the Response.Filter requirements. What’s different than other implementations I’ve seen in various places is that it supports capturing output as a whole to allow retrieving the full response output for capture or modification. The exception are the TransformWrite and TransformWrite events which operate only active chunk of data written by the Response.
For captured output, the Write() method captures output into an internal MemoryStream that is cached until writing is complete. So Write() is called when ASP.NET writes to the Response stream, but the filter doesn’t pass on the Write immediately to the filter’s internal stream. The data is cached and only when the Flush() method is called to finalize the Stream’s output do we actually send the cached stream off for transformation (if the events are hooked up) and THEN finally write out the returned content in one big chunk.
Here’s the implementation of ResponseFilterStream:
/// <summary>
/// A semi-generic Stream implementation for Response.Filter with
/// an event interface for handling Content transformations via
/// Stream or String.
/// <remarks>
/// Use with care for large output as this implementation copies
/// the output into a memory stream and so increases memory usage.
/// </remarks>
/// </summary>
public class ResponseFilterStream : Stream
{
/// <summary>
/// The original stream
/// </summary>
Stream _stream;
/// <summary>
/// Current position in the original stream
/// </summary>
long _position;
/// <summary>
/// Stream that original content is read into
/// and then passed to TransformStream function
/// </summary>
MemoryStream _cacheStream = new MemoryStream(5000);
/// <summary>
/// Internal pointer that that keeps track of the size
/// of the cacheStream
/// </summary>
int _cachePointer = 0;
/// <summary>
///
/// </summary>
/// <param name="responseStream"></param>
public ResponseFilterStream(Stream responseStream)
{
_stream = responseStream;
}
/// <summary>
/// Determines whether the stream is captured
/// </summary>
private bool IsCaptured
{
get
{
if (CaptureStream != null || CaptureString != null ||
TransformStream != null || TransformString != null)
return true;
return false;
}
}
/// <summary>
/// Determines whether the Write method is outputting data immediately
/// or delaying output until Flush() is fired.
/// </summary>
private bool IsOutputDelayed
{
get
{
if (TransformStream != null || TransformString != null)
return true;
return false;
}
}
/// <summary>
/// Event that captures Response output and makes it available
/// as a MemoryStream instance. Output is captured but won't
/// affect Response output.
/// </summary>
public event Action<MemoryStream> CaptureStream;
/// <summary>
/// Event that captures Response output and makes it available
/// as a string. Output is captured but won't affect Response output.
/// </summary>
public event Action<string> CaptureString;
/// <summary>
/// Event that allows you transform the stream as each chunk of
/// the output is written in the Write() operation of the stream.
/// This means that that it's possible/likely that the input
/// buffer will not contain the full response output but only
/// one of potentially many chunks.
///
/// This event is called as part of the filter stream's Write()
/// operation.
/// </summary>
public event Func<byte[], byte[]> TransformWrite;
/// <summary>
/// Event that allows you to transform the response stream as
/// each chunk of bytep[] output is written during the stream's write
/// operation. This means it's possibly/likely that the string
/// passed to the handler only contains a portion of the full
/// output. Typical buffer chunks are around 16k a piece.
///
/// This event is called as part of the stream's Write operation.
/// </summary>
public event Func<string, string> TransformWriteString;
/// <summary>
/// This event allows capturing and transformation of the entire
/// output stream by caching all write operations and delaying final
/// response output until Flush() is called on the stream.
/// </summary>
public event Func<MemoryStream, MemoryStream> TransformStream;
/// <summary>
/// Event that can be hooked up to handle Response.Filter
/// Transformation. Passed a string that you can modify and
/// return back as a return value. The modified content
/// will become the final output.
/// </summary>
public event Func<string, string> TransformString;
protected virtual void OnCaptureStream(MemoryStream ms)
{
if (CaptureStream != null)
CaptureStream(ms);
}
private void OnCaptureStringInternal(MemoryStream ms)
{
if (CaptureString != null)
{
string content = HttpContext.Current.Response.ContentEncoding.GetString(ms.ToArray());
OnCaptureString(content);
}
}
protected virtual void OnCaptureString(string output)
{
if (CaptureString != null)
CaptureString(output);
}
protected virtual byte[] OnTransformWrite(byte[] buffer)
{
if (TransformWrite != null)
return TransformWrite(buffer);
return buffer;
}
private byte[] OnTransformWriteStringInternal(byte[] buffer)
{
Encoding encoding = HttpContext.Current.Response.ContentEncoding;
string output = OnTransformWriteString(encoding.GetString(buffer));
return encoding.GetBytes(output);
}
private string OnTransformWriteString(string value)
{
if (TransformWriteString != null)
return TransformWriteString(value);
return value;
}
protected virtual MemoryStream OnTransformCompleteStream(MemoryStream ms)
{
if (TransformStream != null)
return TransformStream(ms);
return ms;
}
/// <summary>
/// Allows transforming of strings
///
/// Note this handler is internal and not meant to be overridden
/// as the TransformString Event has to be hooked up in order
/// for this handler to even fire to avoid the overhead of string
/// conversion on every pass through.
/// </summary>
/// <param name="responseText"></param>
/// <returns></returns>
private string OnTransformCompleteString(string responseText)
{
if (TransformString != null)
TransformString(responseText);
return responseText;
}
/// <summary>
/// Wrapper method form OnTransformString that handles
/// stream to string and vice versa conversions
/// </summary>
/// <param name="ms"></param>
/// <returns></returns>
internal MemoryStream OnTransformCompleteStringInternal(MemoryStream ms)
{
if (TransformString == null)
return ms;
//string content = ms.GetAsString();
string content = HttpContext.Current.Response.ContentEncoding.GetString(ms.ToArray());
content = TransformString(content);
byte[] buffer = HttpContext.Current.Response.ContentEncoding.GetBytes(content);
ms = new MemoryStream();
ms.Write(buffer, 0, buffer.Length);
//ms.WriteString(content);
return ms;
}
/// <summary>
///
/// </summary>
public override bool CanRead
{
get { return true; }
}
public override bool CanSeek
{
get { return true; }
}
/// <summary>
///
/// </summary>
public override bool CanWrite
{
get { return true; }
}
/// <summary>
///
/// </summary>
public override long Length
{
get { return 0; }
}
/// <summary>
///
/// </summary>
public override long Position
{
get { return _position; }
set { _position = value; }
}
/// <summary>
///
/// </summary>
/// <param name="offset"></param>
/// <param name="direction"></param>
/// <returns></returns>
public override long Seek(long offset, System.IO.SeekOrigin direction)
{
return _stream.Seek(offset, direction);
}
/// <summary>
///
/// </summary>
/// <param name="length"></param>
public override void SetLength(long length)
{
_stream.SetLength(length);
}
/// <summary>
///
/// </summary>
public override void Close()
{
_stream.Close();
}
/// <summary>
/// Override flush by writing out the cached stream data
/// </summary>
public override void Flush()
{
if (IsCaptured && _cacheStream.Length > 0)
{
// Check for transform implementations
_cacheStream = OnTransformCompleteStream(_cacheStream);
_cacheStream = OnTransformCompleteStringInternal(_cacheStream);
OnCaptureStream(_cacheStream);
OnCaptureStringInternal(_cacheStream);
// write the stream back out if output was delayed
if (IsOutputDelayed)
_stream.Write(_cacheStream.ToArray(), 0, (int)_cacheStream.Length);
// Clear the cache once we've written it out
_cacheStream.SetLength(0);
}
// default flush behavior
_stream.Flush();
}
/// <summary>
///
/// </summary>
/// <param name="buffer"></param>
/// <param name="offset"></param>
/// <param name="count"></param>
/// <returns></returns>
public override int Read(byte[] buffer, int offset, int count)
{
return _stream.Read(buffer, offset, count);
}
/// <summary>
/// Overriden to capture output written by ASP.NET and captured
/// into a cached stream that is written out later when Flush()
/// is called.
/// </summary>
/// <param name="buffer"></param>
/// <param name="offset"></param>
/// <param name="count"></param>
public override void Write(byte[] buffer, int offset, int count)
{
if ( IsCaptured )
{
// copy to holding buffer only - we'll write out later
_cacheStream.Write(buffer, 0, count);
_cachePointer += count;
}
// just transform this buffer
if (TransformWrite != null)
buffer = OnTransformWrite(buffer);
if (TransformWriteString != null)
buffer = OnTransformWriteStringInternal(buffer);
if (!IsOutputDelayed)
_stream.Write(buffer, offset, buffer.Length);
}
}
The key features are the events and corresponding OnXXX methods that handle the event hookups, and the Write() and Flush() methods of the stream implementation. All the rest of the members tend to be plain jane passthrough stream implementation code without much consequence.
I do love the way Action<t> and Func<T> make it so easy to create the event signatures for the various events – sweet.
A few Things to consider
Performance
Response.Filter is not great for performance in general as it adds another layer of indirection to the ASP.NET output pipeline, and this implementation in particular adds a memory hit as it basically duplicates the response output into the cached memory stream which is necessary since you may have to look at the entire response. If you have large pages in particular this can cause potentially serious memory pressure in your server application. So be careful of wholesale adoption of this (or other) Response.Filters. Make sure to do some performance testing to ensure it’s not killing your app’s performance.
Response.Filter works everywhere
A few questions came up in comments and discussion as to capturing ALL output hitting the site and – yes you can definitely do that by assigning a Response.Filter inside of a module. If you do this however you’ll want to be very careful and decide which content you actually want to capture especially in IIS 7 which passes ALL content – including static images/CSS etc. through the ASP.NET pipeline. So it is important to filter only on what you’re looking for – like the page extension or maybe more effectively the Response.ContentType.
Response.Filter Chaining
Originally I thought that filter chaining doesn’t work at all due to a bug in the stream implementation code. But it’s quite possible to assign multiple filters to the Response.Filter property. So the following actually works to both compress the output and apply the transformed content:
WebUtils.GZipEncodePage();
ResponseFilterStream filter = new ResponseFilterStream(Response.Filter);
filter.TransformString += filter_TransformString;
Response.Filter = filter;
However the following does not work resulting in invalid content encoding errors:
ResponseFilterStream filter = new ResponseFilterStream(Response.Filter);
filter.TransformString += filter_TransformString;
Response.Filter = filter;
WebUtils.GZipEncodePage();
In other words multiple Response filters can work together but it depends entirely on the implementation whether they can be chained or in which order they can be chained. In this case running the GZip/Deflate stream filters apparently relies on the original content length of the output and chokes when the content is modified. But if attaching the compression first it works fine as unintuitive as that may seem.
Resources
November 09, 2009 @ 12:20 am
- from Houston, Texas
Here’s a a small problem that one of customers ran into a few days ago: He was playing around with some of the sample code I’ve put out for one of my simple jQuery demos which deals with providing a simple pulse behavior plug-in:
$.fn.pulse = function(time) {
if (!time)
time = 2000;
// *** this == jQuery object that contains selections
$(this).fadeTo(time, 0.20,
function() {
$(this).fadeTo(time, 1);
});
return this;
}
it’s a very simplistic plug-in and it works fine for simple pulse animations. However he ran into a problem where it didn’t work when working with tables – specifically pulsing a table row in Internet Explorer. Works fine in FireFox and Chrome, but IE not so much. It also works just fine in IE as long as you don’t try it on tables or table rows specifically. Applying against something like this (an ASP.NET GridView):
var sel =
$("#gdEntries>tbody>tr")
.not(":first-child") // no header
.not(":last-child") // no footer
.filter(":even")
.addClass("gridalternate");
// *** Demonstrate simple plugin
sel.pulse(2000);
fails in IE. No pulsing happens in any version of IE. After some additional experimentation with single rows and various ways of selecting each and still failing, I’ve come to the conclusion that the various fade operations in jQuery simply won’t work correctly in IE (any version). So even something as ‘elemental’ as this:
var el = $("#gdEntries>tbody>tr").get(0);
$(el).fadeOut(2000);
is not working correctly. The item will stick around for 2 seconds and then magically disappear.
Likewise:
sel.hide().fadeIn(5000);
also doesn’t fade in although the items become immediately visible in IE. Go figure that behavior out.
Thanks to a tweet from red_square and a link he provided here is a grid that explains what works and doesn’t in IE (and most last gen browsers) regarding opacity:
http://www.quirksmode.org/js/opacity.html
It appears from this link that table and row elements can’t be made opaque, but td elements can. This means for the row selections I can force each of the td elements to be selected and then pulse all of those. Once you have the rows it’s easy to explicitly select all the columns in those rows with .find(“td”). Aha the following actually works:
var sel =
$("#gdEntries>tbody>tr")
.not(":first-child") // no header
.not(":last-child") // no footer
.filter(":even")
.addClass("gridalternate");
// *** Demonstrate simple plugin
sel.find("td").pulse(2000);
A little unintuitive that, but it works.
Stay away from <table> and <tr> Fades
The moral of the story is – stay away from TR, TH and TABLE fades and opacity. If you have to do it on tables use the columns instead and if necessary use .find(“td”) on your row(s) selector to grab all the columns.
I’ve been surprised by this uhm relevation, since I use fadeOut in almost every one of my applications for deletion of items and row deletions from grids are not uncommon especially in older apps. But it turns out that fadeOut actually works in terms of behavior: It removes the item when the timeout’s done and because the fade is relatively short lived and I don’t extensively test IE code any more I just never noticed that the fade wasn’t happening.
Note – this behavior or rather lack thereof appears to be specific to table table,tr,th elements. I see no problems with other elements like <div> and <li> items.
Chalk this one up to another of IE’s shortcomings.
Incidentally I’m not the only one who has failed to address this in my simplistic plug-in: The jquery-ui pulsate effect also fails on the table rows in the same way.
sel.effect("pulsate", { times: 3 }, 2000);
and it also works with the same workaround. If you’re already using jquery-ui definitely use this version of the plugin which provides a few more options…
Bottom line: be careful with table based fade operations and remember that if you do need to fade – fade on columns.
November 07, 2009 @ 3:58 pm
- from Houston, Texas
One of the more anticipated features of ASP.NET 4.0 – at least for me - is the new ClientIDMode property, which can be used to force controls to generate clean Client IDs that don’t follow ASP.NET’s munged NamingContainer ID conventions. To be clear, NamingContainer IDs are necessary for complex controls and pages that nest many things inside of a single context, but for most application level Web design scenarios those munged IDs get in the way and don’t provide a lot of value.
The munged IDs affect all sorts of development scenarios from design and CSS styling where ID haven’t been predictable for #id styling:
#ctl00_content_txtName
{
font-weight: bold;
}
vs the more expected:
#txtName
{
font-weight: bold;
}
And even more so there’s the whole nightmare of ClientIds in script pages where code like this:
<script type="text/javascript">
var txtName = $("<%= txtName.ClientID %>");
var txtTitle = $("[id$=_txtTitle]");
</script>
or some other pre-generation code is necessary to get client id’s properly referenced in script code.
ClientIDMode
In prior versions of ASP.NET you had to live with the problem or work around with various hacks. In ASP.NET 4.0 things get a little easier via the new ClientIDMode property which allows you to specify exactly how a ClientID is generated.
The idea is simple: you get a new ClientIDMode property on all ASP.NET controls which can be set either at the actual control or somewhere up the NamingContainer chain. If the ClientIDMode property is not set on a control it inherits the setting from the nearest NamingContainer with the exception of the <asp:Content> placeholder which doesn’t participate in ClientIDMode processing (bummer!).
This means if you choose to you can set ClientIDMode on the Page level and it will trickle down into all the child controls on the page, but a custom naming container like a User Control can still override the ClientIDMode to enforce it’s naming container requirements. It’ll be interesting to see how much existing code might break based on this inheritance scheme as custom controls may lose controls over their ClientID naming if the ClientIDMode property isn’t set explicitly.
Anyway let’s take a look at a very simple example and how the ClientIDMode property affects the ID rendering. Imagine you have a page that uses a master page and is set up like this:
<%@PageTitle=""Language="C#"MasterPageFile="~/Site.Master"AutoEventWireup="true"
CodeBehind="WebForm2.aspx.cs"
Inherits="WebApplication1.WebForm2"
ClientIDMode="Predictable"
%>
<asp:ContentID="content"ContentPlaceHolderID="content"runat="server"ClientIDMode="Static" >
<asp:TextBox runat="server"ID="txtName"ClientIDMode="Static" />
</asp:Content>
Here’s what the various values for the txtName property can look like:
AutoID
This is the existing behavior in ASP.NET 1.x-3.x where full naming container munging takes place.
<input name="ctl00$content$txtName" type="text" id="ctl00_content_txtName" />
In Beta 2 this is the default value for the Page. According to the latest VS 2010 documentation however, the default behavior by release time will be Predictable.
Static
This option forces the control’s ClientID to use its ID value directly. No naming container naming at all is applied and you end up with clean client ids:
<inputname="ctl00$content$txtName"type="text"id="txtName" />
This option is what most of us want to use, but you have to be clear on that this can potentially cause conflicts with other control on the page. If there are several instances of the same naming container (several instances of the same user control for example) there can easily be a client ID naming conflict. It’s basically up to you to decide whether this is a problem or not.
Note that if you assign Static to a Databound control like a list child controls in templates do not get unique IDs either, so for list controls where you rely on unique Id for child controls you’ll probably want to use Predictable rather than Static. More on this a little later when I discuss ClientIDRowSuffix.
Predictable
The previous two values are pretty self-explanatory. Predictable however, requires some explanation. To me at least it's not in the least bit predictable :-}.
MSDN defines this enum value as follows:
This algorithm is used for controls that are in data-bound controls. The ClientID value is generated by concatenating the ClientID value of the parent naming container with the ID value of the control. If the control is a data-bound control that generates multiple rows, the value of the data field specified in the ClientIDRowSuffix property is added at the end. For the GridView control, multiple data fields can be specified. If the ClientIDRowSuffix property is blank, a sequential number is added at the end instead of a data-field value. Each segment is separated by an underscore character (_).
The key that makes this value a bit confusing is that it relies on the parent NamingContainer’s ClientID to build it’s own client ID value. Which effectively means that the value is not predictable at all but rather very tightly coupled to the parent naming container’s ClientIDMode setting.
For our simple textbox example, if the ClientIDMode property of the parent naming container (Page in this case) is set to “Predictable” you’ll get this:
<input name="ctl00$content$txtName" type="text" id="content_txtName" />
which gives a name that walk up to the currently active naming container (the MasterPage content container) and starts the name from there downward. Think of this as a semi unique name that’s guaranteed unique only for the naming container.
If on the other hand the Page is set to “AutoID” you get with Predicable on txtName:
<input name="ctl00$content$txtName" type="text" id="ctl00_content_txtName" />
The latter is effectively the same as if you specified AutoID in this scenario because it inherits the AutoID naming from the Page and Content control of the page. But again – predictable behavior always depends on the parent naming container and how it generates its id so the ID may not always be exactly the same as the AutoID generated value because somewhere in the NamingContainer chain the ClientIDMode setting may be set to a different value. For example if you had another naming container in the middle that was set to Static you’d end up effectively with an ID that starts with the NamingContainers' ID rather than the whole ctl000_content munging.
The most common use however for Predictable will be for DataBound controls, which results in a each data bound item to get a unique ClientID.
Predictable is useful, but only if all naming containers down the chain use this setting. Otherwise you’re right back to the munged Ids that are pretty unpredictable.
Inherit
The final setting is Inherit which is the default for all controls except Page (AFAIK). This means that controls by default inherit the naming container’s ClientIDMode setting.
Inheritance
The explicit values are pretty obvious in what they do and when they are applied to individual controls. AutoID is classic behavior, Static is what we want in typical client centric apps, and Predictable is what you typically will want to use for list controls and anything that has possible naming conflicts.
Things get a little more tricky with inheritance of these settings. Specicfically the ClientIDMode property inherits from a NamingContainer down. Unlike most other ASP.NET hierarchy inheritance the ClientIDMode inheritance is based on the NamingContainer not on the Parent Container.
What this means is that if you set ClientIDMode="Static" on a Page or MasterPage all controls inherit that settings:
<%@ Page Title="" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true"
CodeBehind="WebForm2.aspx.cs" Inherits="WebApplication1.WebForm2"
ClientIDMode="Static" %>
If you don’t set the ClientIDMode on any other controls the entire page will use Static. Any UserControls can override the setting but all controls will use this setting.
Now imagine I do:
<%@ Page Title="" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true"
CodeBehind="WebForm2.aspx.cs" Inherits="WebApplication1.WebForm2"
ClientIDMode="Predictable" %>
and I’m running inside of a master page and I put in a block of controls like this:
<asp:Panel runat="server" ClientIDMode="Static">
This is a test:
<asp:TextBox runat="server" ID="txtName" />
<asp:Button ID="btnSubmit" runat="server" Text="Go" />
</asp:Panel>
Quick what should you see? Unfortunately not what I would have expected – although it’s true to what the documentation advertises. The block above renders into:
<div>
This is a test:
<input name="ctl00$content$txtName" type="text" id="content_txtName" />
<input type="submit" name="ctl00$content$btnSubmit" value="Go" id="content_btnSubmit" />
</div>
What’s happening here is that even though we specified Static naming on the Panel (which is not a NamingContainer) Predictable naming is used which runs up to the nearest naming container – in this case the Master Page Content control and using that as its base for the name. If I want those controls to render with clean ids I need to explicitly mark the controls to use Static:
<asp:Panel runat="server" ClientIDMode="Static">
This is a test:
<asp:TextBox runat="server" ID="txtName" ClientIDMode="Static" />
<asp:Button ID="btnSubmit" runat="server" Text="Go" ClientIDMode="Static" />
</asp:Panel>
or by changing the Page’s ClientIDMode to Static:
<%@ Page Title="" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true"
CodeBehind="WebForm2.aspx.cs" Inherits="WebApplication1.WebForm2"
ClientIDMode="Static" %>
With either of those in place I now get:
<div id="Panel1">
This is a test:
<input name="ctl00$content$txtName" type="text" id="txtName" />
<input type="submit" name="ctl00$content$btnSubmit" value="Go" id="btnSubmit" />
</div>
Notice that when page level ClientIDMode="Static" the <div> tag now renders with an explicit ID which is rather inconsistent (it’s not there if I just have ClientIDMode=”Static” on the Panel alone).
Note that you unfortunately cannot use an <asp:Content> control and specify the ClientIDMode like this:
<asp:Content ID="content" ContentPlaceHolderID="content" runat="server" ClientIDMode="Static">
This has no effect on the controls contained inside of the Content container which still inherit the ClientIDMode from Page in this case. This is really annoying because the Content container certainly is part of the naming container hierarchy that is reflected in the ClientIDName for Predictable and AutoId.
There’s a way around this by using:
- Static on the List Control
- Predictable on all of the Child controls
This looks like this:
<asp:GridView runat="server" ID="gvProducts" AutoGenerateColumns="false"
ClientIDMode="Static" ClientIDRowSuffix="id"
DataSourceID="xmlDataSource"
>
<Columns>
<asp:TemplateField>
<ItemTemplate>
<asp:Label runat="server" id="txtTitle" Text='<%# Eval("title") %>'
ClientIDMode="Predictable"/>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField>
<ItemTemplate>
<asp:Label runat="server" id="txtId" Text='<%# Eval("id") %>'
ClientIDMode="Predictable" />
</ItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>
which results in:
<table cellspacing="0" rules="all" border="1" id="Table2" style="border-collapse:collapse;">
<tr>
<th scope="col"> </th><th scope="col"> </th>
</tr><tr>
<td>
<span id="txtTitle_32">West Wind West Wind Web Toolkit</span>
</td><td>
<span id="txtId_32">32</span>
</td>
</tr>
</table>
This does produce what I would consider a desirable result although I had hoped that using Static on the list control without any further formatting would have produced this result. Unfortunately the Predictable setting on each of the child controls is required to get the clean ids into the child controls.
ClientIDRowSuffix
Another feature of the ClientID improvements in ASP.NET 4.0 is the ClientIDRowSuffix which can be applied to DataBound/List controls. I used it above in the grid to have the enumerated client id of the child controls use the value of an Id field from the database as the enumeration value. This setting basically determines how ID values for template controls in databound controls are generated, but it requires that the ClientIDMode is set to Predictable. It produces:
id="txtTitle_32"
where the _32 in this case comes from the Id of the data source which is nice than only using sequentially numbered values in previous versions of ASP.NET which were often worthless in client situations. Using an actual data value that can be looked retrieved on the client and sent back on an Ajax callback makes these IDs much more useful.
Wouldn’t it be nice if Client Row Ids could be generated?
What would be even nicer is that the generated ‘rows’ of a data bound control could optionally generate ids. In most Ajax situations the row level ID is really what’s useful – selections of rows for deletion, editing and updating always require an ID even if there are no child controls, but ASP.NET doesn’t provide an easy mechanism for embedding row ids. It sure would be get output like this:
<tr id="content_gvProducts_33">
...
</tr>
This can be done with code in a GridView (and other list controls) with ItemCreated events it’s quite a pain to do this. For example for the gridview I’m using with a simple XmlDataSource control I have to do this:
protected void gvProducts_RowCreated(object sender, GridViewRowEventArgs e)
{
object dataItem = e.Row.DataItem;
if (dataItem != null)
{
XPathNavigator nav = ((IXPathNavigable)dataItem).CreateNavigator();
if (nav != null)
e.Row.Attributes.Add("id", this.gvProducts.ID + "_" + nav.GetAttribute("id",""));
}
}
to produce output like this:
<tr id="gvProducts_33"> ... </tr>
which is anything but intuitive for such a common scenario (although this IS a bit easier to grab the data if you use Entity list or DataTable binding).
Data List Controls and Static Produces Invalid HTML
One more note: Using Static on list controls with child controls and NOT using Predictable for child controls is problematic as it will generate static IDs for ALL template items:
<table cellspacing="0" rules="all" border="1" id="Table1" style="border-collapse:collapse;">
<tr>
<th scope="col"> </th><th scope="col"> </th>
</tr><tr>
<td>
<span id="txtTitle">West Wind West Wind Web Toolkit</span>
</td><td>
<span id="txtId">32</span>
</td>
</tr><tr>
<td>
<span id="txtTitle">West Wind West Wind Web Store</span>
</td><td>
<span id="txtId">33</span>
</td>
</tr>
</table>
This is invalid HTML since there are multiple controls with the same id attribute on the page which is clearly undesirable. To fix this remember to use Predictable on any of the child controls.
Or better yet stay away from server controls altogether in template columns – stick to plain HTML controls and use AJAX to update values to the server more interactively. :-}
How should we use ClientIDMode?
I suspect it’s going to take some time to figure out all the little nuances of the new ClientIDMode features. At the very least the Static option on individual controls allows you to explicitly force controls to use the name you want it to and that’s a win any way you look at it.
For now I think the following is what I want to use in typical page scenarios in my applications:
- Add ClientIDMode="Static" to each Page (or in web.config’s <pages> setting)
- Add ClientIDMode="Predictable" explicitly to each List Control Children in Databound Template
- Override explicitly to Predictable where naming conflicts are a problem and to AutoId for the extreme edge case
- For Control Development leave at default behavior if possible (ie. Inherit from parent)
- Override only when necessary and preferrably on individual subcontrols
Also for now I think it’s a good idea to EXPLICITLY specify a ClientIDMode on each page (or in your project) or explicitly declare the value in your web.config file:
<pages clientIDMode="Static" />
to ensure you get a predictable setting since the current Beta 2 implementation and the documentation are at odds of what the default value actually is.
It’s funny to think that such simple functionality should cause such complex workarounds and dependent behaviors but I suspect with a consistent regimen of CLientIDMode settings you can achieve output that works for any scenario. Time will tell.