Click here to Skip to main content
15,881,746 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 965K   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.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel;

namespace MagicAjax.UI
{
	#region Public Class RenderStartEventArgs
	/// <summary>
	/// It contains the HtmlTextWriter writer event argument to be used during a
	/// RenderStart event.
	/// </summary>
	public class RenderStartEventArgs : EventArgs
	{
		private bool abortRendering = false;
		private HtmlTextWriter writer;

		public bool AbortRendering
		{
			get { return abortRendering; }
			set { abortRendering = value; }
		}

		public HtmlTextWriter Writer
		{
			get { return writer; }
		}

		public RenderStartEventArgs(HtmlTextWriter writer)
		{
			this.writer = writer;
		}
	}
	#endregion

	/// <summary>
	/// The event handler for a RenderStart event.
	/// </summary>
	public delegate void RenderStartEventHandler(object sender, RenderStartEventArgs e);

	/// <summary>
	///	This control manages its appearance on the page using javascript that sends
	///	to the client during an AjaxCall.
	/// </summary>
	/// <remarks>
	/// It provides the basic functionality for controls like AjaxPanel. It manages
	/// its tag attributes using javascript.
	/// </remarks>
	public abstract class RenderedByScriptControl : AjaxControl, IScriptWriter, INonHtmlHolder
	{
		protected abstract void RenderByScript();

		private bool _isRenderedOnPage = false;
		private bool _monitorVisibilityState = true;
		private string _currentTagHtml = null;
		private bool _isHtmlRendered = false;
		private bool _skipRendering = false;

		public event RenderStartEventHandler RenderStart;
		public event EventHandler RenderEnd;

		public override bool Visible
		{
			get
			{
				return base.Visible;
			}
			set
			{
				if (IsAjaxCall
					&& IsRenderedOnPage
					&& MonitorVisibilityState
					&& value != base.Visible)
					AjaxCallHelper.WriteSetVisibilityOfElementScript(ClientID, value);

				base.Visible = value;
			}
		}

