Monday, 21 November 2011

Creating an Umbraco 5 Hive Provider with Custom Tree and Editor Plugin

Having used Umbraco CMS on a couple of projects in the past and been very impressed with it, I’ve been keen to follow progress on version 5 (or “Jupiter”) – a rewrite of the code-base to use ASP.Net MVC. In particular I’ve been looking recently at some of the extension points of the platform.

One of these is building upon the data access abstraction layer that is being developed for Umbraco 5 – known as Hive. Umbraco itself will use NHibernate for data access, but via the Hive abstraction. The idea is that this default data access method could be swapped out – but perhaps more importantly by building a hive provider developers can expose other data sources to their Umbraco templates and back office.

The back-office itself is also very pluggable, with the various node trees and editor interfaces also being extendable with your own custom implementations. In light of this I’ve been looking at a small project to create a hive provider for a custom database, along with a custom tree and editor for managing the content from within the Umbraco interface. The use case here might be that we already have data within a legacy database. Rather than converting it to document types and importing into Umbraco itself, we might want to keep it in the existing system whilst also providing a familiar interface for both back office editors to manage the content and template developers to surface the content on our Umbraco based website.

Credits and Further Reading

Before going further first would like to note that others having been working on a similar project and the work they have shared has been invaluable in moving forward with this. Hopefully by similarly publishing the code I’ve adapted and extended will help others on the same path.
Building the Solution

In order to implement the hive provider and custom interfaces I’ve set up a separate VS.Net solution with 3 class library projects. I’ve also downloaded the full source of Umbraco and set this up on IIS and SQL server, so I have an installation of Umbraco running.

In order to deploy the functionality to the Umbraco installation the compiled assemblies need to be copied to /App_Plugins/Packages/(PackageName)/lib. To facilitate this I’ve used a post-build step that is set up with VS.Net to run an xcopy command on every successful build, e.g.:

xcopy "$(ProjectDir)bin\Debug\WebMatters.UmbracoAddOns.MusicCatalogHiveProvider.dll" "$(ProjectDir)..\..\UmbracoSource\Source\Web Apps\Umbraco.CMS.Web.UI\App_Plugins\Packages\WebMatters\lib" /i /f /y

It’s also necessary to restart the Umbraco web application when updates are deployed so that the updated are reloaded. To make this happen you can re-save web.config, or use another post-build script to copy an empty text file into the /bin folder.

With this setup deployment was fairly painless, apart from one issue with the embedded views that I used for the editor. I found at times I either needed to delete the temporary internet files or rename my view in order for changes to be picked up.

The Hive Provider

As mentioned I’m looking to create a hive provider to read and update data from a custom database, so the first thing I need is to set this up. To start things simply I’ve just got a single table for my “music catalog” database, a table of Artists:



Within the project’s Entity folder I’ve created a POCO class called Artist representing an artist and derived from the Hive class BaseEntity.

For data access to the custom database I’ve simply used ADO.Net with inline SQL, though of course this could be swapped with an ORM or stored procedures as preferred. The code for this is within SqlMusicCatalogRepository – providing basic CRUD methods.

The Hive Repository class inheriting from AbstractEntityRepository contains a reference to this custom data access class, and the appropriate methods to perform the reads and updates are passed along. Some helper methods have been created to convert between the POCO Artist and the base generic entity used within Hive called TypedEntity.

The TypedEntity contains some properties that will be common to all entities managed through Hive – things like an ID and created/last modified dates. It also contains reference to a collection of attributes for the custom fields – in my case the Name and Description of the Artist. These attributes and their definitions are defined within the various classes contained in the Schema folder.

Once the assembly has compiled and been deployed to the lib folder within the package, in order for Umbraco to use it it's necessary to make an amend to the file umbraco.hive.config found within /App_Data/Umbraco/Config. See the readme.txt file in the code download for details.

The Tree plugin

Tree plugins within Umbraco 5 are MVC controllers, inheriting from the Umbraco class SupportsEditorTreeController. You need to decorate the class it with a custom annotation called [Tree], providing a name and unique key. It’s also necessary to add [assembly: AssemblyContainsPlugins] to the project’s AssemblyInfo.cs file. This way when Umbraco loads it can pick up that the deployed dll contains a custom tree plugin.

Within the overridden GetTreeData method a reference to the hive provider is created and a method to get all entities is made. The resulting list is looped and a node created for each entity, setting properties like the name, the URL of the associated editor, the icon and the context menu items.

It's also necessary to modify a config file in order to tell Umbraco where the tree should live. The file is umbraco.cms.trees.config
found within /App_Data/Umbraco/Config. Again see the code download readme.txt file for details.

The Editor plugin

Editor plugins are also controllers, inheriting from StandardEditorController. As with the tree, steps are required to annotate the class and assembly info in order to notify Umbraco that the dll contains an editor plugin. The unique key provided here should match with the one created within the tree’s EditorControllerId property, such that the actions made on the tree nodes match up with the appropriate editor methods.

