Thursday, May 29, 2014

Umbraco 7 Automatic Template Creation From Visual Studio/Disk

Prior to version 7 of Umbraco, Templates were always in sync between the Umbraco Back Office and it's Database and The tempaltes Physical Location on disk.  For example, you could add a tempalte on disk in the Views Folder and it would show up in the back office.

Unfortunately, in version 7, this doesn't appear to happen anymore.  As such I built a Self Contained class to handle this.

It works in two ways.

  1. A Manual Sync when the site Starts
  2. A FileSystemWatcher that monitors the Views folder for new files with a .cshtml extension.


When the FileSystemWatcher raises the Created event, the new template is registered with Umbraco in it's database, those showing up in the back office.

Here's some context on how I manage my Version 7 Umbraco Project.

  1. I create a new Empty Web Project in Visual Studio
  2. I use the NuGet Package Manager to add "UmbracoCMS" to the project
    1. Now Build the project, and you have the etnire Umbraco CMS in your visual studio project
  3. I set a local IIS set to point to my Projects Location (so the project is running directly in IIS) for local testing.
  4. I load the site in the browser and I install it to a MySql or SqlServer on our intranet (our dev server).
    1. This allows multiple developers to use the same project in source control and all point to the same dev/production databases.
So now, I want to start adding some Templates... But they don't show up in Umbraco, I have to go to the back office and manually add them.  Then When I move to production I have to do it again manually.  Very inconvienient imo.

Also uSync doesn't really solve the problem for me, because uSync doesn't automatically add new content to the Visual Studio project going with this setup, so I have to remember to do that.

I desired a Code First solution, so I made this class.  I use this class in Conjunction with Umbraco Jet.  All of our Document Types are defined as attribute driven Model Classes and this class here will handle keeping the templates registered.

Final Notes:
To use this, all you have to do is add it as a .CS class to the Umbraco Project (like the one I mentioned above) "If you put it in App_Code make sure you change it's Build Action Property in the Properties window to Compile".  Also, you can have this in an external DLL and reference the DLL in the umbraco project and umbraco will still find it.

The reason it works automatically is it is using the ApplicationEventHandler class to register to Umbraco's ApplicationStarted Event.


