Umbraco V7 Compatible packages

Heads Up!

This article is several years old now, and much has happened since then, so please keep that in mind while reading it.

Umbraco V7 is released almost a month ago. I'm working hard to make the current packages compatible with Umbraco V7. I've already converted CMSImport, Mediaprotect and MemberExport. The process of converting is pretty straight forward but I had to make some tweaks in my packages that I want to share in this article.

Maybe it's just me, but I want to have a single package installer for all supported versions of Umbraco. Otherwise users will download the wrong version of the package and their environment will explode.  You may guess who gets blamed ;-). This is the way I developed my packages in the past and I really want to continue this when making my packages compatible with Umbraco V7.  

How to make packages backwards compatible?

All you need to do to make a package compatible with an older version of Umbraco is to compile against that version. For example CMSImport is compatible with version 4.5.2 of Umbraco and above so the whole project is compiled against that version. This ensures I can only use the classes and methods available in that version.  This method works very well for me, even with the new Content and media services. The Umbraco core team did a really great job to ensure the old methods would still work.

Then V7 came...

With V7 not only the UI layer changed from Webforms to Angular JS but because of that the following breaking changes got introduced:

  • The ContentTreeController replaced the old BaseTree
  • Legacy events won't execute when they are initiated form the new ContentTreeController
  • Legacy data types don't work anymore
VsStructure

This was a serious issue for me. It took me a few days to figure out what to do. I realized quickly that I needed to split up parts of the functionality into multiple projects. For example all event handlers were part of the MediaProtect assembly  in previous releases but are now separated into multiple Assemblies:

  1. MediaProtect.Events . Targeted at Umbraco V7
  2. MediaProtect.Events.Legacy. Targeted at all versions before Umbraco V7

This allowed me to use the new events in the MediaProtect.Events which references the Umbraco V7 assemblies, the legacy events project still references the old 4.5.2 assemblies.

Packager challenge

This split-up of assemblies fixed the backwards compatibility issue I had, but introduced a new problem. How can I create an installer that works on all versions of Umbraco? Usually this isn't an issue but since we use some of the methods introduced in umbraco V7 for the MediaProtect.Events project that don't exist in older versions of Umbraco the logfiles would have been full with missing method exceptions.

Package actions to the rescue

If you are familiar with creating packages for Umbraco. You've probably heard of package actions. Package actions are simple classes that you can include in your assembly and gets executed by the Umbraco package installer by providing an XML Snippet for configuration telling the action what to do.

In this case I wrote a package action that I call ConditionalFileDeploy that gets a source and target location. It also can take a min and optional max version.  During install it will inspect the Umbraco version number and when it meets the version criteria it will copy the file to the bin folder and otherwise delete it.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web;
using System.Xml;
using MediaProtect.Umbraco.PackageActions.Helpers;
using umbraco.BusinessLogic;
using umbraco.cms.businesslogic.packager.standardPackageActions;
using umbraco.interfaces;

namespace MediaProtect.Umbraco.PackageActions
{
     /// <summary>
    /// Copies a file when it doesn't exists in the target location
    /// </summary>
    public class ConditionalFileCopyAction : IPackageAction
    {
        /// <summary>
        /// Executes action
        /// </summary>
        /// <param name="packageName">Name of the package.</param>
        /// <param name="xmlNode">The XML node.</param>
        /// <returns></returns>
        public bool Execute(string packageName, XmlNode xmlNode)
        {
            var source = HttpContext.Current.Server.MapPath(XmlHelper.GetAttributeValueFromNode(xmlNode, "source"));
            var target = HttpContext.Current.Server.MapPath(XmlHelper.GetAttributeValueFromNode(xmlNode, "target"));
            var minversion = new UmbracoVersionInfo(XmlHelper.GetAttributeValueFromNode(xmlNode, "minversion"));
            var maxversion = new UmbracoVersionInfo(XmlHelper.GetAttributeValueFromNode(xmlNode, "maxversion"));

            Log.Add(LogTypes.Debug, -1, string.Format("Executing Package Action {0} with Params  Source:{1} target:{2} minversion:{3} maxversion{4} currentVersion{5}", Alias(), source, target, minversion, maxversion, UmbracoVersionInfo.Current));

            if (CanCopy(UmbracoVersionInfo.Current,minversion,maxversion))
            {
                //File doesn't exists
                //Make sure folder gets created
                var targetFolder = Path.GetDirectoryName(target);
                Directory.CreateDirectory(targetFolder);

                //Copy file
                File.Copy(source, target);
            }
            File.Delete(source);
            return true;
        }

