Integrating N2CMS into Who Can Help Me? Part 3
Tags: N2 CMS, Who Can Help Me February 12th, 2010This is part 3 in my series of posts about integrating the .Net open source content management system N2CMS into the Sharp Architecture demo application Who Can Help Me?.
In the first and second post I
- Added the N2 assemblies
- Added the edit site to the web project
- Created the initial content definitions
- Created the database schema
- Added root nodes to the CMS database
- Updated the Web.config
- Set up the N2 Content Routes and intialised the CMS engine
Step 8 – Add a base CMS controller
To link our content items in the CMS database to the appropriate controllers, we need our CMS related controllers to inherit from the N2 controller class ContentController<T> where T is the type of content definition that the controller is handling. This means that we have access to a strongly typed instance of the current content item that the controller is handling within our controller actions.
We already have a BaseController class in WCHM, as we wanted to set up some defaults for the master page name to use etc in the Spark views. Ideally, I’d like to be able to reuse this code, but as our CMS controllers need to be generic, I had to create a new base class, N2Controller<T> which inherits the N2 ContentController<T> and duplicate the method overrides for setting up the default master page etc.
namespace WhoCanHelpMe.Web.Controllers
{
using System.Web.Mvc;
using N2;
using N2.Web.Mvc;
/// <summary>
/// Base N2 controller
/// </summary>
/// <typeparam name="T">
/// The content item type
/// </typeparam>
public class N2Controller<T> : ContentController<T> where T : ContentItem
{
/// <summary>
/// Stores the site master name
/// </summary>
private const string MasterName = "Site";
/// <summary>
/// Default controller action
/// </summary>
/// <returns>
/// The default view with the current CMS item as the model
/// </returns>
public override ActionResult Index()
{
return View(CurrentItem);
}
/// <summary>
/// Returns a view result with default view and master name
/// </summary>
/// <returns>
/// View Result
/// </returns>
protected new ViewResultBase View()
{
return View(string.Empty, MasterName, null);
}
/// <summary>
/// Returns a view result with specified view name and the default master name
/// </summary>
/// <param name="viewName">
/// The view name.
/// </param>
/// <returns>
/// View Result
/// </returns>
protected new ViewResultBase View(string viewName)
{
return View(viewName, MasterName, null);
}
/// <summary>
/// Returns a view result with the specified view name, default master name
/// and the specified model
/// </summary>
/// <param name="viewName">
/// The view name.
/// </param>
/// <param name="model">
/// The model.
/// </param>
/// <returns>
/// View Result
/// </returns>
protected new ViewResult View(string viewName, object model)
{
return View(viewName, MasterName, model);
}
/// <summary>
/// Returns a view result with default view and master name
/// </summary>
/// <param name="model">
/// The model.
/// </param>
/// <returns>
/// View Result
/// </returns>
protected new ViewResult View(object model)
{
return View(string.Empty, MasterName, model);
}
}
}
I also added my specs for the behaviour of the base CMS controller:
namespace MSpecTests.WhoCanHelpMe.Web.Controllers
{
using System.Web.Mvc;
using System.Web.Routing;
using global::WhoCanHelpMe.Domain.Cms.Pages;
using global::WhoCanHelpMe.Web.Controllers;
using Machine.Specifications;
using Machine.Specifications.AutoMocking.Rhino;
using Machine.Specifications.Mvc;
public abstract class context_for_n2_controller : Specification<N2Controller<HomePage>>
{
Establish context = () =>
{
var routeData = new RouteData();
routeData.Values.Add("n2_item", new HomePage { Name = "Home Page" });
subject.ControllerContext = new ControllerContext() { RouteData = routeData };
};
}
[Subject(typeof(N2Controller<>))]
public class when_the_n2_controller_is_asked_for_the_default_action : context_for_n2_controller
{
static ActionResult result;
Because of = () => result = subject.Index();
It should_return_the_default_view = () =>
result.ShouldBeAView().And().ShouldUseDefaultView();
It should_use_the_default_master_page = () =>
result.ShouldBeAView().And().MasterName.ShouldEqual("Site");
It should_populate_the_view_model_with_the_current_item_from_the_cms = () =>
result.Model<HomePage>().Name.ShouldEqual("Home Page");
}
}
What I’ve done here is add a base Index() action to the CMS controller that will just pass the current content item as the model into the view. In reality, I’ll probably not use this as in WCHM we’re pretty strict on using viewmodels to bind to our views, rather than domain entities (which is what the CMS definitions really are) however, this allows a quick and dirty way to pass the CMS content into the view as your’re building up your application. With anything but the most simple application, your view will be made up of more than just the content from the CMS, so you’ll need to build up a more specific model for the view anyway.
Note how N2 works – when it realises that it can handle an incoming request for a URL with a content item it creates that item for us and puts it into the RouteData before calling the associated controller. This means that we easily have access to the current item via the ContentController<T>.CurrentItem property. We can easily simulate this for the purposes of our specs by adding a content item into the RouteData associated with our ControllerContext – (in the Establish context).
Step 9 – Update the HomeController
The goal of this exercise was to surface content from N2CMS onto our WCHM Home page and About page. We’ve now got a base CMS controller that we can use, so next I refactor the existing HomeController to inherit N2Controller.
[Controls(typeof(HomePage))]
public class HomeController : N2Controller<HomePage>
{
...existing code the same for now...
}
The things to note are that we now inherit N2Controller which means we can easily access the current item (as a HomePage) from within our controller, and the Controls[] attribute, which tells N2 which content types this controller is going to handle. Any requests matching a content item of type HomePage will now come this way.
We also need to update our specs as because we now inherit N2Controller, which in turn inherits ContentController<T> (in the N2 namespace), our controller is going to try to access the current content item out of RouteData. Even though we’re not accessing it yet, unless we put it into RouteData, our specs will fail. I updated the Establish context for the HomeControllerSpecs in the same fashion as the specs for our base N2Controller, by adding a content item into the RouteData associated with our ControllerContext.
Establish context = () =>
{
var routeData = new RouteData();
home_page = new HomePage();
routeData.Values.Add("n2_item", home_page);
home_view_model_mapper = DependencyOf<IHomePageViewModelMapper>();
news_tasks = DependencyOf<INewsTasks>();
ServiceLocatorHelper.AddCachingService();
subject.ControllerContext = new ControllerContext() { RouteData = routeData };
};
At this point, our site should build, compile, pass tests and load up in the normal WCHM fashion, so lets take a pause and go off tangent ever so slightly…
Step 10 - Authentication
This is a bit of a tricky one in WCHM as we’re already using Open ID for users to authenticate themselves in order to create profiles. N2 uses the Asp.Net membership providers to manage users and access to the CMS system (although it has it’s own custom provider that stores the users as content items in it’s own database). We don’t want to allow anyone to be able to get into the CMS, but we need to be able to distinguish between those that can and cannot with one unified mechanism.
I went round in circles a bit with this one, and with some help from Jon, I ended up back right where I started, which was to do nothing. The solution I came up with is a little hacky, but it works, and requires no changes to N2, or the WCHM authentication, so all is good for now.
In order to set up a CMS user, I first created a profile in the usual way in WCHM, using my preferred Open ID provider. I then opened the Profiles database table in the WCHM database and found the Open ID username that has been returned and stored against my profile. If, like me, you use a Google account, it will look something like this: (this is not my real Open ID!)
https://www.google.com/accounts/o8/id?id=asdasdasdasd6auasoas_asdadssad_das
I then used this username to create a new admin user in the N2 database in the normal way I would do in N2. Firstly, I need to add the relevant membership config for N2 into the web.config:
<!-- These membership and role providers store their data as content items. You can also use the forms authentication credentials only (remove these sections) or the default ASP.NET providers (check your ASP.NET documentation) -->
<membership defaultProvider="ContentMembershipProvider">
<providers>
<clear/>
<add name="ContentMembershipProvider" type="N2.Security.ContentMembershipProvider, N2.Security" />
</providers>
</membership>
<roleManager enabled="true" defaultProvider="ContentRoleProvider">
<providers>
<clear/>
<add name="ContentRoleProvider" type="N2.Security.ContentRoleProvider, N2.Security" />
</providers>
</roleManager>
<profile defaultProvider="ContentProfileProvider">
<providers>
<clear />
<add name="ContentProfileProvider" type="N2.Security.ContentProfileProvider, N2.Security" />
</providers>
</profile>
Then, I temporarily replace the WCHM authentication configuration with the out-the-box default N2 authentication section:
<authentication mode="Forms">
<forms loginUrl="edit/login.aspx" protection="All" timeout="30000" path="/">
<credentials passwordFormat="Clear">
<!-- WARNING: Change this default password. Please do it now. -->
<user name="admin" password="changeme"/>
</credentials>
</forms>
</authentication>
I can now load up my site, browse to the /edit url and be presented with the standard N2 login screen.
Logging in with the default admin user specified in config gets me into the N2 admin interface:
Now we’re getting somewhere! I can then use the N2 users screen to add a new user – I use the Open ID username as the N2 username, and fill the rest of the details in, but in fact, they’re not going to be used as we’re going to rely on Open ID to authenticate us. Once the user is added, we can see it in the N2 users table:
Now that we have our first real admin user, we can replace the authentication section in web.config with the original WCHM Open ID config:
<authentication mode="Forms">
<forms defaultUrl="~/" loginUrl="~/user/login" timeout="2880" name="__wchm_auth"/>
</authentication>
We can now log in and out of N2 using the Open ID authentication. If I try to browse to the /edit url in order to get to the admin site, I will get presented with the WCHM Open ID authentication screen. Once authenticated using Open ID, I can then access the edit site fine as the authenticated username matches an admin user of the same username in N2.
Next…
So, we’re practically there – we have a working site and a working N2 admin site which we can access using our Open ID authenticated username. The Home page still loads normally and is in fact being routed via the N2 content route. The final steps are to build up the content definitions, and pass the content from N2 to the view…

