One of the niceties or curses of the Ajax Framework is that it allows development of Ajax Enabled pages in a pattern that is very familiar to most ASP.Net developers. The plus side is obviously that for many basic tasks, the learning curve is very light when it comes to Ajaxifying a web page. The downside includes things like diminished responsiveness, unintended data state persistence, and from the UI's perspective, if an UpdatePanel is involved, there's a minor detail about only being able to process one asynchronous request at a time.
That said, I'm going to dedicate the next 3 posts to embracing the Ajax.ASP.Net framework and it's work philosophy of operating just like, or as close to as possible the standard ASP.Net framework. The three topics I'm going to cover have a fair amount of code, so I'm going to spread them out across 3 posts for readability, but they all deal with a single topic; placing a PlaceHolder Control within an UpdatePanel control within a Panel that's being extended by the ModalPopupExtender. One potential and very valid approach to this problem would be to build a new control extender that wraps up all the functionality, but I'm looking more to compare the use of an UpdatePanel vs. WebMethods than I am to create a super-flexible extender.
The approach I'm going to cover mimics something more like the ScriptManager and UpdatePanel relationship. In this first post I'll be covering a custom control I've created called ControlLoaderManager that act's something like a ScriptManager. The second post will cover the ControlLoader which is more like the UpdatePanel, and the 3rd post will show an implementation of these two controls.
So let's get to the ControlLoaderManager. The ControlLoaderManager serves 3 essential functions, first and foremost as a broker between a ControlLoader and any control that needs to interact with it. Secondly it helps wraps up some of the details around state persistence, and as I'm always a little weary of ViewState, it takes advantage of trusty old clear-text hidden fields. Lastly it wraps up the work of a ControlLoader tracking its loaded control from post to post. The entire code listing is at the bottom but first here are some key areas.
protected override void OnInit(EventArgs e)
{
if (Page.Items[LOADERKEY] != null)
{
throw new ApplicationException("Only 1 ControlLoaderManager may be used on a Page.");
}
Page.Items[LOADERKEY] = this;
base.OnInit(e);
}
In the OnInit override we check to make sure only one ControlLoaderManager has been added to a page. The other constraint on the control is that it needs to be in added to the controls collection prior to any ControlLoader either declaratively or in code. We use the Page.Items collection to allow us to neatly scope us to a Page. This means no worries about fattening up the session state with unneeded objects.
public static ControlLoaderManager GetCurrent(Page page)
{
return page.Items[LOADERKEY] as ControlLoaderManager;
}
This will look very familiar to uses of the ScriptManager. Because we've scoped this control to the page, we can easily create static methods that first retrieve the Manager from the Page.Items collection, and then perform the required action.
public void RegisterControlLoader(ControlLoader loader)
{
if (_loaders.ContainsKey(loader.Key))
{
throw new ApplicationException("ControlLoaders must a have a unique Key per page");
}
_loaders[loader.Key] = loader;
loader.TargetControlKey = GetControlLoaderTarget(loader.Key);
}
All child ControlLoaders call this method to register themselves , and get the persisted ControlTarget if available.
public void SetControlLoaderTarget(string loaderKey, string target)
{
if (!_loaders.ContainsKey(loaderKey))
{
throw new ApplicationException(string.Format("Could not find ControlLoader with Key {0}.", loaderKey));
}
_loaders[loaderKey].TargetControlKey = target;
}
This is used to update or specify the control target for a ControlLoader.
public void SetManagedValue(string key, string value)
{
_managedValues[key] = value;
}
public string GetManagedValue(string key)
{
if (!_managedValues.ContainsKey(key))
{
_managedValues[key] = Page.Request[key + MANAGEDVALUEEXT];
}
return _managedValues[key];
}
These methods act as a data value broker. It's often the case the Control A will need to provide data to Control B, this allows for a semi-loose coupling. It's not the most intelligent/robust pattern as the last setter always wins, but it will suffice for the purposes of this project
Full Listing:
using System;
using System.Collections.Generic;
using System.Web.UI;
using System.Web.UI.WebControls;
namespace Controls
{
[ToolboxData("<{0}:ControlLoaderManager runat=server></{0}:ControlLoaderManager>")]
public class ControlLoaderManager : WebControl
{
public ControlLoaderManager()
{
_loaders = new Dictionary<string, ControlLoader>();
_managedValues = new Dictionary<string, string>();
}
#region static methods
public static ControlLoaderManager GetCurrent(Page page)
{
return page.Items[LOADERKEY] as ControlLoaderManager;
}
public static void SetManagedValue(Page page, string key, string value)
{
GetCurrent(page).SetManagedValue(key, value);
}
public static string GetManagedValue(Page page, string key)
{
return GetCurrent(page).GetManagedValue(key);
}
public static void RegisterControlLoader(Page page, ControlLoader loader)
{
GetCurrent(page).RegisterControlLoader(loader);
}
public static ControlLoader GetControlLoader(Page page,string loaderKey)
{
return GetCurrent(page).GetControlLoader(loaderKey);
}
public static void SetControlLoaderTarget(Page page, string loaderKey, string target)
{
GetCurrent(page).GetControlLoader(loaderKey).TargetControlKey = target;
}
#endregion static methods
#region public methods
public void SetManagedValue(string key, string value)
{
_managedValues[key] = value;
}
public string GetManagedValue(string key)
{
if (!_managedValues.ContainsKey(key))
{
_managedValues[key] = Page.Request[key + MANAGEDVALUEEXT];
}
return _managedValues[key];
}
public void RegisterControlLoader(ControlLoader loader)
{
if (_loaders.ContainsKey(loader.Key))
{
throw new ApplicationException("ControlLoaders must a have a unique Key per page");
}
_loaders[loader.Key] = loader;
loader.TargetControlKey = GetControlLoaderTarget(loader.Key);
}
public ControlLoader GetControlLoader(string loaderKey)
{
if (!_loaders.ContainsKey(loaderKey))
{
throw new ApplicationException(string.Format("Could not find ControlLoader with Key {0}.", loaderKey));
}
return _loaders[loaderKey];
}
public void SetControlLoaderTarget(string loaderKey, string target)
{
if (!_loaders.ContainsKey(loaderKey))
{
throw new ApplicationException(string.Format("Could not find ControlLoader with Key {0}.", loaderKey));
}
_loaders[loaderKey].TargetControlKey = target;
}
#endregion public methods
#region event handlers
protected override void OnInit(EventArgs e)
{
if (Page.Items[LOADERKEY] != null)
{
throw new ApplicationException("Only 1 ControlLoaderManager may be used on a Page.");
}
Page.Items[LOADERKEY] = this;
base.OnInit(e);
}
protected override void OnPreRender(EventArgs e)
{
persistState();
base.OnPreRender(e);
}
protected override void Render(HtmlTextWriter writer)
{
writer.WriteLine(string.Format("<!-- ControlLoaderManager:{0} -->", this.ID));
}
#endregion event handlers
#region state managers
protected void persistState()
{
foreach (string key in _managedValues.Keys)
{
ScriptManager.RegisterHiddenField(Page, key + MANAGEDVALUEEXT, _managedValues[key]);
}
foreach (ControlLoader loader in _loaders.Values)
{
ScriptManager.RegisterHiddenField(Page, loader.Key + LOADERTGTEXT, loader.TargetControlKey);
}
}
protected string GetControlLoaderTarget(string loaderKey)
{
return Page.Request[loaderKey + LOADERTGTEXT];
}
#endregion state managers
protected IDictionary<string, ControlLoader> _loaders;
protected IDictionary<string, string> _managedValues;
private static readonly string LOADERKEY = "ControlLoaderManager";
private static readonly string MANAGEDVALUEEXT = "Ctldr";
private static readonly string LOADERTGTEXT = "TgtCtl";
}
}
Print | posted on Sunday, October 07, 2007 8:49 PM