Using masterpages (.master) in C1

Nov 3, 2010 at 6:50 PM

Hey! So i've been hacking around and finally got a implementation of masterpages working so it can render without throwing exceptions all the time. You're maybe thinking wtf, why do we need this, but i believe in decoupling the actual rendering from the backend system (the cms - C1 in this case), and since masterpages is a pretty fundamental part of asp.net and every .net developers toolbox i decided to try plugging in a .master rendering engine instead of the default one i C1 that relies on a xml file.

The steps you have to make to do this is actually quite simple but it took many hours of debugging and looking in Reflector since its obvious no one thought of this when writing the code. For instance i had to call a private static function via reflection since some of the functionality to fix up the emitted xml from C1 aren't public. But what my code does is

1. In the page-renderer i've added a PreInit method, where i look for a master-file with the same name as the template specified for the page. If this file is found i set it as master for the page object.

var template = DataFacade.GetData<IPageTemplate>(false).Single(t => t.Id == _page.TemplateId);
string masterFile = String.Format("~/App_Masters/{0}.master", template.Title);

if (HostingEnvironment.VirtualPathProvider.FileExists(masterFile))
{
   AppRelativeVirtualPath = "~/";
   MasterPageFile = masterFile;
}

2. Later in the cycle i found the part that renders the code and made following assumption

2.a If the Master-property on the page-object returns null, continue with default rendering implementation
2.b If not, use master-page rendering. The code for it ended out to be the following

var contents = PageManager.GetPlaceholderContent(_page.Id);

if (Master != null)
{
    var mi = typeof(PageRenderer).GetMethod("NormalizeXhtmlDocument", BindingFlags.Static | BindingFlags.NonPublic);

    foreach (var content in contents)
    {
        var doc = XElement.Parse(content.Content);
        var context = PageRenderer.GetPageRenderFunctionContextContainer();

        PageRenderer.ExecuteEmbeddedFunctions(doc, context);
        mi.Invoke(null, new[] { new XhtmlDocument(doc) });

        var plc = Master.FindControl(content.PlaceHolderId);
        if (plc != null)
        {
            var body = doc.Descendants().Single(el => el.Name.LocalName == "body");
            var c = body.AsAspNetControl((IXElementToControlMapper)context.XEmbedableMapper);

            plc.Controls.Add(c);
        }

        var head = doc.Descendants().SingleOrDefault(el => el.Name.LocalName == "head");
        if (Header != null && head != null)
        {
            Header.Controls.Add(new LiteralControl(String.Concat(head.Elements())));
        }
    }                
}
else
{
    var renderedPage = PageRenderer.Render(_page, contents);
    this.Controls.Add(renderedPage);
}

There are some points i want to point out to make the above code as modular as possible
1. It was impossible to subclass the existing page-renderer shipped with C1, since its a code-behind file living in ~/renderers/page.aspx.cs. This class has to be included in the Composite assembly.
2. The page-class should expose a virtual method ala RenderContent, BuildControlTree or something third that developers can override and implement their own logic as i've done here
3. PageRenderer.NormalizeXhtmlDocument should be made public
4. When using the preview-tab inside the C1 console its not the ~/Renderer/Page.aspx that is being invoked, but some internal methods which makes the output inconsistent from browsing the site as a normal user, and as a editor inside C1
5. Pt. its still necessary to maintain a xml template file inside C1, for populating the content-placeholders to the page-editor. This process should be pluggable, so i can write a provider that feeds C1 with placeholders based on my own logic

I think that was it for now, but i hope to be able to start a discussion based on this. 
Nov 4, 2010 at 1:13 AM

Nice! I guess we could look into making .master pages a first class citizen in Composite C1... Anyone else out there that hunger for .master support instead of out XML based templates?

Nov 4, 2010 at 1:26 AM

*raises hand*

First of all, KUDOS to burningice for this. Excellent work. :)

Now, while I DID convert my master page to the template format used by Composite C1, it would have been nice to have the option to use .master pages if one so chooses.

 

Robert

Nov 4, 2010 at 9:26 AM
Edited Nov 5, 2010 at 2:59 PM

Good to see that you are finding it useful. I think someone mentioned that masterpages where not chosen because C1 shouldn't be coupled to tight with asp.net Webforms. I can reassure that masterpages are not bound in any way to webforms and Microsofts default rendering in asp.net MVC uses masterpages as well. Only difference is that the masterpage in Webforms inherits from MasterPage while it in MVC inherits from ViewMasterPage.

I would be happy to see masterpages as a first class citizen in Composite C1, but starting with exposing the necessary methods for executing functions and normalizing the xhtml and being able to subclass and override some render-implementation for ~/Renderers/Page.aspx would be a step in the right direction.

Nov 4, 2010 at 9:38 AM

And its not like the content will be that much different from the existing XML template to a masterpage. This is what a sample masterpage looks like on my system. Notice the title and default attributes on the placeholder that i were able to add by subclassing the default asp.net placeholder. Not that i need them right now, but i included it as an example that when populating placeholders for using in the pageditor, you can still keep the same attributes as you have today in the XML template. And since the Head-tag is now a server control, you can access it from other controls by referencing Page.Head, which is nice since it's only xslt-functions that are able to emit content to the head-tag as it is today.

