Thursday, October 4, 2012

Load Umbraco nodes by Guid and print Guid's in Razor

Umbraco internals provide methods for accessing objects by Guid instead of ID.  This is necessary when using multiple environments and moving files around with Courier.  The reason being that ID's are not unique across instances and as items are moved around they will get assigned new ID's.  The following for example will not work as expected when deployed with Courier.

@Model.NodeById("Some static pageId").myVariable

To do this properly the Guid is needed instead. The problem though is that the Razor DynamicNode object doesn't expose or let you load nodes by the Guid.  The included class can be added to your App_Code directory to allow the following code samples to work in Razor.

using JSP;
// Get a object by the Guid
@Model.NodeByGuid("someGuid").myVariable
// Print a object's Guid
@Model.UniqueId()


GuidExtensions.cs
using System;
using umbraco;
using umbraco.MacroEngines;
using umbraco.cms.businesslogic;

namespace JSP
{
    /// <summary>
    /// Extensions to the umbraco framework.
    /// </summary>
    public static class GuidExtensions
    {

        /// <summary>
        /// Get a node by the unique Guid.  This is needed for sites where courier is in use.
        /// </summary>
        /// <param name="self">Current Page</param>
        /// <param name="guid">Guid to load.</param>
        /// <returns>DynamicNode</returns>
        public static DynamicNode NodeByGuid(this DynamicNode self, string guid)
        {
            return self.NodeByGuid(new Guid(guid));
        }

        /// <summary>
        /// Get a node by the unique Guid.  This is needed for sites where courier is in use.
        /// </summary>
        /// <param name="self">Current Page</param>
        /// <param name="guid">Guid to load.</param>
        /// <returns>DynamicNode</returns>
        public static DynamicNode NodeByGuid(this DynamicNode self, Guid guid)
        {
            return self.NodeById(new CMSNode(guid).Id);
        }

        /// <summary>
        /// Get the unique ID for a selected node.
        /// </summary>
        /// <param name="self">Node</param>
        /// <returns>guid</returns>
        public static string UniqueId(this DynamicNode self)
        {
            return new CMSNode(self.Id).UniqueId.ToString();
        }
    }
}

Edit Macros that are Cached in Umbraco

One of the really annoying problems in Umbraco is that is when editing Macro Scripting Files for Macros that are cached your changes will not appear until the cache expires and is rebuilt.  To get around this you have to go and turn off caching on the macro that you are working on and then remember to turn it back on when done.

A much nicer solution to this problem is simply to clear out the cached versions of the macro whenever its script file is saved.  In this way the new version of the macro will always be displayed when refreshing the page after any changes.  This code can be added to the App_Code directory of an umbraco install to do jus that.

ClearMacroCache.cs
using System.Web;
using umbraco.BusinessLogic;
using umbraco.IO;
using System.IO;
using System.Web.Hosting;
using ClientDependency.Core.Config;
using System.Text.RegularExpressions;
using System.Linq;
using umbraco.cms.businesslogic.macro;
using umbraco.cms.businesslogic.cache;

namespace JSP
{
    public class ClearMacroCache : ApplicationBase
    {
        public ClearMacroCache()
        {
            // Get the macro folder.
            var macroPath = HttpContext.Current.Server.MapPath(SystemDirectories.MacroScripts);

            // Watch for file changes ot any macros.
            HttpContext.Current.Application.Add("macroWatcher", new FileSystemWatcher(macroPath));
            var macroFsw = (FileSystemWatcher)HttpContext.Current.Application["macroWatcher"];
            macroFsw.EnableRaisingEvents = true;
            macroFsw.IncludeSubdirectories = true;

            // Trigger an event on any change.
            macroFsw.Changed += new FileSystemEventHandler(expireClientDependency);
            macroFsw.Created += new FileSystemEventHandler(expireClientDependency);
            macroFsw.Deleted += new FileSystemEventHandler(expireClientDependency);
        }

        /// <summary>
        /// This method is called each time a macro file is saved/updated and will clear out and cached versions 
        ///     of the macro to eliminate cacheing problems when developing macros.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected void expireClientDependency(object sender, FileSystemEventArgs e)
        {
            // If this is a real macro file.
            Regex reg = new Regex(@"\d+_");
            if(!reg.Match(e.Name).Success)
            {
                var macro = Macro.GetAll().SingleOrDefault(i => i.ScriptingFile.ToLower() == e.Name.ToLower());

                if (macro != null)
                    Cache.ClearCacheByKeySearch("macroHtml_" + macro.Alias);
            }
        }
    }
}

Monday, October 1, 2012

Automatic Unobtrusive Binding of Labels to Form Elements

One commonly forgotten feaure in HTML is that when creating web forms the labels related to input elements should be related to them.  In the simplest terms it is done like so:

<label for="myItem">My Label</label><input id="myItem">
The benifit to doing this is that browser will automatically focus the input element when the label is selected.  A little thing I know but it's not use in most sites doe to inconvenience.

Thus I have the following bit of javascript to automatically do the same thing.  It looks for any unbound  labels and binds them to the next input, text area, or select item down the DOM tree from them.  That is the next matching element within the same parent that the label is in.  This method will also work if the input fields don't have ID's associated with them as an added bonus.

 To use just include this in a global script running on your site and it will do the work with no noticeable overhead.  This does require jQuery to work.
$(document).ready(function () {
  $("form label").each(function() {
    if (typeof $(this).attr("for") == "undefined" || $("#" + $(this).attr("for")).length == 0) {
      nextControl = $(this).next("input,textarea,select");
      if (nextControl.length > 0) {
        if (typeof nextControl.attr("id") != "undefined") {
          $(this).attr("for", nextControl.attr("id"));
        } else {
          $(this).removeAttr("for");
          $(this).click(function() {
            $(this).next("input,textarea,select").focus()
          });
        }
      }
    }
  });
});