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

Tuesday, May 20, 2014

Using MSBuild and PowerShell to import Files into a Project Visual Studio 2013, Umbraco

Recently I was trying to setup a Visual Studio Project to import Views, Css, Images, etc into an Umbraco Project I've been working on.

Here's the basic setup,
  1. An empty .Net 4.5 Web Project
    1. Add the UmbracoCMS nuget Package
  2. A 2nd Empty Web Project
    1. Add the Umbraco Core Binaries Package

The general idea is to develop a plugin for Umbraco in the 2nd project, but have all of it's content included via an Import Project into the Umbraco Project.  When it's done and packaged, this is how it will be incorporated into any Umbraco Project.

In the hunt to do this, I found an example over at http://blog.samstephens.co.nz/2010-10-18/msbuild-including-extra-files-multiple-builds/ by Sam Stephens.

However, I was unable to get it to work in Visual Studio 2013.  So I thought, it would be nice if I could just pass this off to a power shell script.  Which led me to another article by Jason Lee, http://www.asp.net/web-forms/tutorials/deployment/advanced-enterprise-web-deployment/running-windows-powershell-scripts-from-msbuild-project-files.

So taking the two example's and putting them together, I ended up with the following;

First Step...
  • Create 2 Powershell Scripts in the 2nd Project
    • Note, you can use the Visual Studio Extension Click Here to add powershell intellisense to visual studio
  • PostBuild.ps1
  • PreBuild.ps1
PreBuild.ps1 Contents

// Comment
 Param(
 [Parameter(Mandatory=$True,Position=1)][string]$sourceDirectory,
 [Parameter(Mandatory=$True,Position=2)][string]$destinationDirectory
)

 Write-Host "Before Build!!"
 Write-Host "Source: $sourceDirectory"
 Write-Host "Destination: $destinationDirectory"
 #Do anything else you want to do Pre Build here


PostBuild1.ps1 Contents

 Param(
 [Parameter(Mandatory=$True,Position=1)][string]$sourceDirectory,
 [Parameter(Mandatory=$True,Position=2)][string]$destinationDirectory
)
#CONFIG
$exclude = @('*.cs') #files to exclude go here
$folderNoMatch = [string[]]"\\views\\somefoldertoexclude\\?"#,"More Elements Here","And Another Element","And Another","Etc" #folders to exclude go here
#ENDCONFIG

