Disclaimer: While some of these guidelines may be applied to other Content Management Systems, or .net web forms in general, this is based on my experience with Sitecore.

A common approach of having a CMS based web site implemented usually goes as follows:

  1. Company X (aka 'The Client') approaches a design firm.
  2. Said design firm paints a lot of pretty pictures (also known as wire-frames, mock-ups, etc..), which the client gets sold on
  3. The design firm (or a subcontractor) will cut up those pretty pictures into HTML/CSS/JS files.
  4. That design firm chooses a Sitecore partner to do the implementation of their super clean front-end work.

While I am greatly generalizing, this is a common approach. The point I am hoping to address is that design firms (who aren't familiar with .net or Sitecore) are very likely to make things difficult for the implementation team. Here are a few issues I have seen over the last few projects.

ID attribute on html elements

This is another broad statement, but take it at face-value to avoid problems. Front end developers will reference an element via the id attribute for css styling as well as jQuery (or any other client side framework) manipulations. While this sounds like a very normal approach it can lead to issues when that element gets converted into a .NET Server control and this tag:

    <input name="q" id="q" type="text" />

is now this tag:

    <input name="ctl00_Main_q" id="ctl00_Main_q" type="text" />

This annoying rewrite of the id attribute will break all pre-written css and javascript written for that id. As back-end developers we know that only id's of server controls (or regular tags with runat="server") will be rewritten, but is easier to have front-end dev's avoid referencing elements by id them all together.

HTML Patterns

In general, repeating patterns are very easy to program. Avoid throwing unnecessary id's and classes on elements. A perfect example would be for navigation elements; a side menu should be a simple nested unordered list with no extra classes. Having classes that change based on content, depth in site, etc... it will require logic, which takes time to implement.

Do this:

<h1>Nav Header</h1>
    <ul>
        <li><a href="#">Level 1a</a>
            <ul>
                <li><a href="#">Level 2a</a></li>
                <li><a href="#">Level 2b</a></li>
                <li><a href="#">Level 2c</a></li>
            </ul>
        </li>
         <li><a href="#">Level 2a</a>
            <ul>
                <li><a href="#">Level 2a</a></li>
                <li><a href="#" class="selected">Level 2b</a></li>
                <li><a href="#">Level 2c</a></li>
            </ul>
        </li>
    </ul>

Not this:

<h1>Nav Header</h1>
    <ul class="mainNav">
        <li class="first"><a href="#">Level 1a</a>
            <ul class="subnav">
                <li class="first"><a href="#">Level 2a</a></li>
                <li><a href="#">Level 2b</a></li>
                <li class="last"><a href="#">Level 2c</a></li>
            </ul>
        </li>
         <li class="last"><a href="#">Level 2a</a>
            <ul class="subnav">
                <li class="first"><a href="#">Level 2a</a></li>
                <li><a href="#" class="selected">Level 2b</a></li>
                <li class="last"><a href="#">Level 2c</a></li>
            </ul>
        </li>
    </ul>

Obviously this is an overly simplistic menu system that cannot always be accomplished. My main point here is to avoid classes on elements that aren't absolutely necessary. We can use basic css selectors to get at elements without having to provide more classes to denote first and subnav.

Modularity

If you have ever seen a Sitecore sales demo you have undoubtedly seen the presenter effortlessly change the layout of the page. Possibly adding some 'widgets' to the side or changing the entire layout from two to three columns? Sitecore allows renderings and sublayouts to be placed within placeholders. This introduces a lot of flexibility for the design of site, however, it does require a lot of forethought to make sublayouts look nice in multiple locations.

In order to make your sublayouts (or any other custom 'widgeting' system) work and look great you should follow a couple guidelines.

  1. Keep your html super lean and clean.
  2. Object Oriented Css

Design Images

This is just my preference on how to organize the file system of a Sitecore site and you may have your own system and that is fine. Whatever system you choose, just be consistent as sites can grow big quickly and managing these little files can become daunting.

  • Images referenced from css files should go in an "images" directory underneath the css directory. Furthermore, keep image paths relative to the css file.
  • Any leftover images required for the design should go in a /images directory.
  • Placeholder images should be prefixed with "fpo_" which makes it easy to identify these images as being good candidates to be moved into Sitecore media library.

Multi-Lingual Considerations

Manipulating the DOM with Javascript is trivial with the introduction of client libraries such as jQuery. A potential issue will arise when a developer hard codes in a language specific string (such as 'open,' 'close,' etc...) in a javascript function. In order to avoid this pitfall have the translatable text reside in a hidden span on the page, not in the javascript code.

I have been (slowly) working on the Web Based FileZilla Administration project. One of the issues that I could predict happening would be the creation of a user/group/setting and then the end user would leave the page without saving their changes. There are many ways to handle this type of issue, but I wanted a fairly simple implementation.

What I did was inherit a few of the base controls (TextBox, CheckBox, Button) and added an onchange attribute to them. The onchange attribute works with a client script that gets injected into the page.

Here is the code for the TextBox (CheckBox is almost identical):

public class TextBox : System.Web.UI.WebControls.TextBox
{
   protected override void OnLoad(EventArgs e)
   {
      // load the javascript
      if (!Page.ClientScript.IsClientScriptBlockRegistered(typeof(ConfirmChangeScript), "PageScript"))
         Page.ClientScript.RegisterClientScriptBlock(typeof(ConfirmChangeScript), "PageScript", ConfirmChangeScript.PageScript, true);
      base.OnLoad(e);
   }
   protected override void Render(HtmlTextWriter writer)
   {
      base.Attributes.Add("onchange", ConfirmChangeScript.OnChange);
      base.Render(writer);
   }
}

Here is the ConfirmChangeScript:

public class ConfirmChangeScript : System.Web.UI.Control
{
    public  static string PageScript
    {
        get
        {
            System.Text.StringBuilder script = new System.Text.StringBuilder();
            script.AppendFormat(@"      var isDirty = false; ");
            script.AppendFormat(@"      var showConfirm = true; ");
            script.AppendFormat(@"      window.onbeforeunload = confirmExit;");
            script.AppendFormat(@"      function confirmExit()");
            script.AppendFormat(@"      {{");
            script.AppendFormat(@"        if (!showConfirm) return; ");
            script.AppendFormat(@"        if (isDirty) ");
            script.AppendFormat(@"            return ""You have attempted to leave this page.  If you have made any changes to the ");
            script.AppendFormat(@"fields without clicking the Save button, your changes will be lost.  Are you sure you want to exit ");
            script.AppendFormat(@"this page?"";");
            script.AppendFormat(@"      }}");
            return script.ToString();
        }
    }
    public static string OnChange
    {
        get
        {
            return "isDirty=true";
        }
    }
}

And finally, here is the Button. There is an additional attribute for the button to suppress the confirmation. This would be used in the event that you want to show the confirmation upon clicking the button.

public class Button : System.Web.UI.WebControls.Button
{
    #region EnableShowConfirm property
    private bool _SupressConfirm = true;
    public bool SupressConfirm
    {
        get { return _SupressConfirm; }// get
        set { _SupressConfirm = value; }// set
    }// property
    #endregion
    protected override void OnLoad(EventArgs e)
    {
        if (!Page.ClientScript.IsClientScriptBlockRegistered(typeof(ConfirmChangeScript), "PageScript"))
            Page.ClientScript.RegisterClientScriptBlock(typeof(ConfirmChangeScript), "PageScript", ConfirmChangeScript.PageScript, true);
        base.OnLoad(e);
    }
    protected override void Render(HtmlTextWriter writer)
    {
        if (SupressConfirm)
            base.Attributes.Add("onclick", "showConfirm=false;");
        base.Render(writer);
    }
}

Just add the reference in your .aspx page and drop in a TextBox and Button control. If you edit the contents of the textbox and then try to leave the page it will prompt you to confirm.

You can download the source here

These controls, and code, are very simple and have at least one known bug. For a more advanced approach try looking at Scott Michell's method.

ASP.NET Gravatar Control

March 14, 2007

Gravatar 2.0 has been out for a few weeks now and I didn't see any ASP.NET implementation listed on their site so I figured I would wip one up.

It is pretty simple to implement a Gravatar. All you need to do is set the src attribute of an img tag to a location on Gravatar's domain. The src attribute has a few parameters, but the only "tricky" one is an MD5 hash of the users email address.

Here is the code:

public class Gravatar : System.Web.UI.WebControls.Image
{
    #region Rating property
    public enum GravatarRating { G, PG, R, X }
    private GravatarRating _Rating = GravatarRating.G;
    public GravatarRating Rating
    {
        get { return _Rating; }// get
        set { _Rating = value; }// set
    }// property

    #endregion

    #region Size property
    private int _Size = 80;
    /// 
    /// An optional "size" parameter may follow that specifies the desired width and height of the gravatar. Valid values are from 1 to 80 inclusive. Any size other than 80 will cause the original gravatar image to be downsampled using bicubic resampling before output.
    /// 
    public int Size
    {
        get
        {
            if (_Size <= 0)
                return 80;
            if (_Size > 80)
                return 80;
            return _Size;
        }
        set
        {
            _Size = value;
            base.Width = value;
            base.Height = value;
        }// set
    }// property
    #endregion

    #region Email property
    private string _Email;
    public string Email
    {
        get { return _Email; }// get
        set { _Email = value; }// set
    }// property

    #endregion

    #region DefaultImageUrl property
    private string _DefaultImageUrl;

    public string DefaultImageUrl
    {
        get { return _DefaultImageUrl; }// get
        set { _DefaultImageUrl = value; }// set
    }// property

    #endregion

    #region Hide some members
    new private string ImageUrl { get { return String.Empty; } }
    /// Gravatar only supports a size property
    new private int Width { get { return Size; } }
    new private int Height { get { return Size; } }
    #endregion

    protected override void Render(System.Web.UI.HtmlTextWriter writer)
    {
        System.Text.StringBuilder image = new System.Text.StringBuilder();
        image.Append("http://www.gravatar.com/avatar.php?");
        image.Append("gravatar_id=");
        image.Append(MD5HashMe(Email));
        image.Append("&rating=");
        image.Append(Rating.ToString());
        image.Append("&size=");
        image.Append(Size.ToString());

        if (!String.IsNullOrEmpty(DefaultImageUrl))
        {
            image.Append("&default=");
            image.Append(System.Web.HttpUtility.UrlEncode(DefaultImageUrl));
        }

        base.ImageUrl = image.ToString();
        base.Render(writer);
    }

    private string MD5HashMe(string email)
    {
        System.Text.Encoder enc = System.Text.Encoding.Unicode.GetEncoder();
        byte[] unicodeText = new byte[email.Length * 2];
        enc.GetBytes(email.ToCharArray(), 0, email.Length, unicodeText, 0, true);

        System.Security.Cryptography.MD5 md5 = new System.Security.Cryptography.MD5CryptoServiceProvider();
        byte[] result = md5.ComputeHash(unicodeText);

        System.Text.StringBuilder sb = new System.Text.StringBuilder();
        for (int i = 0; i < result.Length; i++)
            sb.Append(result[i].ToString("X2"));

        return sb.ToString();
    }
}

To use this control all you need to do is add the following to your .aspx page:

<%@ Register TagPrefix="CC" Assembly="CarKnee.Controls" Namespace="CarKnee.Controls" %>

The code is also available for download Gravatar.zip (1 KB).

One of my web servers had been having all sorts of problems over the last few months. It turned out to be the RAID controller, but that is another post. Because of the problems we were having I wanted a simple way to get a notification of when the site we were hosting went down. I didn't want anything fancy, just a simple email when the sites stopped responding. Here is what I found...

First, I downloaded SystemMonitor from MSDN's Coding4Fun. This is an extensible application that can be run in your system tray and notifies you of events. I then wrote up a quick "Monitor" to send a request to the sites. The monitor makes an HttpWebRequest to a site looking for a specific file. Any errors returned (404/500/etc...) will fire off a notification from the SystemMonitor. If a status code of 200 is returned, the monitor will check for "site running" in the content of the page. If "site running" is not found then a notification will be sent.

The advantage with this method is that you can specify the URI of the page you want to test. This page can be .txt, .htm, .asp, .aspx, .whatever. So, if your web server is ok, bu[tags:Web Server, Programming]t your database server has a habit of going down then you can point the SystemMonitor to test for a  page that will test your DB connection. As long as the response status code 200 and the page returned "site running" in the content of the page it will not fire off a notification.

To install the Site Monitor you need to download the SystemMonitor from Coding4Fun. Once downloaded, add my SiteMonitor (and any needed references) to the project and recompile. Once recompiled you just need to add the settings to the App.config file. The config section, as shown below, is pretty self explanatory. The one setting that may be a little abstract is the localIP variable. The machine I installed this on has multiple IP addresses assigned to the NIC. One has access to the Internet and the other doesn't. By setting the localIP variable you can control what IP the monitor will bind to when trying to make a request.

      <monitor runFrequency="00:10"  type="SystemMonitor.Monitors.SiteMonitor,SystemMonitor">
        <settings>
          <setting name="url" value="http://www.example.com/Utility/uptime.aspx" />
          <setting name="localIP" value="" />
          <setting name="proxy.enable" value="false" />
          <setting name="proxy.ip" value="192.168.0.1" />
          <setting name="proxy.port" value="8080" />
        </settings>
        <notifiers>
          <notifier type="SystemMonitor.Notifiers.MessageBoxNotifier,SystemMonitor" />
        </notifiers>
      </monitor>

The Code for the Site Monitor is show below, and available for download here.

class SiteMonitor : MonitorBase
    {
        string _url;
        string _localIP;
        bool _enableProxy = false;
        string _proxyIP;
        int _proxyPort = -1;

        #region Binding the WebRequest to a Local IP with a Delegate
        public delegate IPEndPoint BindIPEndPoint(ServicePoint servicePoint, IPEndPoint remoteEndPoint, int retryCount);
        private IPEndPoint BindIPEndPointCallback(ServicePoint servicePoint, IPEndPoint remoteEndPoint, int retryCount)
        {
            if (retryCount < 3)
                return new IPEndPoint(IPAddress.Parse(_localIP), 0);
            else
                return new IPEndPoint(IPAddress.Any, 0);
        }
        #endregion

        public override void Execute()
        {
            HttpWebRequest request;
            HttpWebResponse response;

            string errTitle = String.Empty, errMessage = String.Empty;
            
            try
            {
                request = (HttpWebRequest)WebRequest.Create(_url);
                request.Timeout = 1000;
                
                // bind to a specific local ip
                if (!String.IsNullOrEmpty(_localIP))
                    request.ServicePoint.BindIPEndPointDelegate = new System.Net.BindIPEndPoint(BindIPEndPointCallback);

                if (_enableProxy && !String.IsNullOrEmpty(_proxyIP) && _proxyPort >= 0)
                    request.Proxy = new System.Net.WebProxy(_proxyIP, _proxyPort);

                response = (HttpWebResponse)request.GetResponse();
                HttpStatusCode status = response.StatusCode;

                if (status != HttpStatusCode.OK)
                {
                    errTitle = "Failed Contacting Web Server";
                    errMessage = String.Format("{0} Returned Http Status Code: {1}, {2}", _url, (int)status, status.ToString());
                }
                else
                {

                    StreamReader reader = new StreamReader(response.GetResponseStream());
                    string content = reader.ReadToEnd();

                    // LOOK FOR [SITE RUNNING]
                    if (content.ToLower().IndexOf("site running") < 0)
                    {
                        errTitle = "Web Server Returned Invalid Response";
                        errMessage = String.Format("{0} Returned Invalid Response", _url);
                    }
                    reader.Close(); reader.Dispose(); reader = null;
                }

            }
            catch (WebException ex) // this will catch 404's
            {
                errTitle = "Failed Contacting Web Server";
                if (ex.Response != null)
                {
                    HttpWebResponse r = (HttpWebResponse)ex.Response;
                    errMessage = String.Format("{0} Returned {1}, {2}", _url, (int)r.StatusCode, r.StatusDescription);
                }
                else
                    errMessage = ex.Message;

            }
            catch (Exception ex)
            {
                errTitle = "Failed Monitoring Site";
                errMessage = string.Format("'{0}' failed with an exception: {1}", _url, ex.Message);
            }
            finally
            {
                response = null;
                request = null;
            }

            // fire off the Notifier
            if (!String.IsNullOrEmpty(errTitle))
                Notify(errTitle, errMessage);
        }

        protected override void Initialize(Dictionary<string, string> settings)
        {
            _url = settings["url"];
            _localIP = settings["localIP"];

            _proxyIP = settings["proxy.ip"];
            try{_enableProxy = bool.Parse(settings["proxy.enable"]);}
            catch { _enableProxy = false; }

            try { _proxyPort = int.Parse(settings["proxy.port"]); }
            catch { _proxyPort = -1; }

        }

        public override string Description
        {
            get { return string.Format("Testing '{0}'.", _url); }
        }

        public override MonitorType MonitorType
        {
            get { return MonitorType.Scheduled; }
        }

        public override Icon Icon
        {
            get { return Properties.Resources.PingIcon; }
        }
    }