        /// <summary>
        /// Determines whether the specified version is valid to copy.
        /// </summary>
        /// <param name="currentVersion">The current version.</param>
        /// <param name="minVersion">The min version.</param>
        /// <param name="maxVersion">The max version.</param>
        /// <returns>
        ///   <c>true</c> if [is valid version] [the specified current version]; otherwise, <c>false</c>.
        /// </returns>
        public bool CanCopy(UmbracoVersionInfo currentVersion, UmbracoVersionInfo minVersion,
                                   UmbracoVersionInfo maxVersion)
        {
            return currentVersion.IsGreaterOrEqual(minVersion) && (!maxVersion.IsSpecified() || currentVersion.IsSmallerOrEqual(maxVersion));
        }

        public string Alias()
        {
            return "MediaProtect_ConditionalFileCopyAction";
        }

        public bool Undo(string packageName, XmlNode xmlData)
        {
            return true;
        }

        public XmlNode SampleXml()
        {
            return helper.parseStringToXmlNode(string.Format("<Action runat=\"install\" alias=\"{0}\" source=\"~/app_data/temp/package.config\" target=\"~/umbraco/plugins/package/package.config\" minversion=\"4\" maxversion=\"6.9.1\" />", Alias()));
        }
    }
}

Conditional file copy action

Using the above package action I could include both dll's into my package but instead of specifying the /bin folder I specified the app_data/temp folder as target location

<file>
  <guid>Mediaprotect.Events.dll</guid>
  <orgPath>/app_data/temp/Mediaprotect</orgPath>
  <orgName>Mediaprotect.Events.dll</orgName>
</file>
<file>
  <guid>Mediaprotect.Events.Legacy.dll</guid>
  <orgPath>/app_data/temp/Mediaprotect</orgPath>
  <orgName>Mediaprotect.Events.Legacy.dll</orgName>
</file>

Package files

And  use this snippet to copy the correct dll

<Action runat="install" alias="MediaProtect_ConditionalFileCopyAction" source="~/app_data/temp/Mediaprotect/Mediaprotect.Events.dll" target="~/bin/Mediaprotect.Events.dll" minversion="7" />
<Action runat="install" alias="MediaProtect_ConditionalFileCopyAction" source="~/app_data/temp/Mediaprotect/Mediaprotect.Events.Legacy.dll" target="~/bin/Mediaprotect.Events.Legacy.dll" minversion="4" maxversion="6.*" />

Package actions

This magic XML snippet basically says Copy MediaProtect.Events.dll to the bin folder when the Umbraco version is 7 or higher. Or copy MediaProtect.Events.Legacy.dll to the bin folder when the Umbraco between 4 and 6.

UI tips

So that was all to make sure our packages would still work in Umbraco V7 and still use a single package installer file. Of course it didn't look "Belle" without a few UI changes. What I did was creating a method to check which icon to display in the tree. Something similar Tim used in his blog post.

Buttons

Another thing I found that the new UI had nice looking buttons. Could be just Twitter bootstrap but I'm not familiar with this yet.  But when installing Media protect buttons were Ugly compared to the ones used by Umbraco. This could easily be solved by adding some css classes to the buttons.

  1. btn btn-success
  2. btn btn-danger
  3. btn

One last thing make sure to use the default Umbraco controls as described on the upgrade instructions page. This ensures that the lay-out of the page looks consistent.

Hope you can use this article when updating your packages for Umbraco V7 during the Christmas break.

Richard Soeteman

Richard is on Twitter as