#Convert the parameter inputs into DirectoryInfo objects "Note: DirectoryInfo is not standardized on whether a folder ends with \ or without.  It is based on whatever the input path is.  In this case with \.  How when enumerating directories they come back without the \ as such, I use TrimEnd("\") all over the place in here.
$sourceDI = [System.IO.DirectoryInfo]$sourceDirectory
$destinationDI = [System.IO.DirectoryInfo]$destinationDirectory
#this is the line that get's the ChildItems of the folder whos contents we are copying.  -Exclude is used here on the file wildcards, but does not work on Directories, so don't use Directories here.
$itemsToCopy = Get-ChildItem $sourceDirectory -Recurse -Exclude @('*.cs')
foreach ($item in $itemsToCopy){  
 $processItem = $true
 #process the folder matches, if it matches one set $processItem to false which skips the item
 foreach ($match in $folderNoMatch ){
   if ($item.FullName -match $match){
  #skip this folder
  $processItem = $false
   }
 }  
 if ($processItem){
  $subPath = $item.FullName.Substring($sourceDI.FullName.Length)
  $destination = Join-Path $destinationDirectory $subPath
  if ($item -is [System.IO.DirectoryInfo]){
   $itemDI = [System.IO.DirectoryInfo]$item
   $sourcePath = $sourceDI.FullName.TrimEnd("\")
   $sourceLength = $sourcePath.Length
   #If the item being copied is the folder itself, we need the destination to be the item's parent folder name, so this folder gets created in the right place
   $destination = Join-Path $destinationDI.FullName.TrimEnd("\") $itemDI.Parent.FullName.TrimEnd("\").Substring($sourceLength)
  }
  Copy-Item -Path $item.FullName -Destination $destination -Force}
 }
Write-Host [PROJECT: IMPORT COMPLETE]

Now you need to add a .Targets file to the 2nd Project


<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="11.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Target Name="XYZProjectPath">
    <CreateProperty Value="$([System.IO.Path]::GetFullPath('..\XYZ.Project'))">
      <Output TaskParameter="Value" PropertyName="XYZProjectPath"/>
    </CreateProperty>
  </Target>  
  <Target Name="BeforeBuild" DependsOnTargets="XYZProjectPath">
    <PropertyGroup>
      <PowerShellExe Condition="'$(PowerShellExe)'==''">%WINDIR%\System32\WindowsPowerShell\v1.0\powershell.exe</PowerShellExe>
      <ScriptLocation>$(XYZProjectPath)\PreBuild.ps1</ScriptLocation>
    </PropertyGroup>
    <Exec Command="$(PowerShellExe) -NonInteractive -executionpolicy Unrestricted -command &quot;&amp; { &amp;&apos;$(ScriptLocation)&apos; &apos;$(XYZProjectPath)\UmbracoImport&apos; &apos;$(ProjectDir)&apos;} &quot;" />
  </Target>
  <Target Name="AfterBuild" DependsOnTargets="XYZProjectPath">
    <PropertyGroup>
      <PowerShellExe Condition="'$(PowerShellExe)'==''">%WINDIR%\System32\WindowsPowerShell\v1.0\powershell.exe</PowerShellExe>
      <ScriptLocation>$(XYZProjectPath)\PostBuild.ps1</ScriptLocation>
    </PropertyGroup>
    <Exec Command="$(PowerShellExe) -NonInteractive -executionpolicy Unrestricted -command &quot;&amp; { &amp;&apos;$(ScriptLocation)&apos; &apos;$(XYZProjectPath)\UmbracoImport&apos; &apos;$(ProjectDir)&apos;} &quot;" />
  </Target>
</Project>

So what have we done here in the targets file?

I created a Variable called XYZProjectPath. This variable can be used to calculathe the path to the project that is being copied from. I set it as a target's file that the other targets depend on.

Second, I created two Target's, one for BeforeBuild and one for AfterBuild. In each Target I created a node for PowerShellExe. This is where the scripts are called. ScriptLocation is set to the XYZProjectPath\PreBuild.ps1 and PostBuild.ps1 in the 2nd Target.

The interesting part is the Exec Command. I am using the -command command line argument of powershell.exe to send the commands to the powershell exe. As such they have to be encoded in a script block using &amp; and &apos; for & and '. What we do here is tell powershell.exe to run the Script and pass it $(XYZProject)\UmbracoImport for the source parameter and $(ProjectDir) for the destination.
$ProjectDir will be set to the directory of the project that is importing this .targets file.

UmbracoImport is the name of the folder in my project where all of my UmbracoContent is stored that I want copied into the Umbraco root folder (because it runs in IIS) and is managed in a visual studio project because I have it source controlled with SubVersion.

Now to use this .Targets file, simply unload the First project and edit it's project file, go down near the
bottom and add a line for <Import Project="..\XYZ.Project\XYZ.Project.targets" /> Assuming both of the projects are in the same folder.

When you do a fresh build after words, you should see similar output in the output window.

3> Before Build!! 3> Source: D:\XYZ\SourceTrunk\SomeProject\UmbracoImport 3> Destination: D:\XYZ\SourceTrunk\UmbracoHost\ 3> UmbracoHost -> D:\XYZ\SomeProject\UmbracoHost\bin\UmbracoHost.dll 3> After Build!! 3> Source: D:\XYZ\SourceTrunk\SomeProject\UmbracoImport 3> Destination: D:\XYZ\SourceTrunk\UmbracoHost\

All of the Content in UmbracoImport is now in the root of UmbracoHost! And because we are dealing with powershell here, there really is no limit to what you can do.

Eventually I'll write some code to merge web.configs so I can have config entries etc that get merged into connection strings and other areas in the destination web.config, and add a mechanism for ignoring existing files.

Edits: Change the Copy code in the PostBuild.ps1 script to use Get-ChildItem instead of Get-Item. (5/20/2014)