Taking just the Edit method as an example, as with the tree the first step is to get a reference to the hive provider and retrieve a single entity based on the passed Id. The resulting TypedEntity is passed to a strongly typed view for rendering the edit form.

I’m using embedded views to avoid having to deploy more than just a single assembly for each project into my Umbraco instance. This is easily done by creating a view as normal, and within its properties setting the Build Action to Embedded Resource. You then need to reference the path and namespace of the view when returning a ViewResult from the controller.

As with a standard MVC editing interface the form posts back to another method (also called Edit, but with a signature providing a reference to the posted form data). I’ve not attempted to use model binding here, rather am retrieving data direct from the Request.Form collection and using this to construct the TypedEntity. This is then passed to the Hive repository’s AddOrUpdate method in order to persist the change. Finally I’m returning a redirect to another view that simply displays a success message.

Accessing Hive data from templates

So thus far we have the list of artists displaying and being editable via the back-office, but one of the reasons for going via the Hive abstraction is that the content can also be rendered in the front-end templates using a consistent method. The following Razor code can be placed within a cshtml view and will display the list of artists from the database:


<h2>Artist list</h2>
<ul>
@using Umbraco.Framework.Persistence.Model;
@using WebMatters.UmbracoAddOns.MusicCatalogHiveProvider.Entity;

@{
var hive = RoutableRequestContext.Application.Hive.GetReader(new Uri("webmatters-music-catalog://"));
using (var uow = hive.CreateReadonly()) {
var artists = uow.Repositories.GetAll().ToArray();
foreach (var artist in artists) {
<li>@artist.Attributes["Name"].DynamicValue</li>
}
}
}
</ul>
Update: see subsequent post for a better way to do this using surface controllers.

20/1/12: Further updated for RC 2 thanks to Ruben's comment below.

Known issues and further development

The current status of the hive provider tree and editor is to provide basic methods for reading, creating, updating and deleting the content through the Umbraco back office; and display in the view templates. Ideally I’d like to extend this to cover a wider range of use cases, in particular:
  • One-many relations: e.g. managing a list of albums for each artist
  • Many-many relations: e.g. managing a list of tags for each album
There are also a couple of key functional issues:
  • I haven’t yet managed to integrate Umbraco notifications on data updates. Currently I’m displaying a new view after updates with a message on it to manually reload the nodes. Obviously this isn’t ideal – would be better to have the usual notification dialog and re-synching of the tree automatically as needed.
  • There’s no validation set up yet.
Get the code

If you’d like to look further you can download the code here.

As I say there’s still a fair bit to I’d like to look at and improve and I’m sure there may be areas where I could have implemented things better – so if anyone would like to comment, or better yet go in and improve the code, please feel free to do so. Depending on feedback may look to add to the Umbraco 5 Contrib project too.