March 26th, 2010 at 6:42 am
Sorry for my questions but I have run into another problem. This time it is some sort of routing problem. When I start the website I get this exception:
The view at ‘~/default.aspx’ must derive from ViewPage, ViewPage, ViewUserControl, or ViewUserControl.
I guess there’s something wrong with the routing but I can’t figure it out. I’ve done exactly as you have.
Have you seen this error or do you know what could cause it?
Thanks again!
/Ingo
March 26th, 2010 at 9:48 am
Maybe I should say that I’m not using Spark and I’m adding this functionality to my project which is based on WhoCanHelpMe.
/Ingo
April 14th, 2010 at 12:39 pm
Your header is a bit wonky in Opera, mate.
April 22nd, 2010 at 10:42 am
I got the same problem as Ingo when run “~/default.aspx” or “~/edit/”
Do you have any clue?
October 10th, 2010 at 12:26 pm
gfd65Axca
June 23rd, 2011 at 11:51 pm
Excellent read, I just passed this onto a colleague who was doing a little research on that. And he just bought me lunch since I found it for him smile Therefore let me rephrase that: Thanks for lunch!
August 5th, 2011 at 3:11 pm
I need it.You blog is good
August 20th, 2011 at 8:17 pm
Good post however I was wondering if you could write a litte more on this subject? I’d be very thankful if you could elaborate a little bit more. Thank you!
August 21st, 2011 at 5:57 pm
You really make it appear so easy along with your presentation but I to find this matter to be actually one thing which I feel I’d by no means understand. It seems too complex and extremely huge for me. I’m taking a look ahead to your next publish, I will try to get the hang of it!
September 29th, 2011 at 12:12 pm
Brownie points for creating a post on this topic. There is not enough content written about it (not particularly useful anyway). It is excellent to see it receiving more coverage. Thanks again!