Click here to Skip to main content
15,888,610 members
Articles / Web Development / HTML

Magic AJAX: Applying AJAX to your existing Web Pages

Rate me:
Please Sign up or sign in to vote.
4.82/5 (72 votes)
28 May 2007MIT12 min read 971.8K   2.7K   251  
How to apply AJAX technologies to your web pages without replacing ASP.NET controls and/or writing JavaScript code.
#region LGPL License
/*
MagicAjax.NET Framework
Copyright (C) 2005  MagicAjax Project Team

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#endregion

using System;
using System.ComponentModel;
using System.Collections;
using System.Collections.Specialized;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;

namespace MagicAjax.UI.Controls
{
	/// <summary>
	/// Defines the AjaxCall connection types.
	/// </summary>
	public enum AjaxCallConnectionType
	{
		/// <summary>
		/// Client will not invoke an AjaxCall
		/// </summary>
		None,
		/// <summary>
		/// Asynchronous AjaxCall
		/// </summary>
		Asynchronous,
		/// <summary>
		/// Synchronous AjaxCall
		/// </summary>
		Synchronous
	}

	/// <summary>
	/// Defines what form elements should be excluded from the post data.
	/// </summary>
	[Flags]
	public enum ExcludeFromPostFlag
	{
		/// <summary>
		/// Post all elements
		/// </summary>
		None = 0,
		/// <summary>
		/// Do not post ViewState
		/// </summary>
		ViewState = 1,
		/// <summary>
		/// Do not post the control fingerprints hidden fields
		/// </summary>
		Fingerprints = 2,
		/// <summary>
		/// Do not post the custom hidden fields of the user
		/// </summary>
		UserHidden = 4,
		/// <summary>
		/// Do not post any hidden field
		/// </summary>
		AllHidden = ViewState | Fingerprints | UserHidden,
		/// <summary>
		/// Do not post any (non-hidden) form element
		/// </summary>
		FormElements = 8,
		/// <summary>
		/// Do not post any element
		/// </summary>
		AllElements = AllHidden | FormElements
	}

	#region AjaxPanelUpdated event Handler & Args
	public class AjaxPanelUpdatedEventArgs
	{
		private Control _updatedControl;

		public Control UpdatedControl
		{
			get { return _updatedControl; }
		}

		public AjaxPanelUpdatedEventArgs(Control updatedControl)
		{
			this._updatedControl = updatedControl;
		}
	}

	public delegate void AjaxPanelUpdatedEventHandler(object sender, AjaxPanelUpdatedEventArgs e);
	#endregion

	/// <summary>
	/// Works like Panel but the controls it contains are rendered on the page by sending
	/// javascript to the client.
	/// </summary>
	/// <remarks>
	/// The main control that makes all the hard work for seamless AJAXing. It spots
	/// controls that are added, removed or altered, and sends the appropriate javascript
	/// for the refreshing of the page. In case it contains a RenderedByScriptControl, it
	/// ignores it and lets the RenderedByScriptControl to produce the appropriate javascript
	/// for its appearance.
	/// 
	/// If an AjaxPanel (parent) contains another AjaxPanel (child), and a control
	/// of the child-AjaxPanel is altered, parent-AjaxPanel won't send the entire html
	/// rendering of the child-AjaxPanel, but instead the child-AjaxPanel will send only
	/// the html of the altered control. Thus, the size of javascript code that the client
	/// gets as a response of an AjaxCall, is greatly reduced.
	/// 
	/// It is not necessary to put all the controls of the page inside an AjaxPanel. Only
	/// the controls that are going to be refreshed on the client using javascript are
	/// required to be contained within an AjaxPanel.
	/// </remarks>
	[Designer("MagicAjax.UI.Design.AjaxPanelDesigner, MagicAjax"),
		 ParseChildrenAttribute(false),
		 PersistChildren(true),
		 ToolboxData("<{0}:AjaxPanel runat=server>AjaxPanel</{0}:AjaxPanel>")]
	public class AjaxPanel : RenderedByScriptControl
	{
		#region Fields

		private AjaxCallConnectionType _ajaxCallConnection = AjaxCallConnectionType.Asynchronous;
		private ExcludeFromPostFlag _excludeFlags = ExcludeFromPostFlag.None;
		private ArrayList _addedControls = new ArrayList();
		private ArrayList _removedControls = new ArrayList();
		private Hashtable _controlHtmlFingerprints = new Hashtable();
		private ControlCollectionState _controlState;
		private Hashtable _controlUpdatedHandlerLists = new Hashtable();

		#endregion

		#region Constructor
		/// <summary>
		/// Creates an instance of a RenderedByScriptControl with the Span tagName.
		/// </summary>
		/// <remarks>
		/// AjaxPanel uses Span instead of Div, because ASP.NET translates Div to a
		/// table if the browser is Firefox.
		/// </remarks>
		public AjaxPanel()
			: base(HtmlTextWriterTag.Span)
		{
			_controlState = new ControlCollectionState(this);
		}
		#endregion

		#region Properties

		#region AjaxCallConnection
		/// <summary>
		/// Defines the connection type (a/synchronous) that will be utilized for an AjaxCall.
		/// Default is AjaxCallConnectionType.Asynchronous.
		/// </summary>
		[Bindable(false),
			 Category("Behaviour"),
			 DefaultValue(AjaxCallConnectionType.Asynchronous)]
		public AjaxCallConnectionType AjaxCallConnection
		{
			get { return _ajaxCallConnection; }
			set { _ajaxCallConnection = value; }
		}
		#endregion

		#region ExcludeFlags
		/// <summary>
		/// Defines the form elements that will be excluded from the POST data.
		/// Default is ExcludeFromPostFlag.None.
		/// </summary>
		[Bindable(false),
			 Category("Behaviour"),
			 DefaultValue(ExcludeFromPostFlag.None)]
		public ExcludeFromPostFlag ExcludeFlags
		{
			get { return _excludeFlags; }
			set { _excludeFlags = value; }
		}
		#endregion

		#endregion

		#region Events

		public event AjaxPanelUpdatedEventHandler ContentUpdated;

		#endregion

		#region Public Methods

		#region Clear
		/// <summary>
		/// It removes the child controls of the AjaxPanel without producing the 
		/// appropriate javascript to refresh the page at the client.
		/// </summary>
		public void Clear()
		{
			Controls.Clear();
			_addedControls.Clear();
			_removedControls.Clear();
			_controlHtmlFingerprints.Clear();
		}
		#endregion

		#region override IsRenderedOnPage
		/// <summary>
		/// If it's in 'NoStore' page mode, returns true if the AjaxPanel's fingerprint
		/// is not empty, false if it is empty.
		/// </summary>
		public override bool IsRenderedOnPage
		{
			get
			{
				if (IsPageNoStoreMode)
				{
					return (Context.Request.Form[ControlCollectionState.GetControlFingerprintsField(this.ClientID)] != String.Empty);
				}
				else
					return base.IsRenderedOnPage;
			}
		}
		#endregion

		public virtual void AddControlUpdatedEventHandler(Control controlToMonitor, EventHandler controlUpdatedHandler)
		{
			ArrayList handlerList = _controlUpdatedHandlerLists[controlToMonitor] as ArrayList;
			if (handlerList == null)
			{
				handlerList = new ArrayList();
				_controlUpdatedHandlerLists.Add(controlToMonitor, handlerList);
			}

			if (!handlerList.Contains(controlUpdatedHandler))
				handlerList.Add(controlUpdatedHandler);
		}

		public virtual void RemoveControlUpdatedEventHandler(Control controlToMonitor, EventHandler controlUpdatedHandler)
		{
			ArrayList handlerList = _controlUpdatedHandlerLists[controlToMonitor] as ArrayList;
			if (handlerList == null)
			{
				handlerList = new ArrayList();
				_controlUpdatedHandlerLists.Add(controlToMonitor, handlerList);
			}

			handlerList.Add(controlUpdatedHandler);
		}

		#endregion

		#region Protected Methods

		#region override OnLoad
		protected override void OnLoad(EventArgs e)
		{
			base.OnLoad(e);

			if (this.MagicAjaxContext.IsBrowserSupported)
			{
				Page.RegisterHiddenField(ControlCollectionState.GetControlFingerprintsField(this.ClientID), String.Empty);
			}

#if !MEDIUM_TRUST
			// Attach to prerendercomplete event so we can do
			// some extra processing before rendering of the AjaxPanels.
			// Also some processing is done after rendering of the AjaxPanels, 
			// see Render method in RenderedByScriptControl.cs.
			// This extra processing involves reflecting hidden fields, scripts and 
			// stylesheets that were added/changed on a callback.
			if (Context != null && !Context.Items.Contains("__ATTACHED_PAGE_HANDLERS"))
			{
				Context.Items.Add("__ATTACHED_PAGE_HANDLERS", String.Empty);
	#if NET_2_0
				this.Page.PreRenderComplete += new EventHandler(AjaxCallHelper.Page_PreRender);
	#else
				this.Page.PreRender += new EventHandler(AjaxCallHelper.Page_PreRender);
	#endif
			}
#endif
		}
		#endregion

		#region override OnAjaxCallEnd
		protected override void OnAjaxCallEnd(EventArgs e)
		{
			if (IsPageNoStoreMode && IsRenderedOnPage)
			{
				// Verify that it is indeed rendered on page. If it's not, set its
				// fingerprint to empty string.
				if (!(GetTopInvisibleControl(this) is RenderedByScriptControl))
					AjaxCallHelper.WriteSetFieldScript(ControlCollectionState.GetControlFingerprintsField(this.ClientID), String.Empty);
			}

			base.OnAjaxCallEnd(e);
		}
		#endregion

		#region override AddedControl
		/// <summary>
		/// Adds the control to the added controls collection.
		/// </summary>
		/// <param name="control"></param>
		/// <param name="index"></param>
		protected override void AddedControl(Control control, int index)
		{
			if (IsHtmlHolder(control))
			{
				if (IsPageNoStoreMode)
				{
					// If the ID of the added control is null, find a unique id manually,
					// or else the default naming order may cause mismatches.
					if (control.ID == null || control.ID == String.Empty)
						control.ID = FindUniqueID();
				}

				_addedControls.Add(control);
			}

			base.AddedControl(control, index);
		}
		#endregion

		#region override RemovedControl
		/// <summary>
		/// Adds the control to the removed controls collection.
		/// </summary>
		/// <param name="control"></param>
		/// <param name="index"></param>
		protected override void RemovedControl(Control control)
		{
			if (IsHtmlHolder(control))
			{
				if (_addedControls.Contains(control))
					_addedControls.Remove(control);
				else
					_removedControls.Add(control);
			}

			base.RemovedControl(control);
		}
		#endregion

		#region override RenderChildren
		/// <summary>
		/// Does a normal rendering of the child controls.
		/// </summary>
		/// <remarks>
		/// Each child control is contained inside a Span tag with unique id so that
		/// it can be easily manipulated apart from the other child controls.
		/// </remarks>
		/// <param name="writer"></param>
		protected override void RenderChildren(HtmlTextWriter writer)
		{
			if (MagicAjaxContext == null)
			{
				// It's in VS Designer, do the default render
				base.RenderChildren(writer);
				return;
			}

			System.Text.StringBuilder sbFull = new System.Text.StringBuilder();
			System.Text.StringBuilder sb = new System.Text.StringBuilder();
			HtmlTextWriter fullwriter = new HtmlTextWriter(new System.IO.StringWriter(sbFull));
			HtmlTextWriter litewriter = new HtmlTextWriter(new System.IO.StringWriter(sb));

			StringBuilder sbLiteral = new StringBuilder();
			HtmlTextWriter literalwriter = new HtmlTextWriter(new System.IO.StringWriter(sbLiteral));

			for (int i = 0; i < Controls.Count; i++)
			{
				Control con = Controls[i];

				// Put inside span tag only html holder controls
				bool isHtmlHolder = IsHtmlHolder(con);

				if (isHtmlHolder)
				{
					writer.WriteBeginTag("span");
					writer.WriteAttribute("id", GetAjaxElemID(con));
					writer.WriteAttribute("name", "__ajaxmark");
					writer.Write(HtmlTextWriter.TagRightChar);

					literalwriter.WriteBeginTag("span");
					literalwriter.WriteAttribute("id", GetAjaxElemID(con));
					literalwriter.WriteAttribute("name", "__ajaxmark");
					literalwriter.Write(HtmlTextWriter.TagRightChar);
				}

				ExtendedRenderControl(con, fullwriter, litewriter);
				writer.Write(sbFull.ToString());
				string html = sb.ToString();

				if (isHtmlHolder)
				{
					_controlHtmlFingerprints[con] = Util.GetFingerprint(html);
					writer.WriteEndTag("span");
					literalwriter.WriteEndTag("span");
				}
				else
				{
					literalwriter.Write(html);
				}

				sbFull.Length = 0;
				sb.Length = 0;
			}

			_controlState.LiteralFingerprint = Util.GetFingerprint(sbLiteral.ToString());

			_addedControls.Clear();
			_removedControls.Clear();

			if (IsPageNoStoreMode)
				SaveControlState();
		}
		#endregion

		#region override AddAjaxAttributesToRender
		protected override void AddAjaxAttributesToRender(HtmlTextWriter writer)
		{
			base.AddAjaxAttributesToRender(writer);

			switch (_ajaxCallConnection)
			{
				case AjaxCallConnectionType.Asynchronous:
					writer.AddAttribute("AjaxCall", "async");
					break;

				case AjaxCallConnectionType.Synchronous:
					writer.AddAttribute("AjaxCall", "sync");
					break;
			}

			if (_excludeFlags != ExcludeFromPostFlag.None)
			{
				writer.AddAttribute("ExcludeFlags", ((int)_excludeFlags).ToString());
			}
		}
		#endregion

		protected virtual void SaveControlState()
		{
			//note:only for NoStore mode
			if (_controlState != null)
			{
				_controlState.SetControlIDs(_controlHtmlFingerprints);
				_controlState.Save(this.ClientID, this.Page);
			}
		}

		protected virtual void LoadControlState()
		{
			_controlState = ControlCollectionState.LoadState(this.ClientID, this);

			if (_controlState == null)
			{
				// The control fingerprints were excluded from the post data.
				// There will be no evaluation of 'html holders'; all the contents
				// of the AjaxPanel will be sent to client in one javascript command.
				return;
			}

			// Find new and previous controls
			_addedControls.Clear();
			_controlHtmlFingerprints.Clear();

			foreach (Control con in this.Controls)
			{
				if (IsHtmlHolder(con))
				{
					if (_controlState.ControlHtmlFingerprints.ContainsKey(con.ClientID))
					{
						_controlHtmlFingerprints[con] = _controlState.ControlHtmlFingerprints[con.ClientID];
					}
					else
					{
						_addedControls.Add(con);
					}
				}
			}

			// Removed controls do not render their 'marker tags' and will be removed from
			// page when literal content is rendered. So there's no need to
			// explicitly send javascript command to remove a control.
			_removedControls.Clear();
		}

		/// <summary>
		/// Parses all form input controls from the html, and checks if
		/// their values were updated (i.e. different from the current Request.Form values)
		/// and sends javascript commands to update the client's form values if necessary.
		/// </summary>
		/// <param name="html"></param>
		protected void ReflectUpdatedFormValues(string html)
		{
			NameValueCollection form = Context.Request.Form;

			Regex regEx = Util.FormElementRegEx;
			MatchCollection matches = regEx.Matches(html);
			for (int i = 0; i < matches.Count; i++)
			{
				Match match = matches[i];
				CaptureCollection attrnames = match.Groups["attrname"].Captures;
				CaptureCollection attrvalues = match.Groups["attrvalue"].Captures;

				Hashtable attrNameValues = new Hashtable(attrnames.Count);
				for (int j = 0; j < attrnames.Count; j++)
				{
					attrNameValues[attrnames[j].Value.ToLower(System.Globalization.CultureInfo.InvariantCulture)] = attrvalues[j].Value;
				}

				// If the form element has the MagicAjax 'ExcludeFromPost' attribute
				// set to 'true', ignore it.
				if (attrNameValues.ContainsKey("excludefrompost")
					 && (attrNameValues["excludefrompost"] as String).ToLower(System.Globalization.CultureInfo.InvariantCulture) == "true")
					continue;

				string tag = match.Groups["tag"].Value.ToLower(System.Globalization.CultureInfo.InvariantCulture);
				string name = (string)attrNameValues["name"];
				string clientID = (string)attrNameValues["id"];

				if (name == null || clientID == null)
					continue;

				switch (tag)
				{
					#region <input> tags

					case "input":
						string type = (string)attrNameValues["type"];
						if (type != null)
						{
							string value = attrNameValues.ContainsKey("value") ? (string)attrNameValues["value"] : String.Empty;
							bool isChecked = attrNameValues.ContainsKey("checked");

							switch (type)
							{
								case "text":
									if (value != form[name])
										AjaxCallHelper.WriteSetFieldScript(clientID, value);
									break;
								case "checkbox":
									if (isChecked != (form[name] != null))
										AjaxCallHelper.WriteFormat("__PageForm[\"{0}\"].checked={1};\r\n", clientID, (isChecked) ? "true" : "false");
									break;
								case "radio":
									if (isChecked && form[name] != value)
									{
										AjaxCallHelper.WriteFormat("__PageForm[\"{0}\"].checked=true;\r\n", clientID);
									}
									else if (!isChecked && form[name] == value)
									{
										AjaxCallHelper.WriteFormat("__PageForm[\"{0}\"].checked=false;\r\n", clientID);
									}
									break;
							}
						}
						break;

					#endregion

					#region <textarea> tags

					case "textarea":
						string text = match.Groups["inner"].Value;
						if (text != form[name])
							AjaxCallHelper.WriteSetFieldScript(clientID, text);
						break;

					#endregion

					#region <select> tags

					case "select":
						Regex regExOpt = Util.FormElementOptionsRegEx;
						bool multiple = attrNameValues.ContainsKey("multiple");

						ArrayList serverOptions = new ArrayList();
						ArrayList serverSelected = new ArrayList();
						string oneSelection = null;

						//now match the options within this <select> tag
						MatchCollection matchesOptions = regExOpt.Matches(match.Groups["inner"].Value);
						for (int j = 0; j < matchesOptions.Count; j++)
						{
							Match matchOption = matchesOptions[j];

							CaptureCollection attrnamesOption = matchOption.Groups["attrname"].Captures;
							CaptureCollection attrvaluesOption = matchOption.Groups["attrvalue"].Captures;

							Hashtable attrNameValuesOption = new Hashtable();
							for (int k = 0; k < attrnamesOption.Count; k++)
							{
								attrNameValuesOption[attrnamesOption[k].Value.ToLower(System.Globalization.CultureInfo.InvariantCulture)] = attrvaluesOption[k].Value;
							}

							string optValue = (string)attrNameValuesOption["value"]; //TODO: check value encodings etc.
							bool optSelected = attrNameValuesOption.ContainsKey("selected");

							if (optSelected && optValue != null)
							{
								if (multiple)
								{
									serverSelected.Add(optValue);
								}
								else
								{
									oneSelection = optValue;
									break;
								}
							}

							serverOptions.Add(optValue);
						}

						if (!multiple)
						{
							if (oneSelection != form[name])
							{
								if (oneSelection == null)
									AjaxCallHelper.WriteFormat("__PageForm[\"{0}\"].selectedIndex=-1;\r\n", clientID);
								else
									AjaxCallHelper.WriteSetFieldScript(clientID, oneSelection);
							}
						}
						else
						{
							string[] selections = form.GetValues(name);
							if (selections == null)
								selections = new string[0];
							bool elemWritten = false;

							// Make selections
							for (int j = 0; j < serverSelected.Count; j++)
							{
								if (Array.IndexOf(selections, serverSelected[j]) == -1)
								{
									if (!elemWritten)
									{
										AjaxCallHelper.WriteFormat("o=__PageForm[\"{0}\"].options;\r\n", clientID);
										elemWritten = true;
									}
									AjaxCallHelper.WriteFormat("o[{0}].selected=true;\r\n", serverOptions.IndexOf(serverSelected[j]));
								}
							}

							// Make unselections
							for (int j = 0; j < selections.Length; j++)
							{
								if (!serverSelected.Contains(selections[j]))
								{
									if (!elemWritten)
									{
										AjaxCallHelper.WriteFormat("o=__PageForm[\"{0}\"];\r\n", clientID);
										elemWritten = true;
									}
									AjaxCallHelper.WriteFormat("o[{0}].selected=false;\r\n", serverOptions.IndexOf(selections[j]));
								}
							}
						}
						break;
					#endregion
				}

			}
		}

		/// <summary>
		/// Determines whether the AjaxPanel should treat the given control as a separate
		/// 'html holder' to compare and 'reflect' its html rendering.
		/// </summary>
		/// <remarks>
		/// Only WebControls, excluding RenderedByScriptControls, are considered html holders.
		/// </remarks>
		/// <param name="control"></param>
		/// <returns></returns>
		protected virtual bool IsHtmlHolder(Control control)
		{
			return (control is WebControl && !(control is INonHtmlHolder));
		}

		#region override PutTagOnPageForAjaxCall
		/// <summary>
		/// If this AjaxPanel is inside another AjaxPanel it puts its empty tag on page
		/// using "clientID$rbs" ID, otherwise it throws an exception.
		/// </summary>
		protected override void PutTagOnPageForAjaxCall()
		{
			if (!IsChildOfAjaxPanel(this))
				throw new MagicAjaxException(String.Format("AjaxPanel '{0}' cannot get visible during an AjaxCall. It must be inside an AjaxPanel that is visible for the initial page request.", this.ClientID));

			System.Text.StringBuilder sb = new System.Text.StringBuilder();
			HtmlTextWriter strwriter = new HtmlTextWriter(new System.IO.StringWriter(sb));

			this.RenderBeginTag(strwriter);
			this.RenderEndTag(strwriter);
			string html = sb.ToString();

			AjaxCallHelper.WriteSetHtmlOfElementScript(html, this.ClientID + "$rbs");
		}
		#endregion

		#region RenderByScript
		/// <summary>
		/// It scans child controls for added, removed or altered controls and sends
		/// the appropriate javascript to the client.
		/// </summary>
		protected override void RenderByScript()
		{
			if (IsPageNoStoreMode)
			{
				LoadControlState();
			}
			else
			{
				InitValidators();
			}

			System.Text.StringBuilder sb = new System.Text.StringBuilder();
			HtmlTextWriter litewriter = new HtmlTextWriter(new System.IO.StringWriter(sb));

			System.Text.StringBuilder sbFull = new System.Text.StringBuilder();
			HtmlTextWriter fullwriter = new HtmlTextWriter(new System.IO.StringWriter(sbFull));

			StringBuilder sbLiteral = new StringBuilder();
			HtmlTextWriter literalwriter = new HtmlTextWriter(new System.IO.StringWriter(sbLiteral));

			// To be used in 'NoStore' mode to continue the rendering of control tree.
			HtmlTextWriter nullWriter = new HtmlTextWriter(System.IO.TextWriter.Null);

			bool allControlsAreNew = (Controls.Count == _addedControls.Count);

			if (!this.IsRenderedOnPage || allControlsAreNew || _controlState == null)
			{
				// Render all the controls in a single html rendering.

				for (int i = 0; i < Controls.Count; i++)
				{
					Control con = Controls[i];

					// Put inside span tag only html holder controls
					bool isHtmlHolder = IsHtmlHolder(con);

					if (isHtmlHolder)
					{
						fullwriter.WriteBeginTag("span");
						fullwriter.WriteAttribute("id", GetAjaxElemID(con));
						fullwriter.WriteAttribute("name", "__ajaxmark");
						fullwriter.Write(HtmlTextWriter.TagRightChar);

						literalwriter.WriteBeginTag("span");
						literalwriter.WriteAttribute("id", GetAjaxElemID(con));
						literalwriter.WriteAttribute("name", "__ajaxmark");
						literalwriter.Write(HtmlTextWriter.TagRightChar);
					}

					ExtendedRenderControl(con, fullwriter, litewriter, true);
					string html = sb.ToString();

					if (isHtmlHolder)
					{
						fullwriter.WriteEndTag("span");
						literalwriter.WriteEndTag("span");
						_controlHtmlFingerprints[con] = Util.GetFingerprint(html);
					}
					else
					{
						literalwriter.Write(html);
					}

					sb.Length = 0;
				}

				if (sbFull.Length > 0)
				{
					string html = sbFull.ToString();
					AjaxCallHelper.WriteSetHtmlOfElementScript(html, ClientID);
				}

				if (_controlState != null)
					_controlState.LiteralFingerprint = Util.GetFingerprint(sbLiteral.ToString());
			}
			else
			{
				// Increase script writing level because we want literal content to
				// be rendered before the child controls renderings
				AjaxCallHelper.IncreaseWritingLevel();

				foreach (Control con in _removedControls)
				{
					_controlHtmlFingerprints.Remove(con);
				}

				for (int i = 0; i < Controls.Count; i++)
				{
					Control con = Controls[i];

					// Only html holder controls are inside a span tag
					bool isHtmlHolder = IsHtmlHolder(con);

					if (isHtmlHolder)
					{
						literalwriter.WriteBeginTag("span");
						literalwriter.WriteAttribute("id", GetAjaxElemID(con));
						literalwriter.WriteAttribute("name", "__ajaxmark");
						literalwriter.Write(HtmlTextWriter.TagRightChar);
						literalwriter.WriteEndTag("span");
					}

					string html;
					sbFull.Length = sb.Length = 0;

					if (isHtmlHolder && _addedControls.Contains(con))
					{
						// It's a new control, create it on the client at the appropriate place.
						ExtendedRenderControl(con, fullwriter, litewriter, true);

						html = sbFull.ToString();
						AjaxCallHelper.WriteSetHtmlOfElementScript(html, GetAjaxElemID(con));

						_controlHtmlFingerprints[con] = Util.GetFingerprint(sb.ToString());
					}
					else
					{
						if (IsPageNoStoreMode)
						{
							// Any scripts of child controls should be after the scripts
							// of parent controls, so increase the script writing level.
							AjaxCallHelper.IncreaseWritingLevel();
							ExtendedRenderControl(con, nullWriter, litewriter);
							AjaxCallHelper.DecreaseWritingLevel();
						}
						else
							ExtendedRenderControl(con, litewriter);

						html = sb.ToString();

						if (isHtmlHolder)
						{
							string htmlFingerprint = Util.GetFingerprint(html);
							if (htmlFingerprint == (string)_controlHtmlFingerprints[con])
							{
								// Its html rendering fingerprint is the same, ignore the
								// html and "reflect" the form elements that it contains
								// on client's page if they're different.
								ReflectUpdatedFormValues(html);
							}
							else
							{
								// Its html rendering fingerprint has changed,
								// "reflect" its html on client.
								ExtendedWriteSetHtmlOfElementScript(html, GetAjaxElemID(con));
								_controlHtmlFingerprints[con] = htmlFingerprint;
							}
						}
						else
						{
							literalwriter.Write(html);
						}

						if (IsPageNoStoreMode)
						{
							AjaxCallHelper.MergeUpperWritingLevelsWithCurrent();
						}
					}
				}

				AjaxCallHelper.DecreaseWritingLevel();

				string literalhtml = sbLiteral.ToString();
				string literalFingerprint = Util.GetFingerprint(literalhtml);
				if (literalFingerprint == _controlState.LiteralFingerprint)
				{
					// Its html rendering fingerprint is the same, ignore the
					// html and "reflect" the form elements that it contains
					// on client's page if they're different.
					ReflectUpdatedFormValues(literalhtml);
				}
				else
				{
					ExtendedWriteSetHtmlOfElementScript(literalhtml, this.ClientID);
					_controlState.LiteralFingerprint = Util.GetFingerprint(literalhtml);
				}

				AjaxCallHelper.MergeUpperWritingLevelsWithCurrent();
			}

			_addedControls.Clear();
			_removedControls.Clear();

			if (IsPageNoStoreMode)
				SaveControlState();
		}
		#endregion

		#region OnContentUpdated
		protected virtual void OnContentUpdated(AjaxPanelUpdatedEventArgs e)
		{
			if (ContentUpdated != null)
				ContentUpdated(this, e);
		}
		#endregion

		#endregion

		#region Private Methods

		#region ExtendedWriteSetHtmlOfElementScript
		/// <summary>
		/// Calls the AJAXCbo.ExtendedSetHtmlOfElement function on the client.
		/// </summary>
		/// <remarks>
		/// It's used by RenderByScript method.
		/// 
		/// AjaxPanel doesn't include RenderedByScriptControl controls in the html rendering,
		/// to reduce the size of the javascript script sent to clients.
		/// AJAXCbo.ExtendedSetHtmlOfElement finds the RenderedByScriptControl controls
		/// that are missing from the html rendering, gets them from the page of the client
		/// and adds them in the appropriate place in the html rendering.
		/// </remarks>
		/// <param name="html">The html rendering of the control</param>
		/// <param name="elementID">The span element id that the control belongs to.</param>
		private void ExtendedWriteSetHtmlOfElementScript(string html, string elementID)
		{
			AjaxCallHelper.Write(String.Format("AJAXCbo.ExtendedSetHtmlOfElement({0},\"{1}\");\r\n", AjaxCallHelper.EncodeString(html), elementID));
		}
		#endregion

		#region ExtendedRenderControl
		/// <summary>
		/// It renders the control but without including the html rendering of
		/// any RenderedByScriptControl controls.
		/// </summary>
		/// <remarks>
		/// It's used by RenderByScript method.
		/// </remarks>
		/// <param name="control"></param>
		/// <param name="litewriter"></param>
		private void ExtendedRenderControl(Control control, HtmlTextWriter litewriter)
		{
			RenderStartEventHandler renderStart = new RenderStartEventHandler(control_RenderStart_Abort);

			ArrayList list = FindRenderedByScriptControls(control);
			bool[] visibleProps = new bool[list.Count];
			bool[] monitorProps = new bool[list.Count];

			for (int i = 0; i < list.Count; i++)
			{
				RenderedByScriptControl con = list[i] as RenderedByScriptControl;
				con.RenderStart += renderStart;
				visibleProps[i] = con.Visible;
				monitorProps[i] = con.MonitorVisibilityState;
				con.MonitorVisibilityState = false;
				con.Visible = true;
			}

			control.RenderControl(litewriter);

			for (int i = 0; i < list.Count; i++)
			{
				RenderedByScriptControl con = list[i] as RenderedByScriptControl;
				con.RenderStart -= renderStart;
				con.Visible = visibleProps[i];
				con.MonitorVisibilityState = monitorProps[i];
			}

			list.Clear();
		}
		#endregion

		#region control_RenderStart_Abort
		/// <summary>
		/// It's part of the ExtendedRenderControl(Control control) method. It replaces the html rendering
		/// of a RenderedByScriptControl with a Span tag named "__ajaxmark".
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void control_RenderStart_Abort(object sender, RenderStartEventArgs e)
		{
			RenderedByScriptControl con = (RenderedByScriptControl)sender;
			e.Writer.Write("<span id=\"{0}$rbs\" name=\"__ajaxmark\"></span>", con.ClientID);
			e.AbortRendering = true;
		}
		#endregion

		#region ExtendedRenderControl
		private void ExtendedRenderControl(Control control, HtmlTextWriter fullwriter, HtmlTextWriter litewriter)
		{
			ExtendedRenderControl(control, fullwriter, litewriter, false);
		}
		#endregion

		#region ExtendedRenderControl
		/// <summary>
		/// It produces the full rendering of the control and the rendering without
		/// including the html rendering of any RenderedByScriptControl controls.
		/// </summary>
		/// <remarks>
		/// It's used by RenderByScript method.
		/// </remarks>
		/// <param name="control"></param>
		/// <param name="fullwriter">The writer to use for the full rendering</param>
		/// <param name="litewriter">The writer to use for the rendering without any RenderedByScriptControl controls</param>
		private void ExtendedRenderControl(Control control, HtmlTextWriter fullwriter, HtmlTextWriter litewriter, bool disableScriptRendering)
		{
			ExtendedTextWriter extTextWriter = new ExtendedTextWriter(fullwriter, litewriter, disableScriptRendering);
			RenderStartEventHandler renderStart = new RenderStartEventHandler(extTextWriter.IScriptWriter_RenderStart);
			EventHandler renderEnd = new EventHandler(extTextWriter.IScriptWriter_RenderEnd);

			ArrayList list = FindRenderedByScriptControls(control);
			bool[] visibleProps = new bool[list.Count];
			bool[] monitorProps = new bool[list.Count];

			for (int i = 0; i < list.Count; i++)
			{
				RenderedByScriptControl con = list[i] as RenderedByScriptControl;
				con.RenderStart += renderStart;
				con.RenderEnd += renderEnd;
				if (!con.Visible)
					con.SkipRendering = true;
				visibleProps[i] = con.Visible;
				monitorProps[i] = con.MonitorVisibilityState;
				con.MonitorVisibilityState = false;
				con.Visible = true;
			}

			HtmlTextWriter extwriter = new HtmlTextWriter(extTextWriter);
			control.RenderControl(extwriter);

			for (int i = 0; i < list.Count; i++)
			{
				RenderedByScriptControl con = list[i] as RenderedByScriptControl;
				con.RenderStart -= renderStart;
				con.RenderEnd -= renderEnd;
				con.Visible = visibleProps[i];
				con.MonitorVisibilityState = monitorProps[i];
				con.SkipRendering = false;
			}

			list.Clear();
		}
		#endregion

		#region Validators
		/// <summary>
		/// Clears 'display' and 'visibility' styles from all the validators.
		/// </summary>
		private void InitValidators()
		{
			ArrayList validators = Util.GetChildControlsOfType(this, typeof(BaseValidator), typeof(RenderedByScriptControl), true);
			foreach (BaseValidator validator in validators)
			{
				validator.Style.Remove("display");
				validator.Style.Remove("visibility");
			}

		}

		private void DisableClientValidators()
		{
			ArrayList validators = Util.GetChildControlsOfType(this, typeof(BaseValidator), typeof(RenderedByScriptControl), true);
			foreach (BaseValidator validator in validators)
			{
				validator.EnableClientScript = false;
			}
		}
		#endregion

		#region FindRenderedByScriptControls
		/// <summary>
		/// It returns an ArrayList of all the RenderedByScriptControl controls that
		/// the supplied control contains.
		/// </summary>
		/// <remarks>
		/// It's used by the ExtendedRenderControl methods.
		/// </remarks>
		/// <param name="control"></param>
		/// <returns></returns>
		private ArrayList FindRenderedByScriptControls(Control control)
		{
			ArrayList list = new ArrayList();

			if (control is RenderedByScriptControl)
			{
				list.Add(control);
			}
			else if (control.Visible)
			{
				for (int i = 0; i < control.Controls.Count; i++)
					list.AddRange(FindRenderedByScriptControls(control.Controls[i]));
			}

			return list;
		}
		#endregion

		#region GetNextExistingElement
		/// <summary>
		/// It returns the next control that is available at the page of the client
		/// so that a new control can be added before it.
		/// </summary>
		/// <remarks>
		/// It's used by the RenderByScript method.
		/// 
		/// It checks to see if the html rendering of the control is stored to determine
		/// whether the control exists on the page or not.
		/// </remarks>
		/// <param name="start">The index position to start searching</param>
		/// <returns></returns>
		private string GetNextExistingElement(int start)
		{
			for (int i = start + 1; i < Controls.Count; i++)
			{
				Control con = Controls[i];

				if (!_addedControls.Contains(con) && _controlHtmlFingerprints[con] != null)
					return GetAjaxElemID(con);
			}

			return null;
		}
		#endregion

		#region IsChildOfAjaxPanel
		/// <summary>
		/// Returns true if the given control is contained inside an AjaxPanel
		/// </summary>
		/// <param name="control"></param>
		/// <returns></returns>
		private bool IsChildOfAjaxPanel(Control control)
		{
			if (control.Parent == null || control.Parent == control.Page)
				return false;

			if (control.Parent is AjaxPanel)
				return true;
			else
				return IsChildOfAjaxPanel(control.Parent);
		}
		#endregion

		#region FindUniqueID
		/// <summary>
		/// Finds an ID that no other AjaxPanel's child control has.
		/// </summary>
		/// <returns></returns>
		private string FindUniqueID()
		{
			string prefix = "_ajaxctl";
			string id;
			for (int num = 0; ; num++)
			{
				id = prefix + num;
				int i;
				for (i = 0; i < this.Controls.Count; i++)
					if (this.Controls[i].ID == id)
						break;

				if (i == this.Controls.Count)
					break;
			}

			return id;
		}
		#endregion

		#region GetAjaxElemID
		/// <summary>
		/// It returns the Span tag id that contains the supplied control.
		/// </summary>
		/// <param name="control"></param>
		/// <returns></returns>
		private string GetAjaxElemID(Control control)
		{
			return control.ClientID + "$ajaxdest";
		}
		private string GetAjaxElemID(string clientID)
		{
			return clientID + "$ajaxdest";
		}
		#endregion

		#endregion

		#region Private Class ControlCollectionState
		private class ControlCollectionState
		{
			private string _literalFingerprint;
			private SortedList _controlHtmlFingerprints;
			private AjaxPanel _owner;

			/// <summary>
			/// Default ctor
			/// </summary>
			public ControlCollectionState(AjaxPanel owner)
			{
				_owner = owner;
				_controlHtmlFingerprints = new SortedList();
			}

			/// <summary>
			/// Constructs a new ControlCollectionState with a list of controlHtmlFingerprints
			/// </summary>
			/// <param name="controlHtmlFingerprints"></param>
			public ControlCollectionState(string literalFingerprint, SortedList controlHtmlFingerprints, AjaxPanel owner)
			{
				_owner = owner;
				_literalFingerprint = literalFingerprint;
				_controlHtmlFingerprints = controlHtmlFingerprints;
			}

			/// <summary>
			/// Loads the ControlCollectionState from the hidden form element
			/// </summary>
			/// <param name="panelClientID"></param>
			/// <returns></returns>
			public static ControlCollectionState LoadState(string panelClientID, AjaxPanel owner)
			{
				string panelKey = GetControlFingerprintsField(panelClientID);
				string panelControlFingerprints = HttpContext.Current.Request.Form[panelKey];
				if (panelControlFingerprints != null)
				{
					SortedList controlHtmlFingerprints = new SortedList();
					string literalFingerprint = null;

					if (panelControlFingerprints != String.Empty)
					{
						string[] namevaluepairs = panelControlFingerprints.Split(';');
						if (owner.MagicAjaxContext.Configuration.CompareMode == MagicAjax.Configuration.OutputCompareMode.FullHtml)
						{
							literalFingerprint = UnicodeEncoding.Default.GetString(Convert.FromBase64String(namevaluepairs[0]));
						}
						else
						{
							literalFingerprint = namevaluepairs[0];
						}

						for (int i = 1; i < namevaluepairs.Length; i++)
						{
							string namevaluepair = namevaluepairs[i];
							string[] namevalue = namevaluepair.Split('#');

							if (owner.MagicAjaxContext.Configuration.CompareMode == MagicAjax.Configuration.OutputCompareMode.FullHtml)
							{
								controlHtmlFingerprints.Add(namevalue[0], UnicodeEncoding.Default.GetString(Convert.FromBase64String(namevalue[1])));
							}
							else
							{
								controlHtmlFingerprints.Add(namevalue[0], namevalue[1]);
							}
						}
					}

					return new ControlCollectionState(literalFingerprint, controlHtmlFingerprints, owner);
				}
				else
				{
					return null;
				}
			}

			/// <summary>
			/// Returns the ClientID of the hidden field containing the Control fingerprints for given panel
			/// </summary>
			/// <param name="panelClientID"></param>
			/// <returns></returns>
			public static string GetControlFingerprintsField(string panelClientID)
			{
				return string.Format("__CONTROL_FINGERPRINTS_{0}", panelClientID);
			}

			public string LiteralFingerprint
			{
				get { return _literalFingerprint; }
				set { _literalFingerprint = value; }
			}

			public System.Collections.SortedList ControlHtmlFingerprints
			{
				get { return _controlHtmlFingerprints; }
			}

			public void SetControlIDs(Hashtable controlHtmlFingerprints)
			{
				_controlHtmlFingerprints.Clear();
				foreach (Control con in controlHtmlFingerprints.Keys)
					_controlHtmlFingerprints.Add(con.ClientID, (string)controlHtmlFingerprints[con]);
			}

			/// <summary>
			/// Saves the ControlState fingerprints.
			/// When in AjaxCall mode, creates SetFieldScript to set hidden field when last Control fingerprints were changed.
			/// </summary>
			/// <param name="panelClientID"></param>
			/// <param name="page"></param>
			public void Save(string panelClientID, Page page)
			{
				System.Text.StringBuilder sbuilder = new System.Text.StringBuilder();

				// Put the literal content first, before the control/fingerprint pairs
				if (_owner.MagicAjaxContext.Configuration.CompareMode == MagicAjax.Configuration.OutputCompareMode.FullHtml)
				{
					sbuilder.Append(Convert.ToBase64String(UnicodeEncoding.Default.GetBytes(LiteralFingerprint)));
				}
				else
				{
					sbuilder.Append(LiteralFingerprint);
				}

				foreach (string key in _controlHtmlFingerprints.Keys)
				{
					sbuilder.Append(';');

					if (_owner.MagicAjaxContext.Configuration.CompareMode == MagicAjax.Configuration.OutputCompareMode.FullHtml)
					{
						sbuilder.AppendFormat("{0}#{1}", key, Convert.ToBase64String(UnicodeEncoding.Default.GetBytes((string)_controlHtmlFingerprints[key])));
					}
					else
					{
						sbuilder.AppendFormat("{0}#{1}", key, _controlHtmlFingerprints[key]);
					}
				}

				string serializedPanelFingerprints = sbuilder.ToString();
				string panelKey = GetControlFingerprintsField(panelClientID);

				if (_owner.MagicAjaxContext.IsAjaxCallForPage(_owner.Page))
				{
					//if ajax callback, generate javascript to set panelfingerprints's hidden field value
					if (HttpContext.Current.Request.Form[panelKey] != serializedPanelFingerprints)
					{
						AjaxCallHelper.WriteSetFieldScript(panelKey, serializedPanelFingerprints);
					}
				}
				else if (_owner.MagicAjaxContext.IsBrowserSupported)
				{
					// The hidden field is already registered at OnLoad event.
					// Set its value by javascript.
					page.RegisterStartupScript(panelKey + "VALUESET", String.Format("<script type='text/javascript'>AJAXCbo.SetFieldIfEmpty(\"{0}\",{1});</script>", panelKey, AjaxCallHelper.EncodeString(serializedPanelFingerprints)));
				}

			}
		}
		#endregion

		#region Private Class ExtendedTextWriter
		/// <summary>
		/// It enables simultaneous rendering of full rendering and rendering without
		/// including the html rendering of any RenderedByScriptControl controls.
		/// </summary>
		/// <remarks>
		/// It's used by ExtendedRenderControl(Control, HtmlTextWriter, HtmlTextWriter).
		/// </remarks>
		private class ExtendedTextWriter : System.IO.TextWriter, IScriptRenderingDisabler
		{
			private HtmlTextWriter fullwriter, litewriter;
			private bool onlyFullRender = false;
			private bool _disableScriptRendering = false;

			public override System.Text.Encoding Encoding
			{
				get { return null; }
			}

			public void IScriptWriter_RenderStart(object sender, RenderStartEventArgs e)
			{
				RenderedByScriptControl con = (RenderedByScriptControl)sender;
				Write("<span id=\"{0}$rbs\" name=\"__ajaxmark\">", con.ClientID);
				onlyFullRender = true;
			}

			public void IScriptWriter_RenderEnd(object sender, System.EventArgs e)
			{
				onlyFullRender = false;
				Write("</span>");
			}

			public ExtendedTextWriter(HtmlTextWriter fullwriter, HtmlTextWriter litewriter, bool disableScriptRendering)
				: base(null)
			{
				this.fullwriter = fullwriter;
				this.litewriter = litewriter;
				_disableScriptRendering = disableScriptRendering;
			}

			#region Overrides of TextWriter
			public override void Write(bool value)
			{
				fullwriter.Write(value);
				if (!onlyFullRender)
					litewriter.Write(value);
			}
			public override void Write(char value)
			{
				fullwriter.Write(value);
				if (!onlyFullRender)
					litewriter.Write(value);
			}
			public override void Write(char[] buffer)
			{
				fullwriter.Write(buffer);
				if (!onlyFullRender)
					litewriter.Write(buffer);
			}
			public override void Write(char[] buffer, int index, int count)
			{
				fullwriter.Write(buffer, index, count);
				if (!onlyFullRender)
					litewriter.Write(buffer, index, count);
			}
			public override void Write(decimal value)
			{
				fullwriter.Write(value);
				if (!onlyFullRender)
					litewriter.Write(value);
			}
			public override void Write(double value)
			{
				fullwriter.Write(value);
				if (!onlyFullRender)
					litewriter.Write(value);
			}
			public override void Write(float value)
			{
				fullwriter.Write(value);
				if (!onlyFullRender)
					litewriter.Write(value);
			}
			public override void Write(int value)
			{
				fullwriter.Write(value);
				if (!onlyFullRender)
					litewriter.Write(value);
			}
			public override void Write(long value)
			{
				fullwriter.Write(value);
				if (!onlyFullRender)
					litewriter.Write(value);
			}
			public override void Write(object value)
			{
				fullwriter.Write(value);
				if (!onlyFullRender)
					litewriter.Write(value);
			}
			public override void Write(string format, object arg0)
			{
				fullwriter.Write(format, arg0);
				if (!onlyFullRender)
					litewriter.Write(format, arg0);
			}
			public override void Write(string format, object arg0, object arg1)
			{
				fullwriter.Write(format, arg0, arg1);
				if (!onlyFullRender)
					litewriter.Write(format, arg0, arg1);
			}
			public override void Write(string format, object arg0, object arg1, object arg2)
			{
				fullwriter.Write(format, arg0, arg1, arg2);
				if (!onlyFullRender)
					litewriter.Write(format, arg0, arg1, arg2);
			}
			public override void Write(string format, params object[] arg)
			{
				fullwriter.Write(format, arg);
				if (!onlyFullRender)
					litewriter.Write(format, arg);
			}
			public override void Write(string value)
			{
				fullwriter.Write(value);
				if (!onlyFullRender)
					litewriter.Write(value);
			}
			public override void Write(uint value)
			{
				fullwriter.Write(value);
				if (!onlyFullRender)
					litewriter.Write(value);
			}
			public override void Write(ulong value)
			{
				fullwriter.Write(value);
				if (!onlyFullRender)
					litewriter.Write(value);
			}
			#endregion

			#region IScriptRenderingDisabler Members

			public bool DisableScriptRendering
			{
				get
				{
					return _disableScriptRendering;
				}
			}

			#endregion
		}
		#endregion
	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Web Developer
Greece Greece
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions