Tuesday, July 28, 2009

Check for Content in Control

While working on a project recently I had a requirement for ContentPlaceHolder in my Master Page that was wrapped in a container that I only wanted to render if the ContentPlaceHolder was actually being populated by a page inheriting the Master. The container was a Firefox-style notice bar that renders at the top of the site with a simple message and a close button. The message area of the notice bar contains the ContentPlaceHolder which will govern whether or not the notice bar gets rendered. To achieve this two things must be done:
  1. Check if any controls exist in the ContentPlaceHolder's Controls collection
  2. If controls do exist, iterate over each one and check whether or not it:
    • A) Has a text property which is not whitespace
    • B) Contains controls in its own control collection meeting the latter condition
    • C) Is currently visible
These requirements are all relatively straightforward, and as you may have guessed, recursion comes to the rescue in this scenario. Although this code is ultimately going to be called on a ContentPlaceHolder, it makes more sense (and saves code) if the method accepts a non-specific control object, rather than a ContentPlaceHolder as its parameter.
public static bool IsNonEmptyControl(Control ctrl)
{
 if (!(ctrl is ITextControl) && ctrl.Controls.Count > 0)
 {
  for (int i = 0; i < ctrl.Controls.Count; i++)
  {
   if (IsNonEmptyControl(ctrl.Controls[i]))
    return true;
  }
 }
 else if (ctrl is ITextControl && ctrl.Visible)
 {
  return HasContent(ctrl as ITextControl);
 }
 return false;
}
This method is fairly simple and has one of two paths- immediate result, or recursion leading to a result. We start by checking if we're dealing with a container or a content control. This is a rather arbitrary test, but what this means is it's a control with a non-empty Controls collection or one with a "Text" property. If the control does have children we loop over each child and call IsNonEmptyControl() on it, or if the control has a text property we check the content of that text property with HasContent().
private static bool HasContent(ITextControl itc)
{
 if (itc != null && !string.IsNullOrEmpty(itc.Text) && !IsWhiteSpace(itc.Text))
  return true;
 //else
 return false;
}

private static bool IsWhiteSpace(string s)
{
 for (int i = 0; i < s.Length; i++)
 {
  if (!char.IsWhiteSpace(s[i]))
   return false;
 }
 return true;
}
Both of these methods are pretty obvious- check thatthe text property is not null, not empty, and does not contain whitespace ("\r, \n, " ", etc). And that's all there is to it. The method can be called on any control and will correctly indicate whether or not that control has content. In my case I call it in my Master Page's Page_PreRender event and pass it my ContentPlaceHolder. Now, there is one problem with this method: it doesn't properly address 2C from above- checking whether or not the content is actually visible when we call this from the Master Page on a ContentPlaceHolder. After digging a bit I realized this is actually less intuitive than I would have hoped due to your friend and mine: Page Life Cycle. When calling this method in the Master Page's PreRender event none of the content pages have yet initialized any of their controls from the code/markup so although they are accessible, all control properties are defaulted. In fact, it is not until Page_PreRenderComplete that we are able to see the actual property values of our controls and, unfortunately, the Master Page does not throw this event. I am honestly not sure how to handle this scenario and I would be very interested to know if anyone has some ideas on how to tackle this problem. Tips
  • If you're using C# 3.0 consider changing the signature to IsNonEmptyControl(this Control ctrl) for some Extension Method goodness.
Sources

No comments: