This Post is also very code heavy and detail light. Instead of spreading the code out for this step, I'm going to just put a large number of code listings at the bottom. In the last post we got a very basic dynamic modal working, that allowed us to push data back and forth between to controls hosted in a Modal dialog. For this next pass I want to leave the modal dialog out of this all together and break into some code using WebMethods.
First, it's a very legitimate thought that I should really be using creating an Extender to do most of the work both from the last, post and especially some of the code I'm going to cover in this post. But I'm not, for the simple reason, that I'm not sold that the overhead of the Ajax.ASP.net Framework is always worth it. Granted the code that will be listed below is full of holes, and really not baked for production, but it's just an example in a process, and hopefully some useful ideas will come from it.
There really is no right answer when it comes to dynamic websites, the right development language, style and methodology is 100% dependent on a client's. That said, the vast majority of the time, when Asp.Net is an appropriate platform, using the Ajax Framework to enhance the user experience is an ideal choice. But just like a powerful tool like a Typed Dataset, there are times when it's just going to be better to work closer to the bone. The terrible analogy I like is the idea of chopping firewood with an Axe vs. a Chainsaw, both are very effective, and both are very dangerous. The trouble with the Chainsaw is that when things go wrong, especially in the hands of an inexperienced operator, the can become catastrophic extremely quickly. Whereas with an Axe, the slower steady pace tends to lend itself to a more contemplative and flexible approach; yes catastrophe is still an option, but it's much easier to contain and manage (- why this is a bad analogy besides the obvious – someone once commented that Axes don't have safety switches).
So for this post and most of what I'm covering in this series I want to take full advantage of the framework, while allowing, at least somewhat, for portability. The bulk of what will be listed in this post will deal with some custom server controls that are designed to mitigate some of the issues with the JavaScript code explosion that can occur with webmethods. The number one concern I have whenever I work with client-side scripts is how to manage and test them over the long run. To help with this, I've made some sample controls that mostly hide the extra JavaScript work that comes with webmethods. Hopefully some part of them will be useful to someone.
IWebMethodMapping: - The mapping controls are used to map input values to JSON object properties. Rather than writing your own var data = new SomeObject(); data.Field1= get$(ID).value statements, these controls use the provided mapping information to generate client side data object "builders" that encapsulate that functionality for you.
WebMethodUIUpdateLink: - This link control is the most fragile part of this example, but essentially it wires up the WebMethod Call, Response handling, and in this case, the fragile part is the extraction and registration of any inline JavaScript. I'm not entirely happy with this arrangement, but I'm also not a fan of the need to use something like ScriptManager.RegisterClientScript. When you use something like that then you are targeting a control for a specific environment, and that' not very ideal, it means that if you want to use any existing controls in say an UpdatePanel, you're going to have to audit, and likely refactor how the work, and you may not be able to use something like the ScriptManager, because it may be used on pages that don't have a ScriptManager. This can be a real pain for QA when you're slowly updating a large site. Hopefully by the end of this series I'll have something more stable.
AjaxControlRender – this is mostly borrowed from here with some added fun to allow the dynamic rendering of Ajax-ed up controls like the CalendarExtender that require a HEAD element to inject style information into.
Everything else is pretty much standard ASP.Net code, so here's the full dump with apologies for formatting, and bad attributes, and the shear amount of, well mostly useless code
IWebMethodMapping
public interface IWebMethodMapping
{
string DataBuilderName{ get; }
}
WebMethodSingleValueMapping
[DefaultProperty("ControlID")]
[ToolboxData("<{0}:WebMethodSingleValueMapping runat=server></{0}:WebMethodSingleValueMapping>")]
public class WebMethodSingleValueMapping : WebControl, IWebMethodMapping
{
[Bindable(true)]
[Category("Default")]
[DefaultValue("")]
[Localizable(true)]
[IDReferenceProperty]
public string ControlID
{
get { return _controlID; }
set { _controlID = value; }
}
protected override void Render(HtmlTextWriter writer)
{
StringBuilder scriptBuilder = new StringBuilder();
scriptBuilder.AppendLine("<script type=\"text/javascript\">");
scriptBuilder.AppendFormat("function getValue{0}(){{", ControlID);
Control mapped = FindControl(ControlID);
scriptBuilder.AppendFormat("return document.getElementById('{0}').value;}}", mapped.ClientID);
scriptBuilder.AppendLine("</script>");
writer.Write(scriptBuilder.ToString());
}
public override Control FindControl(string id)
{
Control control = base.FindControl(id);
if (control != null)
{
return control;
}
for (Control container = NamingContainer; container != null; container = container.NamingContainer)
{
control = container.FindControl(id);
if (control != null)
{
return control;
}
}
return null;
}
private string _controlID;
public string DataBuilderName
{
get { return "getValue" + ControlID; }
}
}
Mapping
public class Mapping
{
[IDReferenceProperty]
public string ControlID
{
get { return _controlID; }
set { _controlID = value; }
}
public string MappedPropertyName
{
get { return _mappedPropertyName; }
set { _mappedPropertyName = value; }
}
private string _controlID;
private string _mappedPropertyName;
}
WebMethodDataTypeMapping
[DefaultProperty("MappedType")]
[ToolboxData("<{0}:WebMethodDataTypeMapping runat=server></{0}:WebMethodDataTypeMapping>")]
public class WebMethodDataTypeMapping : CompositeControl, IWebMethodMapping
{
[Bindable(false)]
[Category("Data")]
[DefaultValue(null)]
[Localizable(false)]
public string MappedType
{
get { return _mappdedType; }
set { _mappdedType = value; }
}
[PersistenceMode(PersistenceMode.InnerDefaultProperty)]
[MergableProperty(false)]
[DefaultValue((string)null)]
public List<Mapping> Mappings
{
get { return _mappings; }
set { _mappings = value; }
}
public string DataBuilderName
{
get
{
return "build" + LoadType().Name;
}
}
protected override void Render(HtmlTextWriter writer)
{
StringBuilder scriptBuilder = new StringBuilder();
Type type = LoadType();
scriptBuilder.AppendLine("<script type=\"text/javascript\">");
scriptBuilder.AppendFormat("function build{0}(){{", type.Name);
scriptBuilder.AppendFormat("var dataObj = new {0}();", type.FullName);
foreach (Mapping mapping in Mappings)
{
Control mapped = FindControl(mapping.ControlID);
if (null == mapped)
{
throw new ApplicationException("Could Not find Control: " + mapping.ControlID);
}
scriptBuilder.AppendFormat("dataObj.{0} = document.getElementById('{1}').value;", mapping.MappedPropertyName, mapped.ClientID);
}
scriptBuilder.AppendLine(" return dataObj; }");
scriptBuilder.AppendLine("</script>");
writer.Write(scriptBuilder.ToString());
}
public override Control FindControl(string id)
{
Control control = base.FindControl(id);
if (control != null)
{
return control;
}
for (Control container = NamingContainer; container != null; container = container.NamingContainer)
{
control = container.FindControl(id);
if (control != null)
{
return control;
}
}
return null;
}
protected Type LoadType()
{
if (string.IsNullOrEmpty(MappedType))
{
throw new ApplicationException("MappedType must be specified");
}
Assembly container = Assembly.GetCallingAssembly();
string typeName = MappedType;
if (MappedType.IndexOf(',') != -1)
{
string[] typeDef = MappedType.Split(',');
typeName = typeDef[0].Trim();
container = Assembly.Load(typeDef[1].Trim());
}
return container.GetType(typeName,true, true);
}
private string _mappdedType;
private List<Mapping> _mappings;
}
WebMethodUIUpdateLink
[DefaultProperty("TargetControlID")]
[ToolboxData("<{0}:WebMethodUIUpdateLink runat=server></{0}:WebMethodUIUpdateLink>")]
public class WebMethodUIUpdateLink : LinkButton
{
[Category("General")]
[DefaultValue("")]
[Localizable(false)]
public string TartetControlID
{
get { return _targetControlID; }
set { _targetControlID = value; }
}
[Category("General")]
[DefaultValue(false)]
[Localizable(false)]
public bool UseParentAsTarget
{
get { return _userParentAsTarget; }
set { _userParentAsTarget = value; }
}
[Category("General")]
[Localizable(false)]
public IWebMethodMapping DataMap
{
get { return _dataMap; }
set { _dataMap = value; }
}
[Category("General")]
[DefaultValue("")]
[Localizable(false)]
public string DataMapControlID
{
get { return _dataMapControlID; }
set { _dataMapControlID = value; }
}
[Category("General")]
[DefaultValue("")]
[Localizable(false)]
public string WebMethod
{
get { return _webMethod; }
set { _webMethod = value; }
}
public override Control FindControl(string id)
{
Control control = base.FindControl(id);
if (control != null)
{
return control;
}
for (Control container = NamingContainer; container != null; container = container.NamingContainer)
{
control = container.FindControl(id);
if (control != null)
{
return control;
}
}
return null;
}
protected override void OnPreRender(EventArgs e)
{
base.OnClientClick = string.Format("{0}InitRequest(); return false;", this.ID);
}
protected override void Render(HtmlTextWriter writer)
{
writer.WriteLine(buildJS());
base.Render(writer);
}
protected string buildJS()
{
if (string.IsNullOrEmpty(WebMethod))
{
throw new ApplicationException("WebMethod must be specified.");
}
StringBuilder scriptBuilder = new StringBuilder();
scriptBuilder.AppendLine("<script type=\"text/javascript\">");
if (!Page.IsPostBack)
{
scriptBuilder.AppendLine("function regJS(html){");
scriptBuilder.AppendLine("while (html.indexOf('<script') != -1){");
scriptBuilder.AppendLine("var scriptStart = html.indexOf('>', (html.indexOf('<' + 'script')));");
scriptBuilder.AppendLine("var scriptEnd = html.indexOf('<' +'/script>');");
scriptBuilder.AppendLine("var script = html.substring(scriptStart+1, scriptEnd);");
scriptBuilder.AppendLine("html = html.substring(0,html.indexOf('<' + 'script')) + html.substring(scriptEnd+9,html.length-1);");
scriptBuilder.AppendLine("var scriptElement = document.createElement('SCRIPT');");
scriptBuilder.AppendLine("scriptElement.type = 'text/javascript';");
scriptBuilder.AppendLine("document.getElementsByTagName('HEAD')[0].appendChild(scriptElement);");
scriptBuilder.AppendLine("scriptElement.innerHTML = script;");
scriptBuilder.AppendLine("}");
scriptBuilder.AppendLine("return html;}");
}
//EDIT this is a bug
//if (!string.IsNullOrEmpty(TartetControlID))
// {
scriptBuilder.AppendFormat("var SRC_TGT = {0};", getTargetClientID());
// }
scriptBuilder.AppendFormat("function {0}ResponseHandler(result){{", this.ID);
if (!Page.IsPostBack)
{
scriptBuilder.AppendLine("result = regJS(result);");
}
scriptBuilder.AppendFormat("var el = document.getElementById({0}); el.innerHTML = result; if(el.style.display == 'none') {{ el.style.display = '';}}", getTargetClientID());
scriptBuilder.AppendLine("}");
if (!string.IsNullOrEmpty("DataMapControlID"))
{
if (null != DataMap)
{
throw new ApplicationException("Both the DataMap and the DataMapControlID cannot be specified.");
}
DataMap = FindWebMethodMapping(DataMapControlID);
}
scriptBuilder.AppendLine("");
scriptBuilder.AppendFormat("function {0}InitRequest(){{", this.ID);
if (null != DataMap)
{
scriptBuilder.AppendFormat("PageMethods.{0}({1}(),{2}ResponseHandler);", WebMethod, DataMap.DataBuilderName, this.ID);
}
else
{
scriptBuilder.AppendFormat("PageMethods.{0}(null,{1}ResponseHandler);", WebMethod, this.ID);
}
scriptBuilder.AppendLine("}");
scriptBuilder.AppendLine("</script>");
return scriptBuilder.ToString();
}
protected string getTargetClientID()
{
if (string.IsNullOrEmpty(TartetControlID) )
{
if (!_userParentAsTarget)
{
throw new ApplicationException("TargetControlID must be specified");
}
return "SRC_TGT";
}
else
{
Control target = FindControl(TartetControlID);
if (null == target)
{
throw new ApplicationException(string.Format("TargetControlID {0} could not be found", TartetControlID));
}
return string.Format("'{0}'",target.ClientID);
}
}
protected IWebMethodMapping FindWebMethodMapping(string id)
{
Control control = base.FindControl(id);
if (control != null)
{
IWebMethodMapping map = control as IWebMethodMapping;
if (null != map)
{
return map;
}
}
for (Control container = NamingContainer; container != null; container = container.NamingContainer)
{
control = container.FindControl(id);
if (control != null)
{
IWebMethodMapping map = control as IWebMethodMapping;
if (null != map)
{
return map;
}
}
}
return null;
}
private string _webMethod;
private string _targetControlID;
private IWebMethodMapping _dataMap;
private string _dataMapControlID;
private bool _userParentAsTarget;
}
AjaxControlRenderer
public class AjaxControlRenderer
{
public static string RenderAjaxControl<T, D>(bool enableViewState, string path, D data) where T : System.Web.UI.Control, IRenderable<D>, new()
{
Page renderPage = new Page();
renderPage.EnableViewState = enableViewState;
HtmlHead header = renderPage.LoadControl(typeof(HtmlHead), null) as HtmlHead; header.EnableViewState = enableViewState;
HtmlForm form = renderPage.LoadControl(typeof(HtmlForm), null) as HtmlForm;
form.EnableViewState = enableViewState;
ScriptManager scriptManager = renderPage.LoadControl(typeof(ScriptManager), null) as ScriptManager;
T controlToRender = renderPage.LoadControl(path) as T;
renderPage.Controls.Add(header);
renderPage.Controls.Add(form);
form.Controls.Add(scriptManager);
form.Controls.Add(new LiteralControl("<!-- START -->"));
form.Controls.Add(controlToRender);
form.Controls.Add(new LiteralControl("<!-- END -->"));
if (null != data)
{
controlToRender.PopulateData(data);
}
StringWriter htmlOutput = new StringWriter();
HttpContext.Current.Server.Execute(renderPage, htmlOutput, false);
string result = htmlOutput.ToString();
result = result.Substring(result.IndexOf("<!-- START -->"));
result = result.Substring(0, result.IndexOf("<!-- END -->") + 14);
return result;
}
}
public interface IRenderable<T>
{
void PopulateData(T data);
}
Usage:
ControlOne
<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="ControlOne.ascx.cs" Inherits="WebMethodWork.ControlOne" %>
<%@ Register Assembly="Controls" Namespace="Controls" TagPrefix="cc1" %>
<cc1:WebMethodDataTypeMapping ID="DataMapping" runat="server" MappedType="WebMethodWork.SampleDataObject, WebMethodWork" >
<Mappings>
<cc1:Mapping ControlID="UID" MappedPropertyName="UID" />
<cc1:Mapping ControlID="VALUE" MappedPropertyName="Value" />
</Mappings>
</cc1:WebMethodDataTypeMapping>
<table border="0" cellpadding="1" cellspacing="0" width="100%" style="background-color:#cecece;">
<tr>
<td colspan="2">
<b>This Is Control One</b>
</td>
</tr>
<tr>
<td align="right">ID Sent to this Control:</td>
<td align="left" style="text-decoration:underline;"><asp:Label ID="lblUID" runat="Server" /></td>
</tr>
<tr>
<td colspan="2" align="center"><hr style="width:98%;" /></td>
</tr>
<tr>
<td align="right">ID to send to ControlTwo:</td>
<td align="left"><asp:TextBox ID="UID" runat="Server" /></td>
</tr>
<tr>
<td align="right">Text to send to ControlTwo:</td>
<td align="left"><asp:TextBox ID="VALUE" runat="Server" /></td>
</tr>
<tr>
<td colspan="2" align="center">
<cc1:WebMethodUIUpdateLink
ID="dynGetControlOneLink"
runat="server"
Text="Get Control Two"
DataMapControlID="DataMapping"
WebMethod="GetControlTwo"
UseParentAsTarget="true" />
</td>
</tr>
<tr>
<td colspan="2" style="font-weight:bold;">
<asp:Label ID="TimeStamp" runat="server"/></td>
</tr>
</table>
public partial class ControlOne : System.Web.UI.UserControl, IRenderable<object>
{
protected void Page_Load(object sender, EventArgs e)
{
TimeStamp.Text = DateTime.Now.ToShortDateString() + " " + DateTime.Now.ToLongTimeString();
}
public void PopulateData(object data)
{
this.lblUID.Text = data as string;
}
}
ControlTwo
<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="ControlTwo.ascx.cs" Inherits="WebMethodWork.ControlTwo" %>
<%@ Register Assembly="Controls" Namespace="Controls" TagPrefix="cc1" %>
<cc1:WebMethodSingleValueMapping ID="DataMapping" runat="server" ControlID="UID" />
<table border="0" cellpadding="1" cellspacing="0" width="100%" style="background-color:#cecece;">
<tr>
<td colspan="2">
<b>This Is Control Two</b>
</td>
</tr>
<tr>
<td align="right">ID Sent to this Control:</td>
<td align="left" style="text-decoration:underline;"><asp:Label ID="lblUID" runat="Server" /></td>
</tr>
<tr>
<td align="right">Text Sent to this Control:</td>
<td align="left" style="text-decoration:underline;"><asp:Label ID="lblValue" runat="Server" /></td>
</tr>
<tr>
<td colspan="2" align="center"><hr style="width:98%;" /></td>
</tr>
<tr>
<td align="right">ID to send to ControlTwo:</td>
<td align="left"><asp:TextBox ID="UID" runat="Server" /></td>
</tr>
<tr>
<td colspan="2" align="center">
<cc1:WebMethodUIUpdateLink
ID="dynControlTwoLink"
runat="server" Text="Get Control One"
DataMapControlID="DataMapping"
WebMethod="GetControlOne"
UseParentAsTarget="true" />
</td>
</tr>
<tr>
<td colspan="2" style="font-weight:bold;">
<asp:Label ID="TimeStamp" runat="server"/></td>
</tr>
</table>
public partial class ControlTwo : System.Web.UI.UserControl, IRenderable<SampleDataObject>
{
protected void Page_Load(object sender, EventArgs e)
{
TimeStamp.Text = DateTime.Now.ToShortDateString() + " " + DateTime.Now.ToLongTimeString();
}
public void PopulateData(SampleDataObject data)
{
this.lblUID.Text = data.UID;
this.lblValue.Text = data.Value;
}
}
Default
<form id="form1" runat="server">
<asp:ScriptManager ID="MainManager" runat="server" EnablePageMethods="true" />
<div>
<div style="border: solid 1px #cccccc;">
<asp:Label ID="TimeStamp" runat="server"/><br />
<cc1:WebMethodSingleValueMapping ID="ControlOneDataMapping" runat="server" ControlID="UID" />
ID To pass to Control One or Two:<asp:TextBox ID="UID" runat="Server" /><br />
Value To pass to Control Two:<asp:TextBox ID="VALUE" runat="Server" /><br />
<br />
<cc1:WebMethodUIUpdateLink ID="GetControlOneLink" runat="server"
Text="Get Control One"
DataMapControlID="ControlOneDataMapping"
TartetControlID="Target"
WebMethod="GetControlOne" />
<br />
<cc1:WebMethodDataTypeMapping ID="ControlTwoDataMapping" runat="server" MappedType="WebMethodWork.SampleDataObject, WebMethodWork" >
<Mappings>
<cc1:Mapping ControlID="UID" MappedPropertyName="UID" />
<cc1:Mapping ControlID="VALUE" MappedPropertyName="Value" />
</Mappings>
</cc1:WebMethodDataTypeMapping>
<cc1:WebMethodUIUpdateLink ID="GetControlTwoLink" runat="server"
Text="Get Control Two"
DataMapControlID="ControlTwoDataMapping"
TartetControlID="Target"
WebMethod="GetControlTwo" />
</div>
<br />
<br />
<asp:Panel ID="Target" runat="Server" style="display:none; border: solid 1px #000000; width:400px;" />
</div>
</form>
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
TimeStamp.Text = DateTime.Now.ToShortDateString() + " " + DateTime.Now.ToLongTimeString();
}
}
Default.aspx.WebMethod.cs
public partial class _Default
{
[WebMethod]
public static string GetControlOne(string ID)
{
return AjaxControlRenderer.RenderAjaxControl<ControlOne, object>(true, "ControlOne.ascx", ID);
}
[WebMethod]
public static string GetControlTwo(SampleDataObject data)
{
return AjaxControlRenderer.RenderAjaxControl<ControlTwo, SampleDataObject>(true, "ControlTwo.ascx", data);
}
}
SampleDataObject
public class SampleDataObject
{
public string UID
{
get { return _uid; }
set { _uid = value; }
}
public string Value
{
get { return _value; }
set { _value = value; }
}
public SampleDataObject()
{
}
private string _uid;
private string _value;
}
Print | posted on Monday, October 08, 2007 11:12 PM