Wensveen's Blog

May 27, 2011

Toggling boilerplate HTML visibility in ASP.NET

Filed under: Uncategorized — Tags: , — wensveen @ 10:23 pm

The problem

When you are writing an aspx page, you write HTML mixed with ASP.NET controls. Sometimes, when some control is not visible, or has no visible (html) output, you may need to hide the html that is containing the control as well.

For example, suppose a document from some CMS may or may not have a Title, or said Title may be empty. Your aspx page might look like this:

..
<h1><cms:FieldControl runat="server" Field="Title" /></h1>
..

Suppose FieldControl is a control that renders the a field of the current document when that field exists and isn’t empty. When Title is empty, you don’t want the <h1> tag to be rendered either.

Solution 1: Placeholders and code-behind

The most straightforward solution to this problem is to place an <asp:Placeholder> control around the <h1>. Then in code-behind, test the FieldControl for output and set the Placeholder visibility accordingly. For example:

<asp:Placeholder runat="server" ID="phTitle">
	<h1><cms:FieldControl runat="server" ID="fcTitle" Field="Title" /></h1>
</asp:Placeholder>

code behind:

protected override void OnPreRender(EventArgs e)
{
	base.OnPreRender(e);
	phTitle.Visible = fcTitle.Visible && !String.IsNullOrEmpty(fcTitle.Text)
}

While there is nothing wrong with this code, it could become cumbersome if you have more than a few of these snippets. There may even be controls that don’t expose a Text property or a similar way to test the control for output.

Solution 2: Introducing the Enclosure control

Borrowing (again) from the Apache Wicket framework, I’ve written a control similar (but not quite the same) to their Enclosure control.

The idea is the same as the Placeholder version, except that you don’t need any code-behind (declarative vs. programmatic). This also makes it suitable for environments where code-behind is discouraged or not allowed, e.g. SharePoint.

The functionality is best shown with an example:

<evil:Enclosure runat="server">
	<div style="border: 1px solid red;">
		Some content.<br />
		Some content.<br />
		Some content.<br />
		Some content.<br />
		<asp:Literal runat="server" ID="ltLiteral" Text="An asp:Literal" /><br />
		Some content.<br />
		Some content.<br />
	</div>
</evil:Enclosure>
<br />
<evil:Enclosure runat="server" AssociatedControlID="ltLiteral">
	<div style="border: 1px solid red;">
		The display of this content, which is in a separate Enclosure, depends on the display of the asp:Literal above.<br />
	</div>
</evil:Enclosure>
<br />
<asp:TextBox runat="server" ID="tbLiteralText" Text="An asp:Literal" />
<asp:CheckBox runat="server" ID="cbLiteralVisible" Text="Literal is visible?" Checked="true" />
<asp:Button runat="server" ID="btnApply" Text="Apply" OnClick="btlApply_Click" />

When the Apply button is clicked, the visibility and text properties of ltLiteral are set (code not shown here for brevity). When this results in ltLiteral not having any output, both Enclosures and their content are hidden. The first Enclosure doesn’t have the AssociatedControlID property set, so it checks the output of all contained controls, in this case only ltLiteral. The second Enclosure also checks the output of ltLiteral, because it is set with the AssociatedControlID property. The AssociatedControlID property is also useful when there are multiple controls inside an Enclosure but you want the visibility to depend on only one control.

Nesting

Enclosures can be nested:

<evil:Enclosure runat="server">
	<ul>
		<li>Some content</li>
		<li>Some content</li>
		<evil:Enclosure runat="server">
			<li><asp:Literal runat="server" ID="ltNestedLiteral1" Text="Literal inside nested enclosure 1" /></li>
		</evil:Enclosure>
		<evil:Enclosure runat="server">
			<li><asp:Literal runat="server" ID="ltNestedLiteral2" Text="Literal inside nested enclosure 2" /></li>
		</evil:Enclosure>
		<li>Some content</li>
	</ul>
</evil:Enclosure>

When either one of the literals is invisible, only the containing Enclosure is hidden (in this case to prevent an empty <li> tag). When both of the literals are invisible, the Enclosure containing the <ul> is also hidden.

The code

So, here’s the code that does the hard work:

public class Enclosure : PlaceHolder
{
	public string AssociatedControlID { get; set; }

	protected override void Render(HtmlTextWriter output)
	{
		// Create a buffer to render to, instead of 'output'
		StringBuilder stringBuilder = new StringBuilder();
		HtmlTextWriter writer = new HtmlTextWriter(new StringWriter(stringBuilder));

		// AssociatedControlID property set? Check the control for output.
		if (!String.IsNullOrEmpty(AssociatedControlID))
		{
			Control control = FindControl(AssociatedControlID) ?? Page.FindControlRecursive(AssociatedControlID);
			if (control != null)
			{
				control.RenderControl(writer);
			}

			// When the selected control has output, render
			if (!String.IsNullOrEmpty(stringBuilder.ToString().Trim()))
			{
				base.Render(output);
			}
		}
		else
		{
			// Check all controls for output

			bool hasContent = false;
			foreach (Control control in Controls)
			{
				int currentLength = stringBuilder.Length;
				control.RenderControl(writer);

				// Ignore LiteralControl instances, because they are (probably) generated by the compiler
				if (!hasContent && !(control is LiteralControl))
				{
					// When the stringBuilder length has increased, control has rendered something.
					hasContent = stringBuilder.Length > currentLength;
				}
			}

			// When the buffer has non-LiteralControl output, write it to the real output. This avoids the
			// need to render controls multiple times (which might degrade performance).
			if (hasContent)
			{
				output.Write(stringBuilder.ToString());
			}
		}
	}
}

The controls that are checked for output, either via the AssociatedControlID property, or when inside an Enclosure without ACID property are rendered to a buffer to test them for output. We don’t need to check any Visible or Text property of the control (Solution 1 required that). It just works!

When ACID is set, the control is found by first checking the current naming container with this.FindControl(..). When this fails, find the control recursively on the page. I’ve written a useful extension method for that:

public static class ControlExtensions
{
	public static Control FindControlRecursive(this Control thisControl, string id)
	{
		Queue<Control> controls = new Queue<Control>();
		controls.Enqueue(thisControl);

		while (controls.Count > 0)
		{
			Control control = controls.Dequeue();

			if (control.ID == id)
			{
				return control;
			}

			foreach (Control child in control.Controls)
			{
				controls.Enqueue(child);
			}
		}
		return null;
	}
}

To prevent stack overflows when the control hierarchy is really deep, I use a Queue of to-be-checked controls. When a control has children, they are placed on the end of the queue. Effectively, this is a breadth-first search through the control hierarchy. A depth-first search can be achieved by pushing the children onto a Stack in reverse order, or by using a LinkedList or some such. This code is inspired on multiple posts around the internet about finding controls recursively. Thank you, internet!

Have fun!

Advertisements

2 Comments »

  1. Great stuff!

    The Enclosure tag is definitly something that i’ll be using in future projects!

    Cheers,
    Erik

    Comment by Erik — June 16, 2011 @ 8:44 am

  2. Nice. I used to love this functionality in Wicket 🙂

    Comment by Rommert — June 16, 2011 @ 12:14 pm


RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Blog at WordPress.com.

%d bloggers like this: