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).

I had been getting this error:

Validation of viewstate MAC failed. If this application is hosted by a Web Farm or cluster, ensure that <machineKey> configuration specifies the same validationKey and validation algorithm. AutoGenerate cannot be used in a cluster.

I scoured the net and none of the suggested resolutions (EnableViewStateMac) were working for me, so I decided to dig in a little bit.

Here is my situation:

I have a page, 'X', that does a Server.Transfer to page 'Y'. Y is a form that enters information into a database upon pressing a button. Upon pressing the button to submit the information, the error would fire off. If I ran page Y without Transferring to it, the page would work normally. As I mentioned above, no typical work around was working for me.

The reason that no typical solution was working for me is that I have a "base page" that I use for all the pages on my site. In this base page I do a couple things, one of which is that I alter the action attribute of my <form> tag. I do this because I have a custom UrlReWriting HttpModule and I need my actions to match the URI in the address bar of the browser, not the virtual path to the executing script.

What was happening was that Y's action was still set to X! The resolution for me was to add a property to my base page that would disable altering the form's action. The page now has page Y posting back to page Y and everybody is happy!

All web developers and designers should know by now that you should not use the traditional <object><embed>... method for placing Flash content on your site. Macromedia (Adobe) recommends using javascript to load the Flash content and it works just fine. However, if you are a .NET programmer and have a lot of flash content it is fairly easy to create a Control that handles the repetitive tasks.

First we need to create a little javascript file. I place my javascript in a /Utility/ directory (a la CommunityServer) and here it is:

/Utility/LoadFlash.js

function loadFlashDoc(fPath, fWidth, fHeight){
document.write('<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,29,0" ');
document.write(' width="' + fWidth + '" height="' + fHeight + '"> ');
document.write(' <param name="movie" value="' + fPath + '"> ');
document.write(' <param name="quality" value="high"> ');
document.write(' <param name="wmode" value="transparent" /> ');
document.write(' <embed wmode="transparent" quality="high" pluginspage="http://www.macromedia.com/go/getflashplayer" type="application/x-shockwave-flash" ');
document.write(' src="' + fPath + '" ');
document.write(' width="' + fWidth + '" ');
document.write(' height="' + fHeight + '"></embed> ');
document.write('</object>');
}

 

Nothing to complex here. Just write out the object & embed lines used to show the flash file specified as fPath with the fWidth and fHeight.

Now for the ASP.NET Control.

public class FlashLoader : System.Web.UI.Control
    {

        #region FlashFile property
        private String _FlashFile;
        public String FlashFile
        {
            get { return _FlashFile; }// get
            set { _FlashFile = value; }// set
        }// property

        #endregion

        #region Width property
        private int _Width;

        public int Width
        {
            get { return _Width; }// get
            set { _Width = value; }// set
        }// property

        #endregion

        #region Height property
        private int _Height;

        public int Height
        {
            get { return _Height; }// get
            set { _Height = value; }// set
        }// property

        #endregion

        #region Title property
        private String _Title = String.Empty;
        /// <summary>
        /// A Title for the Flash File
        /// </summary>
        public String Title
        {
            get { return _Title; }// get
            set { _Title = value; }// set
        }// property

        #endregion


        protected override void OnLoad(EventArgs e)
        {
            if (String.IsNullOrEmpty(FlashFile))
                return;

            if (!Page.ClientScript.IsClientScriptIncludeRegistered(typeof(FlashLoader), "flashLoader"))
                Page.ClientScript.RegisterClientScriptInclude(typeof(FlashLoader), "flashLoader", "/Utility/loadFlash.js");
        }

        protected override void Render(HtmlTextWriter writer)
        {
            if (String.IsNullOrEmpty(FlashFile))
                return;

            writer.WriteLine(String.Format(@"<script type=""text/javascript"">loadFlashDoc('{0}', '{1}', '{2}');</script>", FlashFile, Width, Height));
            writer.WriteLine("<noscript>");
            writer.Write("\t<object classid=\"clsid:d27cdb6e-ae6d-11cf-96b8-444553540000\" codebase=\"http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=7,0,0,0\"");
            writer.WriteLine(String.Format(@"width=""{0}"" height=""{1}"" id=""zoom_map"" align=""top"">", Width, Height));
            writer.Write("\t\t");
            writer.WriteLine(String.Format(@"<param name=""movie"" value=""{0}"" />", FlashFile));
            writer.WriteLine("\t\t<param name=\"wmode\" value=\"transparent\" />");
            writer.WriteLine("\t\t<param name=\"quality\" value=\"high\" />");
            writer.WriteLine("\t\t<param name=\"bgcolor\" value=\"#FFFFFF\" />");
            writer.Write("\t\t");
            writer.WriteLine(String.Format(@"<embed src=""{0}"" quality=""high"" bgcolor=""#FFFFFF"" width=""{1}"" height=""{2}"" name=""{3}"" align=""top"" wmode=""transparent"" type=""application/x-shockwave-flash"" pluginspage=""http://www.macromedia.com/go/getflashplayer""></embed>", FlashFile, Width, Height, Title));
            writer.WriteLine("\t</object>");
            writer.WriteLine("</noscript>");
        }
    }

 


To use this control you would just register your control and add the tag as such:


<FLASH:FlashLoader id="Flash" runat="server" FlashFile="/path/to/file.swf" Width="475" Height="275" Title="Flash File" /> 

And you will get output as follows:

<script src="/Utility/loadFlash.js" type="text/javascript"></script>
...
<script type="text/javascript">loadFlashDoc('/path/to/file.swf', '475', '275');</script>
<noscript>
<object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=7,0,0,0"width="475" height="275" id="zoom_map" align="top">
<param name="movie" value="/path/to/file.swf" />
<param name="wmode" value="transparent" />
<param name="quality" value="high" />
<param name="bgcolor" value="#FFFFFF" />
<embed src="/path/to/file.swf" quality="high" bgcolor="#FFFFFF" width="475" height="275" name="Flash File" align="top" wmode="transparent" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer"></embed>
</object>
</noscript>

 

Keyvan Nayyeri wrote up an article (and HttpModule) on how he handles removing the www from the URI when people visit his site. I wrote something similar to this a while back for a site I develop. This particular company has about 10 domain names that all point to the companies main web site. Back in 2000, before I started working for the company, the search engines had indexed all of the domain names. While this may be good I have always been worried about duplicate content in Google's Index.

The code below is a toned down version of the HttpModule I use.

First, I have a config file that properly configures the module.

...
<forceDomain enabled="true" debug="false">
  <primaryDomain>www.example.com</primaryDomain>
  <ignoreDomains>
    <add value="localhost" />
    <add value="www.example2.com" />
  </ignoreDomains>
</forceDomain>
...

The config file tells the module that the primary domain for the site is www.example.com. It then goes on to setup domain names that are not redirected. Naturally, we want localhost requests to not be redirected. For this example, www.example2.com is also used. (yes, I realize that www.example.com isn't a domain it is a URI, but I am too lazy to go back and change things)

Now to the code of the HttpModule.

public class ForceDomain : System.Web.IHttpModule
    {
        private ForceDomainConfiguration ForceDomainConfiguration;
        
        public void Init(HttpApplication app)
        {
            app.BeginRequest += new EventHandler(this.OnBeginRequest);
        }

        public void OnBeginRequest(Object source, EventArgs e)
        {
            // grab the configuration
            ForceDomainConfiguration = ForceDomainConfiguration.GetConfig();


            // If disabled.. exit out.
            if (!ForceDomainConfiguration.ModuleEnabled)
                return;
            // get access to app and context
            HttpApplication app = (HttpApplication)source;
            HttpContext ctx = app.Context;

            // Get the PrimaryDomain the site operates with
            String DomainName = ForceDomainConfiguration.PrimaryDomain;
            if (String.IsNullOrEmpty(DomainName))
            {
                throw new System.Exception("Primary Domain not set in config file. /Config/forceDomain/primaryDomain");
            }

            DomainName = DomainName.ToLower();
            // If we are operating in the primary domain, then just get out of here.
            if (ctx.Request.Url.Host.ToLower() == DomainName)
                return;

            // loop through the ignoredDomains.

            // If we are supposed to ignore it, then break out
            foreach (String s in ForceDomainConfiguration.IgnoredDomains)
                if (ctx.Request.Url.Host.ToLower() == s.ToLower())
                    return;

            // If we got this far, create the new URL and redirect
            StringBuilder sendTo = new StringBuilder();
            if (ctx.Request.IsSecureConnection)
                sendTo.Append("https://");
            else
                sendTo.Append("http://");

            sendTo.Append(DomainName);
            sendTo.Append(ctx.Request.RawUrl);

            ctx.Response.Status = "301 Moved Permanently";
            ctx.Response.AddHeader("Location", sendTo.ToString());
            ctx.Response.End();

        }

        public void Dispose()
        { 
        }
    }

Like I said, the code above is a toned down version of the module I use. I also make reference to ForceDomainConfiguration.GetConfig() which is code that I haven't showed here. If anyone wants me to package this up just drop a comment and I will see what I can do.

[tags:ASP.NET, HttpModule]