Upgrading Umbraco Packages for V8 (Whilst Maintaining Support for V7)

With the recent release of Umbraco version 8, a number of package developers are likely turning their attention to upgrading any packages they have developed to work with the new version of the content management system. I currently support a couple that we make use of at Zone, and, judging by downloads and forum requests, are also still getting some use in the wider community: Personalisation Groups is a package offering personsalisation features, and Umbraco Mapper, a mapping engine that supports mapping from Umbraco content into custom view models.

Before commencing I considered a few options for the upgrade. I outlined them in this forum post, hoping for some feedback from others but it seemed to slip under the radar! Broadly though, I debated between the following:

  1. Ignore V8 for now, and support just V7.
  2. Move onto V8 with the next release, and, other than leaving archived versions available, stop worrying about V7 support.
  3. Start a brand-new package, and new code repository, and maintain both separately.
  4. Lean on source control. Perhaps have two branches - master-v7 and master-v8. master-v7 will be just renamed from current master and be version 1.0, and master-v8 will diverge from it, doing all the necessary to upgrade to Umbraco 8 and bumping the package to 2.0. As new features are added, they could be merged or cherry-picked between branches.
  5. See if there's some way to share as much code as possible in a common project or library, allowing me to release a package or versions for both Umbraco 7 and 8, but minimising the maintenance burden of having to add features and fix bugs in two places.

Option 1. I counted out straight away - even if there'll be a bit of time as take-off ramps up, clearly the future of Umbraco is with version 8, so not supporting that version wouldn't be of help to anyone wanting to use that. There's an argument not to bother if the functionality provided by the package has been superseded with out of the box features of version 8. Possibly in time the personalisation package will be less necessary with the introduction of variants beyond just language, but for now, it still adds value.

Approach number 2. also isn't ideal. As well as the time for version 8 to be widely adopted, many projects simply won't end up being upgraded from version 7, and I'd like to still support them with any new features or bug fixes I might add to the packages in future.

Option 3. would work of course, but although there are API changes with version 8, fundamentally it's still the same product, so it would be a shame to have to maintain two separate code-bases that would be very similar.

Using source control as described in option 4. seemed like it would work, but might prove a bit confusing to manage, would have some merges to handle, and if I ever wanted to introduce a breaking change to the Umbraco 7 version, I'd have nowhere to go with a major version change (as 2.0 is taken for the Umbraco 8 version).