<%@ Master Language="C#" AutoEventWireup="true" CodeFile="Standard.master.cs" Inherits="App_Masters_Standard" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title><dotgl:PageTitle runat="server" /></title>
</head>
<body>
    <div>
        <dotgl:ContentPlaceHolder id="contentplaceholder" title="Content" default="true" runat="server" />
    </div>
</body>
</html>

 

Jan 28, 2011 at 12:23 AM

It is now possible to see the rendered masterpage when using the preview-tab inside C1. Pt. you need to edit the source of Composite.Worflows to get it to work. Hopefully Composite will consider to make Masterpages a first class citizen now when this last quirk has been resolved.

The following method replaces the existing method in the file CompositeC1\Composite.Workflows\Plugins\Elements\ElementProviders\PageElementProvider\EditPageWorkflow.cs

private void editPreviewCodeActivity_ExecuteCode(object sender, EventArgs e)
{
    Control renderedPage = null;

    try
    {
        var selectedPage = this.GetBinding<IPage>("SelectedPage");
        var namedXhtmlFragments = this.GetBinding<Dictionary<string, string>>("NamedXhtmlFragments");

        var ctx = HttpContext.Current;

        ctx.Items.Add("SelectedPage", selectedPage);
        ctx.Items.Add("NamedXhtmlFragments", namedXhtmlFragments);

        var sb = new StringBuilder();

        ctx.Server.Execute("~/Renderers/Page.aspx", new StringWriter(sb));

        var response = ctx.Response;
        response.Filter = new FixLinksFilter(response.Filter);

        renderedPage = new LiteralControl(sb.ToString());
    }
    catch (Exception ex)
    {
        renderedPage = new LiteralControl("<pre>" + ex + "</pre>");
    }

    var serviceContainer = WorkflowFacade.GetFlowControllerServicesContainer(WorkflowEnvironment.WorkflowInstanceId);
    var webRenderService = serviceContainer.GetService<IFormFlowWebRenderingService>();

    webRenderService.SetNewPageOutput(renderedPage);
}

And and you need to replace ~/Renderers/Page.aspx as explained here http://compositec1contrib.codeplex.com/wikipage?title=MasterPages

Mar 27, 2011 at 1:51 AM
Edited Mar 27, 2011 at 2:02 AM

I second the motion to make master page support a first class citizen!

First, great work burningice!

I did notice that there are just a few things 'missing' in the description that, although easily overcome, would be nice to know for some; to get the new code to 'play nice' in the Renderers/page.aspx.cs file you need to reference the following namespaces;

using System.Linq;

using System.Web.Hosting;

using Composite.Core.Xml;

_page seems to have been replaced with page in the current release.

Again, thanks burningice for this solution... hopefully I'll have it up and running here shortly.

Mar 27, 2011 at 2:10 AM

@rowdyrobota The thread above is slightly outdated - burningice have launched http://compositec1contrib.codeplex.com/ which (amongst other features) let you get master page support with no modifications to Composite C1. 

Just copy in the contrib assembly, register it in web.config and away you go :) See http://compositec1contrib.codeplex.com/wikipage?title=MasterPages


Mar 27, 2011 at 2:27 AM

HAHA! I should have done some more research! Anyway, that should speed things up ;) Thanks!

Mar 27, 2011 at 3:20 AM

Im glad for your interest!

It is correct that the above changes mentioned for the preview-tab has been included in the official C1 source, so now you don't need anything else but to just use the CompositeC1Contrib project. Next step will be to include MasterPages in the official source as well, i hope - one is always allowed to dream :)

Mar 27, 2011 at 3:33 AM
Edited Mar 27, 2011 at 3:49 AM

I realize that this is probably just pilot error on my part but after removing and reinstalling the Composite reference (it wasn't mapped to the composite.dll in C1) I am getting the following compile errors via the contrib:

 

C:\CMS\C1Contrib\Web\Mvc\C1HtmlHelper.cs(28,28): error CS1729: 'System.Web.Mvc.MvcHtmlString' does not contain a constructor that takes 1 arguments

C:\CMS\C1Contrib\Web\Mvc\ContentController.cs(47,13): error CS0012: The type 'System.Web.HttpRequestBase' is defined in an assembly that is not referenced. You must add a reference to assembly 'System.Web.Abstractions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35'.

C:\CMS\C1Contrib\Web\Mvc\ContentController.cs(47,25): error CS1061: 'System.Web.HttpRequestBase' does not contain a definition for 'QueryString' and no extension method 'QueryString' accepting a first argument of type 'System.Web.HttpRequestBase' could be found (are you missing a using directive or an assembly reference?)

C:\CMS\C1Contrib\Web\Mvc\ContentController.cs(49,173): error CS1061: 'System.Web.HttpRequestBase' does not contain a definition for 'Url' and no extension method 'Url' accepting a first argument of type 'System.Web.HttpRequestBase' could be found (are you missing a using directive or an assembly reference?)

 

The namespaces I believe should be there are there. I'll probably figure it out soon enough but figured I'd throw it out there so I don't have to spin my wheels too much.

As mentioned, most likely something very silly on my part. I've been at this most of the day and it's all starting to get a little blurry!

Thanks!!!

 

update: I have moved this to it's proper discussion group here: http://compositec1contrib.codeplex.com/discussions/251347