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.
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
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:
- MediaProtect.Events . Targeted at Umbraco V7
- 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()));
}
}
}
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>
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.*" />
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.
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.
- btn btn-success
- btn btn-danger
- 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 @rsoeteman