So 5. was what I looked to tackle. If interested, you can see the work in progress for the Personalisation Groups package under the feature/v8-support branch of the code-base (or, if you read this after they've been released, in master branch).

In the rest of this article I'll outline the steps I took and some of the specific issues I needed to amend to support the new Umbraco release.

Solution Structure

Currently - or previously, depending when you read this - there's a single project in the solution for the package (plus one for tests). To support V8, I broke this up into three:

  • A common project, that has no package dependency or reference to Umbraco.
  • A V7 project, that has a reference to the common project and to a V7 NuGet reference to UmbracoCms.Core
  • A V8 project, that has a reference to the common project and to the V8 NuGet references to UmbracoCms.Core and UmbracoCms.Web

Plus tests projects for each.

At the start of the migration the first and third projects, other than the references described, were empty.

Migration Approach

The aim at this point is to migrate as much code as possible from the V7 project up into the common one (which remember, has no knowledge of Umbraco). Leaving just the minimum I can get away with, that has to depend on Umbraco APIs.

Some of this was quite easy - any class that didn't have an Umbraco. reference in the using statements at the top clearly has no dependency on Umbraco APIs and could be moved up into the common project.

For others, I had to do a bit more surgery. Oftentimes there would be a class that did quite a bit of work, with only one or two methods partly dependent on an Umbraco reference. For these I looked to extract classes, such as base controllers that the custom controllers in the V7 project derived from, or static helper classes that the code in the V7 project could call out to.

There were some cases where I'd leaned on Umbraco helper classes that, although useful, weren't strictly necessary. For example, there was a handy class called Mandate in V7, gone in V8, that could be used for argument guard clauses in the constructors or methods, to ensure expected parameters were passed. There were also string extension methods, cache and config helpers I'd been using. For these I simply removed the Umbraco dependency and wrote - or simply copied - the code into my own solution. It's a bit more code to maintain of course going forward, but it's stable and not much in quantity, and now I'm in control of it and don't need to worry about it being different or missing in the two Umbraco versions.

For NuGet dependencies - where they weren't themselves dependent on Umbraco, I migrated the package reference from the Umbraco specific project to the common one. I found I could even do this with ClientDependency, a dependency also of Umbraco, as both version 7 and 8 used the same major version, so it was compatible with both.

All along this project I'm checking that the solution still compiles, migrated unit tests pass, and the package itself still works as it should on V7.

Updating for V8 - C#

When I got to the point where I thought I'd migrated as much code and logic as I could up into the common project, I copied what was left into the V8 project, referencing the V8 Umbraco binaries, and compiled. Obviously quite a bit broke! So now it was a case of going through all the red errors and updating them to the new method names, APIs and patterns used in Umbraco V8.

As evidenced by the quantity of posts to the V8 forum, there's a number of people digging into this and documentation hasn't quite caught up yet. So I was grateful here for a number of blog posts and people active on the forums for help. Particularly useful were blog posts by Daniël from Perplex and Stephan from HQ.

What follows is grab-bag of the specific things I found I needed to update (or could make use of in version 8):

Dependency injection is supported in V8. For V7 you could use it, but it wasn't a first-class citizen with the project, so it didn't really make sense to use for a package (as you'd have to reference a DI container that could then interfere with what any users of the package have already selected for this project).

As such I was able to change this code for V7:

public class MemberController : BaseJsonResultController
{
	public ContentResult GetMemberTypes()
	{
		var memberTypeService = ApplicationContext.Current.Services.MemberTypeService;
		var memberTypes = memberTypeService.GetAll()
			.OrderBy(x => x.Alias)
			.Select(x => x.Alias);
		return JsonResult(memberTypes);
	}
}

To this for V8:

public class MemberController : BaseJsonResultController
{
	private readonly IMemberTypeService _memberTypeService;

	public MemberController(IMemberTypeService memberTypeService)
	{
		_memberTypeService = memberTypeService;
	}
		  
	public ContentResult GetMemberTypes()
	{
		var memberTypes = _memberTypeService.GetAll()
			.OrderBy(x => x.Alias)
			.Select(x => x.Alias);
		return JsonResult(memberTypes);
	}
}

Dependency injection isn't mandatory, you can still use service location too in V8.

UmbracoContext is accessed via Current.UmbracoContext instead of UmbracoContext.Current.
To get the current page Id, UmbracoContext.PageId has become UmbracoContext.PublishedRequest.PublishedContent.Id.

The UmbracoHelper class still exists in V8, but a few method names have changed:

  • TypedContent --> Content
  • TypedContentAtRoot --> ContentAtRoot
  • DocumentTypeAlias --> ContentType.Alias (or IsDocumentType("someDocTypeAlias"))

The MembershipHelper also remains, so retrieving the currently logged in member is possible via Current.UmbracoHelper.MembershipHelper.GetCurrentMember().

Property editors are referred to as data editors in code now, leading to PublishedPropertyType.PropertyEditorAlias becoming PublishedPropertyType.EditorAlias, the PropertyEditor attribute is now DataEditor.

For converting values within property editors, there are some new overrides that replace attributes.

V7:

[PropertyValueType(typeof(PersonalisationGroupDefinition))]
[PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.ContentCache)]
public class PersonalisationGroupDefinitionPropertyValueConverter :
	PropertyValueConverterBase
{
	public override bool IsConverter(PublishedPropertyType propertyType)
	{
		return
			propertyType.PropertyEditorAlias.Equals(
				AppConstants.PersonalisationGroupDefinitionPropertyEditorAlias);
	}

	public override object ConvertSourceToObject(PublishedPropertyType propertyType,
												 object source,
												 bool preview)
	{
		if (source == null || UmbracoContext.Current == null)
		{
			return null;
		}

		return
			JsonConvert.DeserializeObject<PersonalisationGroupDefinition>(
				source.ToString());
	}
}

V8:

public class PersonalisationGroupDefinitionPropertyValueConverter :
	PropertyValueConverterBase
{
	public override Type GetPropertyValueType(
		PublishedPropertyType propertyType)
			=> typeof(PersonalisationGroupDefinition);

	public override PropertyCacheLevel GetPropertyCacheLevel(
		PublishedPropertyType propertyType)
			=> PropertyCacheLevel.Element;

	public override bool IsConverter(PublishedPropertyType propertyType)
	{
		return propertyType.EditorAlias.Equals(
			AppConstants.PersonalisationGroupDefinitionPropertyEditorAlias);
	}

	public override object ConvertSourceToIntermediate(
		IPublishedElement owner,
		PublishedPropertyType propertyType,
		object source,
		bool preview)
	{
		return source;
	}

	public override object ConvertIntermediateToObject(
		IPublishedElement owner,
		PublishedPropertyType propertyType,
		PropertyCacheLevel referenceCacheLevel,
		object inter,
		bool preview)
	{
		if (inter == null)
		{
			return null;
		}

		return
			JsonConvert.DeserializeObject<PersonalisationGroupDefinition>(
				inter.ToString());
	}
}

One of the more significant changes is hooking into Umbraco events - rather than deriving a class from ApplicationEventHandler and overriding ApplicationStarting or ApplicationStarted, you use components and composition. So for registering custom routes, we have this change (both calling to a static class that defines the routes, held in the common project).

V7:

public class UmbracoApplicationEvents : ApplicationEventHandler
{
	protected override void ApplicationStarted(
		UmbracoApplicationBase umbracoApplication,
		ApplicationContext applicationContext)
	{
		RouteConfig.RegisterRoutes(RouteTable.Routes);
	}
}

V8:

public class PackageComposer : IUserComposer
{
	public void Compose(Composition composition)
	{
		composition.Components().Append<RoutingComponent>();
	}
}

public class RoutingComponent : IComponent
{
	public void Initialize()
	{
		RouteConfig.RegisterRoutes(RouteTable.Routes);
	}

	public void Terminate()
	{
	}
}

In another case I had some events I wanted to hook into the page request lifecycle. These changed as follows:

V7:

public class RegisterApplicationEvents : ApplicationEventHandler
{
	protected override void ApplicationStarted(
		UmbracoApplicationBase umbracoApplication,
		ApplicationContext applicationContext)
	{
		UmbracoApplicationBase.ApplicationInit += ApplicationInit;
	}

	private static void ApplicationInit(object sender, EventArgs e)
	{
		var app = (HttpApplication)sender;
		app.PostRequestHandlerExecute += UserActivityTracker.TrackSession;
	}
}

V8:

public class PackageComposer : IUserComposer
{
	public void Compose(Composition composition)
	{
		composition.Components().Append<NumberOfVisitsComponent>();
	}
}

public class NumberOfVisitsComponent : IComponent
{
	public void Initialize()
	{
		UmbracoApplicationBase.ApplicationInit += ApplicationInit;
	}

	private static void ApplicationInit(object sender, EventArgs e)
	{
		var app = (HttpApplication)sender;
		app.PostRequestHandlerExecute += UserActivityTracker.TrackSession;
	}

	public void Terminate()
	{
	}
}

Updating for V8 - JavaScript

When it came to the JavaScript parts of the package, I adopted a slightly different approach. Here I've moved all the JavaScript into the common project, even though, of course, it has dependencies on Umbraco too that might have - and as it turned out, do have - breaking changes. As a dynamic language though, JavaScript is more amenable to "reflection" than the statically typed C# is, so I decided to see if I could handle the changes in code.

There's a danger that this might make the code more convoluted, when conditionals and code branches for V7 and V8, but as it turned out the differences that I needed to be concerned with were quite small, and relatively easily handled.

One change was in the removal of the angular service dialogService, replaced with editorService (thanks Kevin). In code I could then do the following, checking for the existence of one injected object, using it if it exists, or if not, falling back to the V8 alternative.

if (dialogService) {
	// V7 - use dialogService
	dialogService.open(
		{
			template: templateUrl,
			callback: function(data) {
				...
			},
			closeCallback: function() {
				...
			}
		});
} else {
	// V8 - use editorService
	editorService.open(
		{
			title: "Edit definition detail",
			view: templateUrl,
			size: "small",
			submit: function (data) {
				...
				editorService.close();
			},
			close: function () {
				...
				editorService.close();
			}
		});
}

In terms of actually injecting the services into the angularjs controller, I found I couldn't simply reference them in the controller's constructor, like I had in V7:

angular.module("umbraco")
	.controller("UmbracoPersonalisationGroups.PersonalisationGroupDefinitionController",
		function ($scope, dialogService, editorService) {
			...
		});  

This would error, as one of the dependencies, in either version, could not be found. I got around it by using the $injector service that comes with angularjs:

angular.module("umbraco")
	.controller("UmbracoPersonalisationGroups.PersonalisationGroupDefinitionController",
		function ($scope, $injector) {

			var dialogService = null;
			var editorService = null;
			try {
				dialogService = $injector.get("dialogService");
			} catch (e) {
				editorService = $injector.get("editorService");
			}
			
			...
		});

I ran into at least one issue with the fact that Umbraco 8 comes with a more recent version of angularjs than version v7 ships with. This was in call-back methods, where I was handling the response via the success() method. This no longer exists, so switched to using then().

 

Updating for V8 - CSS

The final set of breaking changes I needed to consider where in back-office CSS. I'd used a number of Umbraco provided angularjs components like umbraco-pane, umbraco-control-group to render the package user interface in a similar style to the rest of the back-office. These too have changed, and I had some examples where elements no longer rendered how I would like.

This actually turned out to be one of the trickiest to resolve if I wanted to keep a single view file for maintenance. In some cases, I reverted to no longer using the Umbraco styles and providing my own, which isn't as clean but gave me more control over the layout. In others, I didn't really want to have to add all those custom styles and where I had to make a choice, accepted some minor deterioration in presentation in V7 for the ideal layout in V8.

Conclusions

With a bit of help from blog posts, forums and the developing documentation, the changes to get the package working on version 8 didn’t turn out to be too onerous, and, at least for my purposes with the Personalisation Groups package, it was also feasible to separate out the code as described in this article, so I could support both versions and reduce the maintenance burden moving forward.

Whether other packages are so amenable to this approach will likely be a factor of how dependent on Umbraco they are. By definition, all Umbraco packages will be to some extent of course, but when I dug in it was apparent just how much didn't have a direct dependency and thus was possible to refactor out into a common project.

At the time of writing I'm doing some final testing and in conclusion hope to be able to release the new version for Umbraco 8, as well as an update for Umbraco 7 that maintains current functionality, but internally uses the new solution architecture.

About the Author

Andy lives in Bassano del Grappa, Italy and works remotely for Zone, a digital agency now part of Cognizant Digital. He's worked with Umbraco for many years, on projects, packages and core contributions. Find him on Twitter at @andybutland.

comments powered by Disqus