When building generic ASP.NET Server controls that also provide a
sort of API service to other custom controls or page level code, it's
often necessary to ensure that only a single instance of a control
exists, and that only that single instance of this control or component
can be accessed in the context of an ASP.NET request. While this is
usually easy to accomplish in actual page code (since you can reference
the control or component explicitly), if the API is also consumed by
other Server Controls it's crucial that it's easy and efficient to find
and access that single instance of a control effectively. This isn't a
typical scenario, but it's a common pattern for controls that expose
API level functionality in addition to the actual control based markup.
This isn't a new concept (Simone had a great post
on this some time ago), but I've run into this pattern so many times
and have also gotten a number of questions about this that I thought
I'd revisit it here since I just went through the excercise again.
For example, a few days ago I talked about a ScriptContainer control
that's akin to the ASP.NET ScriptManager control, but with a few
additional features. The idea is that there's a single level API that
can be used to retrieve an instance to the ScriptContainer, so that
code either in CodeBehind or a customer Server Control has access to
that instance sitting on a page.
So, in this example I can reference the wwScriptControl on the page with code like this:
wwScriptContainer script = wwScriptContainer.Current;
script.AddScript("~/scripts/wwEditable.js", true);
and always be guaranteed that I get that single instance of the
control whether it's already embedded in the page markup or if not it's
created on the fly and returned to me. Incidentally the ASP.NET
ScriptManager also does something similar with its GetCurrent() method,
but it only returns an instance if there's already one loaded - it's
your responsibility to create one and add it to the page otherwise. The
wwScriptContainer.Current property here instead always returns a
control instance including creating one and adding it to the page when
none exists yet.
Another example, where this applies is my ClientScriptProxy API,
which returns a single instance of the ClientScriptProxy object that
references either a ScriptManager if one is on the page, or otherwise
talks to the Page.ClientScript object and so provides the same behavior
regardless of which API is available. In this case too I need to ensure
that only a single instance of this object exists and that I can always
retrieve that single instance from anywhere in code. The
ClientScript.Current property serves exactly this purpose.
ASP.NET Singletons and HttpContext.Current
This Singleton pattern
can be applied to any ASP.NET control (or even component or Page) that
in addition to a markup based control interface also exposes a generic
API that scoped to the current ASP.NET request/page executing. In
ASP.NET request level state is managed through the HttpContext object
which is yet another example of a Singleton.
The basic concept of a Singleton is very straight forward. Here's Simone's example which is as concise as it gets:
public class SingletonPerRequest
{
public static SingletonPerRequest Current
{
get
{
return (HttpContext.Current.Items["SingletonPerRequest"] ??
(HttpContext.Current.Items["SingletonPerRequest"] =
new SingletonPerRequest())) as SingletonPerRequest;
}
}
}
where the SingletonPerRequest() creates whatever object reference
you need to create. The key thing is that the property is static and
that it returns a value that is stored on the HttpContext.Items
collection.
To understand how this works it's important to understand how the
HttpContext object relates to the ASP.NET request flow.
HttpContext.Current represents the current ASP.NET Request context and
this context is available throughout the course of an ASP.NET request
starting with the HttpApplication.BeginRequest pipeline event all the
way through HttpApplication.EndRequest(). It also includes module and
handler execution which is also part of the application pipeline.
ne important aspect of the HttpContext object is that it also
contains an Items collection - an in-process statebag that allows you
to attach arbitrary state at any point in processing of a request and
access it later. Because HttpContext.Current is static and always
accessible any state stored in Items is always available (unless
explicitly released) until the end of the request. It's an ideal store
for request specific information like for example store an instance
reference to a control that should only be accessed through a single
reference. IOW, a Singleton.
Long story short for my ScriptContainer example, I can use
HttpContext.Current.Items["wwScriptContainer"] to save and instance of
the script container control and then implement a custom method or
property like .Current to retrieve that single instance.
Here's what this code looks like for ScriptContainer:
public ScriptContainer()
{
this._Scripts = new List<HtmlGenericControl>();
this._InternalScripts = new List<ScriptItem>();
if(HttpContext.Current != null)
{
// *** Save a Per Request instance in Context.Items so we can retrieve it
// generically from code with wwScriptContainer.Current
if (HttpContext.Current.Items.Contains("ScriptContainer"))
throw new InvalidOperationException("Only one wwScriptContainer is allowed per page.");
HttpContext.Current.Items["ScriptContainer"] = this;
}
}
public override void Dispose()
{
if (HttpContext.Current != null)
HttpContext.Current.Items.Remove("ScriptContainer");
}
/// <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
/// wwScriptContainer 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;
ScriptContainer ctl = null;
if (HttpContext.Current != null)
{
// *** Retrieve the current instance
ctl = HttpContext.Current.Items["ScriptContainer"] as ScriptContainer;
if (ctl != null)
return ctl;
}
ctl = new ScriptContainer();
Page page = HttpContext.Current.Handler as Page;
if (page == null)
throw new InvalidOperationException("ScriptContainer.Current only works with Page based handlers.");
page.Form.Controls.Add(ctl);
return ctl;
}
}
The idea is that during initialization of the control the
control/component is immediately pushed into a Context.Items item and
saved. This instance becomes that single instance accessed. In fact,
the constructor explicitly checks for multiple instances and throws if
more than one is instantiated explicitly. This means you couldn't have
multiple markup controls on the page or a markup control and manually
create an instance.
Once stored the Item stored reference - if one exists - can then be
retrieved in the .Current property getter which checks to see if the
item exists and returns it.
Typically the .Current getter is also responsible for creating a new
instance if one doesn't exist, but how that's accomplished or whether
this is supported can vary wildly especially with controls. Here the
code creates a generic instance and adds it to the ASP.NET form's
Controls collection. Note that from within a static property you won't
have access to the Page instance (ie. no this.Page) so you have to
access the Page through the HttpContext.Current.Handler and cast that
to a Page object.
But again how that works will depend on your specific scenario. The
issue is that if you have a control there may be a number of
configuration settings that must be set in order to create an instance
'generically'. In the case of the ScriptContainer a generic version is
acceptable, but with other controls configuration may be required. You
may also need to know whether the control was returned from an existing
instance or whether a new instance was created and so you might have an
additional property that returns this fact. Or you can return null from .Current and then explicitly let client code create the new instance and configure it.
Typically Singletons have to worry about potential multi-user
issues, but in ASP.NET request scenarios using HttpContext.Current this
isn't an issue since the instance stored on .Current is visible only to
the current request, which is of course running on a single thread. So
multi-threading and locking is not something you have to worry about.
This Singleton like approach is a pattern I use a lot for ASP.NET
Controls that I create - it's a very useful tool for caching expensive
construction and ensuring that you truly are only accessing a single
instance of a control or component. But you can also apply this same
sort of thing to Page level code or even request level code.
If you want to take a closer look at the ScriptContainer component you can find it as part of the West Wind Ajax Toolkit download.
source:http://www.west-wind.com