		/// <summary>
		/// Determines whether the RenderedByScriptControl control has been rendered
		/// on the page, either by normal rendering or by script.
		/// </summary>
		/// <remarks>
		/// IsRenderedOnPage is set to true during a normal rendering or a rendering
		/// by script. It is set to false at WriteScript method when the Visible property
		/// is false.
		/// </remarks>
		[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
		public virtual bool IsRenderedOnPage
		{
			get { return _isRenderedOnPage; }
		}

		/// <summary>
		/// Gets or sets whether every time "Visible" property is changed
		/// AjaxCallHelper.WriteSetVisibilityOfElementScript method should be called.
		/// Default is true.
		/// </summary>
		/// <remarks>
		/// If this property is true, every time you change the "Visible" property during a
		/// AjaxCall, the display attribute of the style of the control on the page will
		/// be set to "" or "none" by calling the AjaxCallHelper.WriteSetVisibilityOfElementScript
		/// method.
		/// </remarks>
		[Bindable(false), Category("Behavior"), DefaultValue(true)]
		public bool MonitorVisibilityState
		{
			get { return _monitorVisibilityState; }
			set { _monitorVisibilityState = value; }
		}

		/// <summary>
		/// Returns the tag name of the control.
		/// </summary>
		/// <returns></returns>
		public string GetTagName()
		{
			return TagKey.ToString();
		}

		public RenderedByScriptControl()
		{
			this.ID = "";
		}
		public RenderedByScriptControl(HtmlTextWriterTag tag)
			: base(tag)
		{
			this.ID = "";
		}

		/// <summary>
		/// Defines wether the Render method will skip rendering completely.
		/// </summary>
		/// <remarks>
		/// Mainly used by AjaxPanel for RenderedByScriptControls that have Visible
		/// property set to false. 
		/// </remarks>
		[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
		internal bool SkipRendering
		{
			get { return _skipRendering; }
			set { _skipRendering = value; }
		}

		/// <summary>
		/// It defines whether the RenderedByScriptControl was already rendered during
		/// a normal rendering.
		/// </summary>
		/// <remarks>
		/// The RenderedByScriptControl can be normally rendered by the Render method
		/// even during an AjaxCall (i.e when all the controls of an AjaxPanel is new
		/// and the AjaxPanel renders all of them in a single html rendering).
		/// In this case it's not necessary to send any javascript to the client.
		/// </remarks>
		[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
		protected bool IsHtmlRendered
		{
			get { return _isHtmlRendered; }
		}

		protected override void OnPreRender(EventArgs e)
		{
			if (this.MagicAjaxContext.IsBrowserSupported && !IsChildOfRenderedByScriptControl(this))
			{
				string hiddenStore;

				if (IsPageNoStoreMode)
				{
					// Firefox, when performing a refresh, loads the page but keeps any INPUT
					// field values. Thus, the html of a RenderedByScriptControl is restored
					// as if the page was loaded because of the browser's "Back Button".
					// We cannot avoid this because Firefox also keeps any changes that occured
					// to ViewState, so we need the controls to keep their changes.
					hiddenStore = "__" + this.ClientID + "$RBS_Store";
				}
				else
				{
					// At storing modes we don't mess with the ViewState field, so use
					// a different name for the hidden field at each page request so that
					// it can be reset for Firefox's "Refresh".
					hiddenStore = "__" + this.ClientID + "$RBS_Store" + DateTime.Now.Ticks;
				}

				Page.RegisterHiddenField(hiddenStore, null);
				Page.RegisterArrayDeclaration("RBS_Controls", String.Format("document.getElementById(\"{0}$RBS_Holder\")", this.ClientID));
				Page.RegisterArrayDeclaration("RBS_Controls_Store", String.Format("document.{0}[\"{1}\"]", Util.GetPageFormID(this.Page), hiddenStore));

				// Keep track of last Ajax control, so we can run some additional code after rendering the last Ajax control
				if (!HttpContext.Current.Items.Contains("__LAST_AJAX_CONTROL"))
					HttpContext.Current.Items.Add("__LAST_AJAX_CONTROL", this.ClientID);
				else
					HttpContext.Current.Items["__LAST_AJAX_CONTROL"] = this.ClientID;
			}

			base.OnPreRender(e);
		}

		protected bool IsChildOfRenderedByScriptControl(Control control)
		{
			if (control.Parent == null || control.Parent == control.Page)
				return false;
			else if (control.Parent is RenderedByScriptControl)
				return true;
			else
				return IsChildOfRenderedByScriptControl(control.Parent);
		}

		protected override void OnAjaxCallStart(EventArgs e)
		{
			_isHtmlRendered = false;
			base.OnAjaxCallStart(e);
		}

		/// <summary>
		/// Normal rendering.
		/// </summary>
		/// <remarks>
		/// It raises the RenderStart and RenderEnd events. If the rendering is not
		/// aborted during the RenderStart event, it renders the control and sets
		/// the _isHtmlRendered variable to true.
		/// </remarks>
		/// <param name="writer"></param>
		protected override void Render(HtmlTextWriter writer)
		{
			RenderStartEventArgs eventArgs = new RenderStartEventArgs(writer);
			OnRenderStart(eventArgs);

			if (!eventArgs.AbortRendering && !SkipRendering)
			{
				if (!IsAjaxCall || (writer.InnerWriter is IScriptRenderingDisabler && (writer.InnerWriter as IScriptRenderingDisabler).DisableScriptRendering))
				{
					if (!IsChildOfRenderedByScriptControl(this))
					{
						// Put the html of this control inside a SPAN tag so that it can
						// be saved and restored when the page is loaded from the Browser's cache,
						// i.e. when the back button is pressed.
						writer.Write("<span id='{0}$RBS_Holder'>", this.ClientID);
						base.Render(writer);
						writer.Write("</span>");
					}
					else
						base.Render(writer);

					_isHtmlRendered = true;
					_isRenderedOnPage = true;
				}
				else
				{
					WriteScript();
				}
			}

#if !MEDIUM_TRUST
			if (this.MagicAjaxContext.IsBrowserSupported && !IsChildOfRenderedByScriptControl(this))
			{
				if ((string)HttpContext.Current.Items["__LAST_AJAX_CONTROL"] == this.ClientID)
				{
					// This is the last Ajax control.
					// Reflect added startup-scripts and arrays AFTER reflecting the controls HTML
					if (MagicAjaxContext.Current.IsAjaxCall)
					{
						AjaxCallHelper.HandleArrayDeclares(this.Page, true);
						AjaxCallHelper.HandleClientStartupScripts(this.Page, true);

						bool reflectFingerprints = false;
						// See if one of the _currentScriptFPs is not in _previousScriptFPs
						for (int i = 0; i < AjaxCallHelper._currentScriptFPs.Count && !reflectFingerprints; i++)
						{
							if (!AjaxCallHelper._previousScriptFPs.Contains(AjaxCallHelper._currentScriptFPs[i]))
							{
								// Store scriptblock fingerprints in hidden field
								string allScriptFPs = string.Join(";", (string[])AjaxCallHelper._currentScriptFPs.ToArray(typeof(string)));
								AjaxCallHelper.WriteSetFieldScript("__MAGICAJAX_SCRIPT_FINGERPRINTS", allScriptFPs);
								break;
							}
						}
					}
					else
					{
						// Normal request (i.e. first page request)
						AjaxCallHelper.HandleArrayDeclares(this.Page, false);
						AjaxCallHelper.HandleClientStartupScripts(this.Page, false);

						// Fill hidden field __MAGICAJAX_SCRIPT_FINGERPRINTS using script
						writer.WriteLine("<script type=\"text/javascript\">document.{0}[\"__MAGICAJAX_SCRIPT_FINGERPRINTS\"].value={1};</script>", Util.GetPageFormID(this.Page), AjaxCallHelper.EncodeString(string.Join(";", (string[])AjaxCallHelper._currentScriptFPs.ToArray(typeof(string)))));
					}
				}
			}
#endif

			OnRenderEnd(EventArgs.Empty);
		}

		/// <summary>
		/// It stores the tag html for later checking.
		/// </summary>
		/// <param name="writer"></param>
		public override void RenderBeginTag(HtmlTextWriter writer)
		{
			System.Text.StringBuilder sb = new System.Text.StringBuilder();
			HtmlTextWriter strwriter = new HtmlTextWriter(new System.IO.StringWriter(sb));
			base.RenderBeginTag(strwriter);
			_currentTagHtml = sb.ToString();

			base.RenderBeginTag(writer);
		}

		/// <summary>
		/// If the tag html of the control is changed, send the attributes using javascript.
		/// </summary>
		public virtual void WriteScript()
		{
			if (!this.IsInAjaxScope)
				return;

			if (!this.Visible)
			{
				if (_isRenderedOnPage)
				{
					// Verify that it is indeed rendered on page
					_isRenderedOnPage = (GetTopInvisibleControl(this) is RenderedByScriptControl);
				}
				return;
			}

			// If there was a normal rendering, javascript is not needed
			if (IsHtmlRendered)
				return;

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

			if (!IsRenderedOnPage)
			{
				// Put the control's tag on page
				PutTagOnPageForAjaxCall();
			}
			else
			{
				// Take care of the tag of the control
				// TODO: Take care of the tag for 'NoStore' mode too.

				if (!IsPageNoStoreMode)
				{
					base.RenderBeginTag(strwriter);
					string html = sb.ToString();
					sb.Length = 0;

					if (_currentTagHtml != html)
					{
						AjaxCallHelper.WriteSetAttributesOfControl(ClientID, FormatAttributes(html));
						_currentTagHtml = html;
					}
				}
			}

			this.RenderByScript();

			_isRenderedOnPage = true;
		}

		/// <summary>
		/// Called when the control gets visible during AjaxCall and is not already
		/// rendered on page.
		/// </summary>
		/// <remarks>
		/// RenderedByScriptControl's method throws an exception because it doesn't know
		/// where to put the tag. An inheriting control should override this method
		/// to provide the appropriate functionality.
		/// </remarks>
		protected virtual void PutTagOnPageForAjaxCall()
		{
			throw new MagicAjaxException(String.Format("RenderByScript control '{0}' cannot get visible during an AjaxCall.", this.ClientID));
		}

		/// <summary>
		/// Gets the top control that has its Visible property set to false.
		/// </summary>
		/// <remarks>
		/// It's used to determine whether the control is rendered on page or not.
		/// </remarks>
		/// <param name="control"></param>
		/// <returns></returns>
		protected Control GetTopInvisibleControl(Control control)
		{
			if (control.Parent == null || control.Parent.Visible)
				return control;
			else
				return GetTopInvisibleControl(control.Parent);
		}

		protected virtual void OnRenderStart(RenderStartEventArgs eventArgs)
		{
			if (RenderStart != null)
				RenderStart(this, eventArgs);
		}

		protected virtual void OnRenderEnd(EventArgs e)
		{
			if (RenderEnd != null)
				RenderEnd(this, e);
		}

		/// <summary>
		/// Finds the next visible control of the control collection of this
		/// control's parent that has its ID attribute set.
		/// </summary>
		/// <returns></returns>
		private Control FindNextVisibleSibling()
		{
			for (int i = Parent.Controls.IndexOf(this) + 1; i < Parent.Controls.Count; i++)
			{
				Control con = Parent.Controls[i];

				if (con.Visible && con.ID != null)
					return con;
			}

			return null;
		}

		/// <summary>
		/// It finds the attributes from the html of the control's tag, and formats them
		/// so that they are send by AjaxCallHelper.WriteSetAttributesOfControl method.
		/// </summary>
		/// <param name="html">The html tag of the control</param>
		/// <returns></returns>
		private string FormatAttributes(string html)
		{
			System.Text.StringBuilder sb = new System.Text.StringBuilder(html);
			System.Text.StringBuilder attribs = new System.Text.StringBuilder();
			int mode = 0;

			for (int i = 0; i < sb.Length; i++)
			{
				char ch = sb[i];
				if (ch == '>') break;

				switch (mode)
				{
					case 0:
						if (ch == ' ') mode++;
						break;
					case 1:
						switch (ch)
						{
							case '\"':
								mode++;
								break;
							case ' ':
								attribs.Append('|');
								break;
							default:
								attribs.Append(ch);
								break;
						}
						break;
					case 2:
						if (ch == '\"')
							mode--;
						else
							attribs.Append(ch);
						break;
				}
			}

			return attribs.ToString();
		}

		/// <summary>
		/// Interface used with a HtmlTextWriter to define whether to disable
		/// script rendering and do a normal rendering or not.
		/// </summary>
		protected interface IScriptRenderingDisabler
		{
			bool DisableScriptRendering
			{
				get;
			}
		}
	}
}

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