In this post I am loading ASP.NET user controls to running page using AJAX page methods.
The sample code makes easy to load UserControls from server to the calling page.
Also it loads the associated css stylesheet file url (if needed by the control) and adds new LINK object to the header of the page, so that page loads the stylesheet file (there is a method to unload LINK object from header back.
Also the sample loads any custom javascript code (if needed by the control), and executes it right after getting it from the server.
I think you got the idea, used with Windows Workflow Foundation this approach can do a lot of good things :). The next three pages:
Please review my previous post, where I am explaining how Page method can be called by method name using reflection. I used that approach in this sample.
Also look at the post, where I am explaining how we can control AjaxControlToolkit ModalPopup control from javascript code.
I use some more interesting tricks in the sample, - code generation project, to generate navigation class using code generation templates engine inside VS 2008. See more about this in my previous post.
I use in sample code some new features of C# 3.0, - collection initializers and lambda expressions.
There is a lot of code in the sample, but I will try to list only most important parts of it. You can review full source code by downloading it.
1) How UserControls are organized:
Note, none of them are loaded into main page's control collection, they are just rendered during page method requests
2) How client side button requests the UserControl:
<input
value="Load user control!"
type="button"
onclick="mainScreen.LoadServerControlHtml(
'Welcome',
{'pageID':1,'data':null},
'methodHandlers.BeginRecieve');"
/>
mainScreen.LoadServerControlHtml javascript function:
mainScreen.LoadServerControlHtml = function(_title, _obj, _callback) {
/// <summary>
/// Loads Server user control to the modal dialog
/// </summary>
/// <param name="_title">Title of modal popup</param>
/// <param name="_obj">
/// object that we pass to the server
/// </param>
mainScreen.ShowModal(
_title,
(mainScreen.activityImgObj)
?
("<center><img src='" + mainScreen.activityImgObj.src + "' /></center>")
:
""
);
mainScreen.ExecuteCommand(
'GetWizardPage',
_callback,
_obj
);
}
This function shows modal dialog with progress image inside it and calls another js function which calls server page method
3) Server-side page method uses reflection to call 'GetWizardPage' method in dedicated command class:
/// <summary>
/// returns rendered control's string representation.
/// object "data" should be passed from javascript method
/// as array of objects consisting of two objects,
/// first - pageID - integer identificator by which we will
/// lookup real control path; second object may be some data
/// that the control needs.
/// </summary>
public object GetWizardPage(object data)
{
bool errorLogged = false;
try
{
Dictionary<string, object> param =
(Dictionary<string, object>)data;
int pageID = (int)param["pageID"];
object customData = param["data"];
string controlPath =
m_NavigationData.Find(x => x.Key == pageID).Value;
if (!String.IsNullOrEmpty(controlPath))
{
if(
controlPath.ToLower()
.EndsWith(".htm")
||
controlPath.ToLower()
.EndsWith(".html")
||
controlPath.ToLower()
.EndsWith(".txt"))
{
string result = "";
using (
TextReader tr =
new StreamReader(
HttpContext.Current.Server.MapPath(controlPath)
)
)
{
result = tr.ReadToEnd();
}
return new ContentsResponse(result, string.Empty, string.Empty);
}
else
{
return TemplateViewManager.RenderView(controlPath, customData);
}
}
}
catch (Exception ex)
{
// Log error
errorLogged = true;
}
if (!errorLogged)
{
// Log custom error saying
// we did not find the page
}
return ContentsResponse.Empty;
}
m_NavigationData is static variable, we use collection initializer to assign it a value right in the place of declaration:
private static List<KeyValuePair<int, string>>
m_NavigationData = new List<KeyValuePair<int, string>>()
{
new KeyValuePair<int, string>(1,Pages.Controls.Welcome),
new KeyValuePair<int, string>(2,Pages.Controls.AcceptLicenceAgreement),
new KeyValuePair<int, string>(3,Pages.Controls.PleaseClickProceedButton),
new KeyValuePair<int, string>(4,Pages.Controls.ConfirmationMessage),
new KeyValuePair<int, string>(5,Pages.Controls.Installing),
new KeyValuePair<int, string>(6,Pages.Controls.ThankYouMessage)
};
ContentsResponse object looks like:
namespace Devarchive_net
{
public class ContentsResponse
{
public ContentsResponse(string _html, string _script, string _customStyle)
{
html = _html;
script = _script;
customStyle = _customStyle;
}
public static ContentsResponse Empty
{
get
{
return new ContentsResponse(string.Empty, string.Empty, string.Empty);
}
}
public string html = "";
public string script = "";
public string customStyle = "";
}
}
this class is returned to the browser.
As you see page method reads the requested file right from disk as text file and returns it - if the extension for the page is htm, html or txt.
If the extension is ascx, it renders the control using TemplateViewManager.RenderView static method :
using System.IO;
using System.Web;
namespace Devarchive_net
{
public class TemplateViewManager
{
public static ContentsResponse RenderView(string path)
{
return RenderView(path, null);
}
public static ContentsResponse RenderView(string path, object data)
{
TemplatePage pageHolder = new TemplatePage();
TemplateUserControl viewControl =
(TemplateUserControl)pageHolder.LoadControl(path);
if (viewControl == null)
return ContentsResponse.Empty;
if (data != null)
{
viewControl.Data = data;
}
pageHolder.Controls.Add(viewControl);
string result = "";
using (StringWriter output = new StringWriter())
{
HttpContext.Current.Server.Execute(pageHolder, output, false);
result = output.ToString();
}
return new ContentsResponse(
result,
viewControl.StartupScript,
viewControl.CustomStyleSheet
);
}
}
}
Here is the trick with stylesheets and custom script. All user controls that may be requested should derive from TemplateUserControl class. This class defines the members for custom script and custom stylesheet:
using System.Web.UI;
namespace Devarchive_net
{
public class TemplateUserControl : UserControl
{
private object m_Data = null;
public object Data
{
get { return m_Data; }
set { m_Data = value; }
}
private string m_StartupScript = string.Empty;
public string StartupScript
{
get { return m_StartupScript; }
set { m_StartupScript = value; }
}
private string m_CustomStyleSheet = string.Empty;
public string CustomStyleSheet
{
get { return m_CustomStyleSheet; }
set { m_CustomStyleSheet = value; }
}
}
}
Also there is a TemplatePage class, this class is derived from Page class. The instance of this class is created when rendering UserControl to get rendered HTML.
Here is it:
using System.Web.UI;
namespace Devarchive_net
{
public class TemplatePage : Page
{
public override void VerifyRenderingInServerForm(Control control) { }
}
}
Next...
4) Browser gets back a response from the server in callback function we specified earlier:
methodHandlers.BeginRecieve = function(_result) {
/// <summary>
/// method that shows result from
/// page method "GetWizardPage"
/// </summary>
var res = false;
if(_result.customStyle && _result.customStyle!="") {
mainScreen.LoadStyleSheet(_result.customStyle);
}
if(_result.html && _result.html!="") {
mainScreen.mainModalContentsDiv.innerHTML = _result.html;
res = true;
}
if(_result.script && _result.script!="") {
eval(_result.script);
}
if(!res) {
mainScreen.CancelModal();
} else {
mainScreen.mainModalExtender._layout();
setTimeout('mainScreen.mainModalExtender._layout()', 3000);
}
};
It loads CSS files, then contents, and finally executes custom script if any was returned from the server.
I think you will be also interested with mainScreen.LoadStyleSheet method, here is it:
mainScreen.LoadStyleSheet = function(_path) {
if(!this.styleSheets[_path]) {
var styleSheet;
styleSheet=document.createElement('link');
styleSheet.type="text/css";
styleSheet.rel='stylesheet';
styleSheet.href = _path;
document.getElementsByTagName("head")[0].appendChild(styleSheet);
this.styleSheets[_path] = styleSheet;
}
};
Also there is another method, that is called when modal dialog is closed :
mainScreen.CancelModal = function() {
/// <summary>
/// Hides modal dialog
/// </summary>
this.mainModalExtender.hide();
var _path;
for(_path in this.styleSheets) {
document.getElementsByTagName("head")[0].removeChild(this.styleSheets[_path]);
delete this.styleSheets[_path];
}
};
This method removes any links to custom stylesheet files to avoid design issues in future calls to server.
Lets return to the server code, and see what UserControls itself contain inside, and what the can do.
Lets see Wecome.ascx file:
<%@ Control Language="C#" AutoEventWireup="true" CodeFile="Welcome.ascx.cs" Inherits="Controls_Welcome" %>
<div class="div">
<table class="tblStyle">
<tr>
<td class="tdLeftUpper">
<img
alt="Setup" src="Controls/Style/LeftImg.jpg"
style="width: 162px; height: 311px" />
</td>
<td class="tdRightUpper">
<p class="largeText" style="white-space:normal">
Welcome to the Microsoft ASP.NET AJAX
Extentions Source Code Setup Wizard<br />
</p>
<br />
<br />
<p class="smallText" style="white-space:normal">
The Setup Wizard allows you to change
the way Microsoft ASP.NET AJAX Extensions
Source
<span
class="customStyleSheetClass">
Code
</span>
features are installed on
your computer or to remove it from your
computer. Click Next to continue or Cancel
to exit Setup Wizard.
</p>
<div
style="border:solid 1px black;"
class="smallText"
id="custSpan">
</div>
</td>
</tr>
<tr>
<td colspan="2" class="tdLower">
<input
type="button"
class="wizBtn wizBtnBack"
value="Back"
onclick=""
disabled="disabled"
/>
<input
type="button"
class="wizBtnR wizBtnNext"
value="Next"
onclick="
mainScreen.LoadServerControlHtml(
'License Agreement',
{'pageID':2,'data':null},
'methodHandlers.BeginRecieve');
"
/>
<input
type="button"
class="wizBtn wizBtnClose"
value="Cancel"
onclick="mainScreen.CancelModal();"
/>
</td>
</tr>
</table>
</div>
Note, we use all client side controls, but - you can use any server control you like ! You can use GridView etc.
Code behind for Welcome.ascx:
using System;
using Devarchive_net;
public partial class Controls_Welcome : TemplateUserControl
{
protected void Page_Load(object sender, EventArgs e)
{
this.StartupScript =
"$get('custSpan').innerHTML=" +
"'Note - you will read this message "+
"because we passed custom javascript code from "+
"c# code!" +
"The word Code is red because of custom stylesheet!'" +
";"
;
this.CustomStyleSheet =
String.Format(
"Controls/Style/Welcome.css?{0}"
,
Guid.NewGuid().ToString()
);
}
}
here I assign custom javascript that should be executed in browser after call. You can move this into separate file and read it from c# to make separation of different languages clearer.
Also I assign a stylesheet url to the CustomStyleSheet variable.
One more thing - I wrote the code in the way we can pass some custom value from browser when requesting the user control.
If you remember we requested control from javascript like this:
{'pageID':2,'data':true}
here the second parameter is custom data, in this case it is just a boolean value, but we can pass array, dictionary, and more.
In User control we can analyze this value, to return back some specific view. For example open PleaseClickProceedButton.ascx file. There you can find line:
<%=(Data!=null && (bool)Data)?"checked=\"checked\"":"" %>
You can see how we parse the data passed from client browser.
I use this architecture in my current project. It is just great ! speed is much higher comparing to the page with UpdatePanels, Page methods are much faster, they don't postback any form data, as UpdatePanel does. Besides this, page method executes static method - comparing to UpdatePanel that executes whole page controls collection life cycle. Just use this carefully, if you need lot of form data and a small number of dynamically loaded user controls, use UpdatePanel.