15 comments:

  1. Hey Andy,

    It looks good, but I can't get it to work on Umbraco 5 RC1.

    I get a nice error when loading any page, while the dll is deployed under packages:

    Could not load types from assembly WebMatters.UmbracoAddOns.MusicCatalogHiveProvider, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null, errors:
    Exception: System.TypeLoadException: Derived method 'PerformGet' in type 'WebMatters.UmbracoAddOns.MusicCatalogHiveProvider.Entity.Repository' from assembly 'WebMatters.UmbracoAddOns.MusicCatalogHiveProvider, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' cannot reduce access.
    Exception: System.TypeLoadException: Method 'GetReadonlyRepository' in type 'WebMatters.UmbracoAddOns.MusicCatalogHiveProvider.Schema.RepositoryFactory' from assembly 'WebMatters.UmbracoAddOns.MusicCatalogHiveProvider, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' does not have an implementation.
    Exception: System.TypeLoadException: Method 'GetReadonlyRepository' in type 'WebMatters.UmbracoAddOns.MusicCatalogHiveProvider.Entity.RepositoryFactory' from assembly 'WebMatters.UmbracoAddOns.MusicCatalogHiveProvider, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' does not have an implementation.

    It might have something to do with missing the AssemblyInfo.cs file. The reference existed in the project, but the file did not. However, it compiled without this file.

    Any ideas how to fix this?

    ReplyDelete
  2. I haven't specifically rechecked it with RC1 Ruben, but looking at the error it sounds like something has changed in the code base that's not incompatible. Will look to do this and update the code as necessary.

    Thanks for the note on AssemblyInfo.cs - you're right, I had this excluded from checking into source control. It gets regenerated as you've found, but you would have had to re-add the [assembly: AssemblyContainsPlugins] line that Umbraco requires to recognise the plugin.

    ReplyDelete
  3. Hey Andy,

    Thanks for the update for RC2, I finally got it working.
    One change was necessary, however; maybe you can alter the code snippet in your blog. Instead of the untyped calls, which do not work with RC2, I used the following:

    <h2>Artist list</h2>
    <ul>
    @using Umbraco.Framework.Persistence.Model;
    @using WebMatters.UmbracoAddOns.MusicCatalogHiveProvider.Entity;

    @{
    var hive = RoutableRequestContext.Application.Hive.GetReader(new Uri("webmatters-music-catalog://"));
    using (var uow = hive.CreateReadonly<IMusicCatalog>())
    {
    var artists = uow.Repositories.GetAll<TypedEntity>().ToArray();
    foreach (var artist in artists) {
    <li>@artist.Attributes["Name"].DynamicValue</li>
    }
    }
    }
    </ul>

    Cheers,
    Ruben

    ReplyDelete
  4. Thanks Ruben - have updated.

    ReplyDelete
  5. I get a nice error too, but not the same as Ruben:
    Could not load types from assembly WebMatters.UmbracoAddOns.MusicCatalogHiveProvider, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null, errors:
    Exception: System.TypeLoadException: Declaration referenced in a method implementation cannot be a final method. Type: 'WebMatters.UmbracoAddOns.MusicCatalogHiveProvider.Entity.Repository'. Assembly: 'WebMatters.UmbracoAddOns.MusicCatalogHiveProvider, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'.
    Exception: System.TypeLoadException: Declaration referenced in a method implementation cannot be a final method. Type: 'WebMatters.UmbracoAddOns.MusicCatalogHiveProvider.Schema.Repository'. Assembly: 'WebMatters.UmbracoAddOns.MusicCatalogHiveProvider, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'

    Any idea what is going wrong. I use by the way the latest version (31 Jan 2012) of Umbraco.

    ReplyDelete
  6. Got the same error so I updated the references to the latest DLL's in the latest download. I got the following error:

    Error 1 'WebMatters.UmbracoAddOns.MusicCatalogHiveProvider.Schema.Repository' does not implement inherited abstract member 'Umbraco.Hive.ProviderSupport.AbstractSchemaRepository.PerformRemoveRelation(Umbraco.Framework.Persistence.Model.Associations.IRelationById)' C:\Development\abutland\abutland73-web-matters-umbraco5-addons-6a4a79d31e99\MusicCatalogHiveProvider\Schema\Repository.cs 17 18 MusicCatalogHiveProvider

    Any ideas?

    ReplyDelete
  7. Hi Gary, Mounhim - looks like I need to make a couple of updates to get this working again with RTM. Will do so when I get a chance.

    ReplyDelete
  8. That's done now - couple of changes to method names.

    ReplyDelete
  9. So now we have a sweet way of accessing hive data from templates. But is there any possibility of mapping urls to hive content items?

    So for instance, if the database contains an artist U2, it would be nice to have the url /artists/U2 render a template with the artist hive content item passed in, or fetched from the template.

    Any idea how to do this?

    ReplyDelete
  10. Thanks for this, it's great.

    One problem I kept having was whenever I changed something in your project and built, it would not refresh in umbraco, no matter what I did. Clearing cache, rebuilding, "touching" the bin directory etc.

    In the end I had to resort to emptying the ASP.NET Temporary Files directory each time I made a change... this often causing problems in itself.

    Is this an easily rectifiable problem?

    ReplyDelete
  11. Thanks for this post - super helpful way to start with Umbraco 5.

    I am wondering if you know if one could swap out your standard

    Html.TextArea for the description and use instead Umbraco's Rich Text editor allowing the user access to Umbraco links, styles and images?

    This would be the killer piece of functionality for Umbraco 5 allowing you to give the users an integrated Umbraco experience but have your data in a proper, manageable external database.

    ReplyDelete
  12. Hey Andy,

    Thanks a lot for the article and code. It helps me quite a bit since I am new to Umbraco. I am using Umbraco 5.0.1. and I keep getting the error:

    "Could not find the editor controller with id 8e71899c-0f8d-11e1-a728-d08b4824019b"

    The missing property [assembly: AssemblyContainsPlugins] was my first guess, but it is set. Do you might have any ideas?

    Thanks

    Stephan

    ReplyDelete
  13. Figured it out. I overlooked that the views are embedded (I copied it to the views subfolder in the package). As soon as I deleted the folder everything worked like a charm.

    Stephan

    ReplyDelete
  14. Hi Andy, thanks for the post. It all works nice, however the nodes show up as follows:

    - Content
    -- Music Catalog (the custom one)
    -- Content (default Content node)

    So just out of nothing a new level of Content appears.

    Why is this? Is there any way to get the Music Catalog node as a sibling of Custom test block?

    Also see this: http://stackoverflow.com/questions/10653339/umbraco-5-custom-hive-tree-provider-how-is-the-location-determined

    ReplyDelete
  15. @Hainesy - this looks useful for helping with that situation: http://cultiv.nl/blog/2012/4/13/tip-of-the-week-clean-those-aspnet-temp-files/

    ReplyDelete