// Comment
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Reflection;
    using System.Text;
    using System.Threading.Tasks;
    using System.Web;
    using global::Umbraco.Core;
    using global::Umbraco.Core.Models;
    using global::Umbraco.Core.Services;
    using global::Umbraco.Web;
    public class CodeDiveEngine
    {
        #region Internal Classes
        public class CodeDiveEngineAppEventHandler : ApplicationEventHandler
        {
            protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
            {
                base.ApplicationStarted(umbracoApplication, applicationContext);
                CodeDiveEngine.StartEngine();
            }
        }
        #endregion

        #region Fields
        //static
        private static object lockObject = new object();
        private static object syncLock = new object();
        private static object templateLock = new object();
        private static object stopLock = new object();
        private static CodeDiveEngine _Instance = null;

        //instanced
        private ApplicationContext applicationContext = null;
        private UmbracoApplication umbracoApplication = null;
        private IFileService fileService = null;
        private List domainAssemblies = null;
        //-Template
        private string baseAppPath;
        private string viewsPath;
        private string partialViewsPath;
        private List physicalTemplates = null;
        private FileSystemWatcher templateWatcher = null;
        private FileSystemEventHandler templateWatcherCreateHandler = null;
        private FileSystemEventHandler templateWatcherDeleteHandler = null;
        #endregion

        #region Properties
        public static CodeDiveEngine Instance
        {
            get
            {
                //I made this singleton as a precaution, which might need tweaked around abit
                //e.g. if you have two umbraco sites running on the same app pool sharing the same code base
                //there will be two contexts, so a singleton allows them both to use the same CodeDiveEngine (being on the same app pool)
                //if they are on separate app pools then there will be two processes, and this is irrelevant.
                if (_Instance == null)
                {
                    lock (lockObject)
                    {
                        if (_Instance == null)
                            _Instance = new CodeDiveEngine();
                    }
                }
                return _Instance;
            }
        }
        #endregion

        #region Constructor
        private CodeDiveEngine()
        {
            this.applicationContext = ApplicationContext.Current;
            this.umbracoApplication = (UmbracoApplication)HttpContext.Current.ApplicationInstance;
            this.fileService = this.applicationContext.Services.FileService;
            this.baseAppPath = umbracoApplication.Context.Server.MapPath("~/");
            this.viewsPath = string.Concat(baseAppPath, "views\\");
            this.partialViewsPath = string.Concat(viewsPath, "partials");
            this.domainAssemblies = AppDomain.CurrentDomain.GetAssemblies().ToList();

            //intialize syncs.
            this.EngageTemplateSync();
        }
        #endregion

        #region Template Sync
        public void EngageTemplateSync()
        {
            //initial Manual Sync
            this.SyncTemplates();
            //initialize a file systme watcher to look for new cshtml files in the views folder on the file system.
            this.templateWatcher = new FileSystemWatcher(viewsPath, "*.cshtml")
            {
                EnableRaisingEvents = true,
                NotifyFilter = NotifyFilters.FileName, 
                IncludeSubdirectories = false//,
                //InternalBufferSize = 16000000 //The engine syncs templates on app start, so a missed template isn't a huge deal, and you would need to add many templates at once for one to be missed.
            };            
            this.templateWatcherCreateHandler = new FileSystemEventHandler(delegate(object sender, FileSystemEventArgs e)
                {                    
                    RefreshInstalledTemplates();
                    SyncTemplates();
                });          
            this.templateWatcherDeleteHandler = new FileSystemEventHandler(delegate(object sender, FileSystemEventArgs e)
                {
                    //If you want to delete templates from umbraco use 
                    //fileService.DeleteTemplate("aliasHere", 0) where 0 is the user id, 0 = admin                    
                });
            this.templateWatcher.Created += this.templateWatcherCreateHandler;
            this.templateWatcher.Deleted += this.templateWatcherDeleteHandler;
        }
        private void SyncTemplates()
        {
            //prevent two thread's from being in Sync Templates at the same time.
            lock (syncLock)
            {
                this.RefreshInstalledTemplates();
                IEnumerable registeredTemplates = this.fileService.GetTemplates();
                IEnumerable notInstalledTemplates =
                    this.physicalTemplates.Where(pt => !registeredTemplates.Any(rt => rt.Name.Equals(Path.GetFileNameWithoutExtension(pt.FullName), StringComparison.InvariantCultureIgnoreCase)));
                foreach (FileInfo fi in notInstalledTemplates)
                {
                    //Register Template
                    string newName = Path.GetFileNameWithoutExtension(fi.FullName);
                    Template newTemplate = new Template(viewsPath, newName, newName);
                    fileService.SaveTemplate(newTemplate, 0);
                }
            }
        }
        private void RefreshInstalledTemplates()
        {
            lock (templateLock)
            {
                this.physicalTemplates = Directory.GetFiles(viewsPath, "*.cshtml").Select(p => new FileInfo(p)).ToList();
            }
        }
        #endregion

        #region Static Methods
        public static CodeDiveEngine StartEngine()
        {
            return CodeDiveEngine.Instance;
        }
        public void StopEngine()
        {
            //lock this operation in the event multiple Umbraco sites running on the same app pool Stop at the same time.
            if (CodeDiveEngine._Instance != null)
            {
                lock (stopLock)
                {
                    if (CodeDiveEngine._Instance != null)
                    {
                        this.templateWatcher.EnableRaisingEvents = false;
                        this.templateWatcher.Created -= this.templateWatcherCreateHandler;
                        this.templateWatcher.Deleted -= this.templateWatcherDeleteHandler;
                        this.templateWatcher.Dispose();
                        CodeDiveEngine._Instance = null;
                    }
                }
            }
        }
        #endregion
    }
)

5 comments:

  1. Hey there, because of the closing tags at the end of the file (fileinfo, itemplate, fileinfo, assembly) - this won't build. Is there something I'm missing? I added the class into a Helpers folder within my umbraco project.

    ReplyDelete
  2. Sorry, I forgot I started blogging here a long time ago.. It's missing the closing bracket }

    ReplyDelete
  3. I plan on moving this to my new blog soon as I'm done building it.

    ReplyDelete
  4. I plan on moving this to my new blog soon as I'm done building it.

    ReplyDelete
  5. Sorry, I forgot I started blogging here a long time ago.. It's missing the closing bracket }

    ReplyDelete