home
 
email me   home      
My Resume
My Recent Sites
My Project Examples
Etc...













How to Create a Custom .NET Marquee Server Control
Saturday, April 16, 2005

Background

I'm currently working on a web-based customer support (trouble-ticket) system, and one of the requirements was that I have some sort of "alert" system, where an administrator can create a list of alerts for the users of the system to quickly see. My idea was to scroll them horizontally across the top of the pages, just under the persistant user menu.

I did some hunting around and found the best course of action would be to use an HTML "marquee" element which (you guessed it!) scrolls text up, down, left or right on the screen for you with very little work involved. Well now, .NET, while having a wonderful library of ready-to-go controls, unfortunately doesn't have an HtmlControl called "marquee". So, it was up to me to create my own custom WebControl (Server Control) that would handle the job. The beauty of a Server Control is that it is totally portable- I can just drop the assembly (.dll) in the web application's /bin directory, create a reference in the Aspx page, and be ready to go. Updating it from that point on is also very easy, with just a quick re-compilation of the solution, or simply FTP'ing the updated assembly to the running web application. There are no interface files to manage like a User Control, all of the output is handled in the Render method, which is also a very attractive reason to invest the time in Server Controls instead of User Controls.

I'm already a veteran with building server controls, so I quickly went into my scottn.WebControls project and added a new class called "Marquee". The Marquee has 12 simple properties which for the most part replicate the actual HTML attributes of the marquee element:

  • Enabled : (Bool) Determines whether this marquee is visible or invisible on the parent page.
  • ScrollDirection : (Enum - MarqueeScrollDirection) Sets which direction the marquee should scroll (up, down, left, right).
  • ScrollBehavior : (Enum - MarqueeScrollBehavior) Sets what type of scrolling (scroll, slide, alternate).
  • ScrollDelay : (int) Sets how long to delay between each jump.
  • ScrollAmount : (int) Sets the speed of the scrolling.
  • Loop : (int) How many times to loop. If null, loop = infinite.
  • BGColor : (string) Sets the background color of the marquee.
  • HSpace : (string) Sets the horizontal space to the left and right of the marquee.
  • VSpace : (string) Sets the vertical space at the top and bottom of the marquee.
  • MarqueeStyle : (string) Sets the "style" property of the marquee.
  • HtmlDecode : (bool) Determines whether to Server.HtmlDecode each item prior to rendering it.
  • Items : (System.Collections.Specialized.StringCollection) This is the collection of items the marquee will scroll.

Notes and Considerations

Because the marquee is a very simple control that repeats a list of single items, I chose to populate it's contents from a StringCollection property called "Items". It could very well be changed to a data source property that could accept a DataTable, DataSet or DataView. Right now I am populating the control from an XML file because I felt it was a waste of time to build SQL stored procedures/table for only 2 possible items, a "key" and a "value" (the key for preventing duplicate items). Plus, the XML file could easily be manually modified and uploaded to the website for instant-update of alerts. The only problem I can see so far with using an XML file as a data source is that you cannot store certain characters, they must be encoded, for example the ">" and "<". Any HTML content stored, for example links and so forth, must be HTML-encoded first, which is why I have the "HtmlDecode" property on the control, to account for this nuisance.

Populating the StringCollection property from an XML config file

<?xml version="1.0" encoding="utf-8"?>
<Alerts>
  <Alert name="Test1" value="&lt;a href=#&gt;testing a link&lt;/a&gt;" />
  <Alert name="Test2" value="Testing second dynamic value!" />
  <Alert name="Test3" value="Testing third dynamic value!!" />
  <Alert name="Test4" value="Testing fourth dynamic value!!" />
  <Alert name="Test5" value="Testing fifth dynamic value!!" />
  <Alert name="Test6" value="Testing sixth dynamic value!!" />
  <Alert name="Test7" value="Testing seventh dynamic value!!!" />
  <Alert name="Test8" value="Testing eighth dynamic value!!!" />
</Alerts>

Building the Marquee Server Control

  
using System;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Web.UI;
using System.Web.UI.Design;
using System.ComponentModel.Design;

namespace scottn.Web.WebControls
{
 #region "Enums"
 public enum MarqueeScrollDirection 
 {
  Left,
  Right,
  Up,
  Down
 }
 public enum MarqueeScrollBehavior 
 {
  Scroll,
  Slide,
  Alternate
 }
 #endregion
 
 /// <summary>
 /// Marquee is a scrolling marquee that allows string items to be scrolled up/down or left/right across a webpage.
 /// </summary>
 [ToolboxData("<{0}:Marquee runat=server>")]
 [Designer("MarqueeDesigner")]
 public class Marquee : System.Web.UI.WebControls.WebControl,INamingContainer
 {
  #region "Properties"
  private StringCollection _Items = new StringCollection();
  /// <summary>
  /// Enabled determines whether this marquee is visible or invisible on the parent page.
  /// </summary>
  public override bool Enabled 
  {
   get
   {
    return (bool)ViewState["Enabled"];
   }
   set
   {
    ViewState["Enabled"] = value;
   }
  }
  /// <summary>
  /// ScrollDirection sets which direction the marquee should scroll (up, down, left, right)
  /// </summary>
  public MarqueeScrollDirection ScrollDirection
  {
   get
   {
    return (MarqueeScrollDirection)ViewState["Direction"];
   }
   set
   {
    ViewState["Direction"] = value;
   }
  }
  /// <summary>
  /// ScrollBehavior sets what type of scrolling (scroll, slide, alternate)
  /// </summary>
  public MarqueeScrollBehavior ScrollBehavior
  {
   get
   {
    return (MarqueeScrollBehavior)ViewState["Behavior"];
   }
   set
   {
    ViewState["Behavior"] = value;
   }
  }
  /// <summary>
  /// ScrollDelay sets how long to delay between each jump
  /// </summary>
  public int ScrollDelay
  {
   get
   {
    return (int)ViewState["ScrollDelay"];
   }
   set
   {
    ViewState["ScrollDelay"] = value;
   }
  }
  /// <summary>
  /// ScrollAmount sets the speed of the scrolling. Marquee moves the content by 
  /// displaying the content, then delaying for some short period of time, then 
  /// displaying the content again in a new position. SCROLLAMOUNT sets the size 
  /// in pixels of each jump. A higher value for SCROLLAMOUNT makes the marquee 
  /// scroll faster. The default value is 6. 
  /// </summary>
  public int ScrollAmount
  {
   get
   {
    return (int)ViewState["ScrollAmount"];
   }
   set
   {
    ViewState["ScrollAmount"] = value;
   }
  }
  /// <summary>
  /// If null, the Loop = Infinite
  /// </summary>
  public int Loop
  {
   get
   {
    return (int)ViewState["Loop"];
   }
   set
   {
    ViewState["Loop"] = value;
   }
  }
  /// <summary>
  /// BGColor sets the background color of the marquee
  /// </summary>
  public string BGColor
  {
   get
   {
    return (string)ViewState["BGColor"];
   }
   set
   {
    ViewState["BGColor"] = value;
   }
  }

  /// <summary>
  /// Hspace sets the horizontal space to the left and right of the marquee
  /// </summary>
  public int HSpace
  {
   get
   {
    return (int)ViewState["HSpace"];
   }
   set
   {
    ViewState["HSpace"] = value;
   }
  }
  /// <summary>
  /// VSpace sets the vertical space at the top and bottom of the marquee. 
  /// </summary>
  public int VSpace
  {
   get
   {
    return (int)ViewState["VSpace"];
   }
   set
   {
    ViewState["VSpace"] = value;
   }
  }
  /// <summary>
  /// Items are the individual elements to scroll within the marquee.
  /// </summary>
  public StringCollection Items 
  {
   get
   {
    return _Items;
   }
   set
   {
    _Items = value;
   }
  }
  /// <summary>
  /// MarqueeStyle sets the inline css style of the marquee control
  /// </summary>
  public string MarqueeStyle
  {
   get
   {
    return (string)ViewState["MarqueeStyle"];
   }
   set
   {
    ViewState["MarqueeStyle"] = value;
   }
  }
  /// <summary>
  /// Determines whether to first Server.HtmlDecode the items before displaying. This is helpful
  /// for driving the Marquee from an XML config file which has limitation in storing certain characters.
  /// </summary>
  public bool HtmlDecode 
  {
   get
   {
    return (bool)ViewState["HtmlDecode"];
   }
   set
   {
    ViewState["HtmlDecode"] = value;
   }
  }
  #endregion
  #region "Constructors"
  public Marquee()
  {
   this.Enabled = true;
   base.Width = 100;
   base.Height = 15;
   this.ScrollDirection = MarqueeScrollDirection.Left;
   this.ScrollBehavior = MarqueeScrollBehavior.Scroll;
   this.ScrollDelay = 85;
   this.ScrollAmount = 6;
   this.Loop = 0;
   this.BGColor = "white";
   this.HSpace =  1;
   this.VSpace = 1;
   this.MarqueeStyle = "";
   this.HtmlDecode = false;
  }
  #endregion
  #region "Render"
  /// <summary>
  /// Render this control to the output parameter specified.
  /// </summary>
  /// <param name="output"> The HTML writer to write out to </param>
  protected override void Render(HtmlTextWriter output)
  {
   if(Enabled && this.Items != null && this.Items.Count > 0)
   {
    //Start Rendering the Control's required HTML
    //Write the Beginning of the Start Tag
    string _MarqueeHtml = "<marquee width=\"" + base.Width + "\"";
    _MarqueeHtml += " height=\"" + base.Height +"\"";
    _MarqueeHtml += " direction=\"" + this.ScrollDirection +"\"";
    _MarqueeHtml += " behavior=\"" + this.ScrollBehavior +"\"";
    _MarqueeHtml += " scrolldelay=\"" + this.ScrollDelay +"\"";
    _MarqueeHtml += " scrollamount=\"" + this.ScrollAmount +"\"";
    
    if (this.Loop > 0) 
    {
     _MarqueeHtml += " loop=\"" + this.Loop +"\"";
    }
    _MarqueeHtml += " bgcolor=\"" + this.BGColor +"\"";
    _MarqueeHtml += " hspace=\"" + this.HSpace +"\"";
    _MarqueeHtml += " vspace=\"" + this.VSpace +"\"";
    //Write any inline css style
    if (this.MarqueeStyle != "" && this.MarqueeStyle.Length > 0) 
    {
     _MarqueeHtml += " style=\"" + this.MarqueeStyle +"\"";
    }
    //Write the End of the Start Tag
    _MarqueeHtml += ">";
    //Loop and add Child Scroll Elements
    if (this.Items.Count > 0) 
    {
     foreach (string _item in Items) 
     {
      if (this.ScrollDirection == MarqueeScrollDirection.Up || 
       this.ScrollDirection == MarqueeScrollDirection.Down) 
      {
       if (this.HtmlDecode) 
       {
        _MarqueeHtml += System.Web.HttpContext.Current.
          Server.HtmlDecode(_item.ToString()) + "<br>";
       } 
       else 
       {
        _MarqueeHtml += _item.ToString() + "<br>";
       }
      } 
      else 
      {
       if (this.HtmlDecode) 
       {
        _MarqueeHtml += System.Web.HttpContext.Current.
          Server.HtmlDecode(_item.ToString()) +" &nbsp;";
       } 
       else 
       {
        _MarqueeHtml += _item.ToString() +" &nbsp;";
       }
      }
     }
    }
    //Write the Ending Tag
    _MarqueeHtml += "</marquee>";
    //Render the Control to the Output
    output.WriteLine(_MarqueeHtml);
   }
  }
  #endregion
 }
 #region "Designer Class"
 public class MarqueeDesigner:ControlDesigner
 {
  string Width;
  public override bool AllowResize
  {
   get
   {
    return true;
   }
  }
  public  override string GetDesignTimeHtml()
  {
   string DesignTimeHtml = "";
       
   Marquee ControlDesigned = (Marquee)this.Component;
   
   Width = this.Behavior.GetAttribute("Width", true).ToString();
   DesignTimeHtml = "<marquee width=\""+ Width + "\"></marquee>";
         
   return DesignTimeHtml;
  
  }
 }
 #endregion
}
 

Building the Associated DataObject (the class that handles the data-in, data-out, etc.)

  
public class Alert 
{
 /// <summary>
 /// Adds/Updates an Alert to the Marquee configuration file.
 /// </summary>
 /// <param name="PathToConfig">The Path and Name of the XML configuration file</param>
 /// <param name="AlertItemName">The Alert Item Key/Name</param>
 /// <param name="AlertItemValue">The Alert Item Value</param>
 /// <returns>Boolean success/failure of insert/update</returns>
 public static bool AddUpdateAlert(string PathToConfig, string AlertItemName, string AlertItemValue)
 {
  bool _retVal = false;
  XmlDocument _xmlDoc = LoadAlerts(PathToConfig);
  XmlNode _xmlRootNode =  _xmlDoc.SelectSingleNode("//Alerts");
  if (_xmlRootNode == null)
   throw new InvalidOperationException("Alerts section not found in the configuration file.");
  try
  {
   XmlElement _xmlElem = (XmlElement)_xmlRootNode.SelectSingleNode(string.Format("//Alert[@name='{0}']", AlertItemName));
   if (_xmlElem != null)
   {
    _xmlElem.SetAttribute("value", AlertItemValue);
   }
   else
   {
    _xmlElem = _xmlDoc.CreateElement("Alert");
    _xmlElem.SetAttribute("name", AlertItemName);
    _xmlElem.SetAttribute("value", AlertItemValue); 
     
    _xmlRootNode.AppendChild(_xmlElem);
   }
    
   _xmlDoc.Save(PathToConfig);
   _retVal = true;
  }
  catch(Exception ex)
  {
   _retVal = false;
   throw ex;
  }
  finally 
  {
   _xmlDoc = null;
  }
  return _retVal;
 }
 /// <summary>
 /// Removes a TCT Ticket Alert from the Marquee configuration file.
 /// </summary>
 /// <param name="PathToConfig">The Path and Name of the XML configuration file</param>
 /// <param name="AlertItemName">The Alert Item Key/Name</param>
 /// <returns>Boolean success/failure of deletion</returns>
 public static bool RemoveAlert(string PathToConfig, string AlertItemName)
 {
  bool _retVal = false;
  XmlDocument _xmlDoc = LoadAlerts(PathToConfig);
  XmlNode _xmlRootNode =  _xmlDoc.SelectSingleNode("//Alerts");
  try
  {
   if (_xmlRootNode == null)
    throw new InvalidOperationException("Alerts section not found in the configuration file.");
   else
   {
    _xmlRootNode.RemoveChild(_xmlRootNode.SelectSingleNode(string.Format("//Alert[@name='{0}']", AlertItemName)));
    _xmlDoc.Save(PathToConfig);
   }
   _retVal = true;
  }
  catch (NullReferenceException ex)
  {
   _retVal = false;
   throw new Exception(string.Format("The name {0} does not exist.", AlertItemName), ex);
  }
  finally 
  {
   _xmlDoc = null;
  }
  return _retVal;
 }
 /// <summary>
 /// Returns a String Collection list of all Alerts (values) from the Config file
 /// </summary>
 /// <param name="PathToConfig">The Path and Name of the configuration file</param>
 /// <returns>StringCollection</returns>
 public static System.Collections.Specialized.StringCollection GetAlerts(string PathToConfig) 
 {
  XmlDocument _xmlDoc = null;
  System.Collections.Specialized.StringCollection _scAlerts = new System.Collections.Specialized.StringCollection();
  
  try
  {
   _xmlDoc = new XmlDocument();
   _xmlDoc.Load(PathToConfig);
   
   foreach (XmlElement _xmlElem in _xmlDoc.GetElementsByTagName("Alert")) 
   {
    _scAlerts.Add(_xmlElem.Attributes["value"].Value);
   }
   
   return _scAlerts;
  }
  catch (System.IO.FileNotFoundException Ex)
  {
   throw new Exception("No Alerts configuration file found.", Ex);
  }
  finally 
  {
   _xmlDoc = null;
  }
 }
 /// <summary>
 /// Returns a DataTable list for display of TCT Alerts
 /// </summary>
 /// <param name="PathToConfig">The Path and Name of the configuration file</param>
 /// <returns>DataTable result</returns>
 public static DataTable List(string PathToConfig) 
 {
  DataTable _dtAlerts = new DataTable("Alerts");
  _dtAlerts.Columns.Add("name", typeof(string));
  _dtAlerts.Columns.Add("value", typeof(string));
  XmlDocument _xmlDoc = new XmlDocument();
  try
  {
   _xmlDoc.Load(PathToConfig);
   
   foreach (XmlElement _xmlElem in _xmlDoc.GetElementsByTagName("Alert")) 
   {
    //Create a new row
    DataRow _drNew = _dtAlerts.NewRow();
    //Set values of new row
    _drNew["name"] = _xmlElem.Attributes["name"].Value;
    _drNew["value"] = _xmlElem.Attributes["value"].Value;
    //Add new row to DataTable Alerts
    _dtAlerts.Rows.Add(_drNew);
   }
   
   //Return the new DataTable Alerts
   return _dtAlerts;
  }
  catch (System.IO.FileNotFoundException Ex)
  {
   throw new Exception("No Alerts configuration file found.", Ex);
  }
  finally 
  {
   _xmlDoc = null;
  }
 }
 /// <summary>
 /// Loads the XML configuration file where TCT Alerts are stored.
 /// </summary>
 /// <param name="PathToConfig">The Path and Name of the configuration file</param>
 /// <returns>XmlDocument</returns>
 private static XmlDocument LoadAlerts(string PathToConfig)
 {
  XmlDocument _xmlDoc = null;
  
  try
  {
   _xmlDoc = new XmlDocument();
   _xmlDoc.Load(PathToConfig);
   return _xmlDoc;
  }
  catch (System.IO.FileNotFoundException Ex)
  {
   throw new Exception("No Alerts configuration file found.", Ex);
  }
 }
}