Making the page layout system plug'able and .master page support native

Topics: Feature requests
Coordinator
Aug 22, 2011 at 11:20 AM

I’m looking into decoupling the “layout engine” and the core, moving the task of assembling pages to a provider. Right now there is only one native layout engine in Composite C1 (our XML based templates) and features like asp.net master page support delivered from the C1Contrib project cannot integrate seamlessly. By decoupling the current layout engine from the core and allow providers to assemble pages we should be able to ensure a more seamless experience, get full QA on the master page features and also add more layout options with less work. One goal here is to also offer a Razor based template option.

Below is a conceptual description of how we consider implementing the decoupling. If you have the time to mentally sink deep into this feature area you are very welcome to come with suggestions or ask nasty questions that could save us time late in the process.

There are a few fundamentals to this task that should be observed

  • This is not an attempt to make the “from incoming request to page assembly” block something you can control with providers – if you for some reason want to completely override the rendering of pages you would have to use std. asp.net hooks like the C1Contrib project does today.
  • The responsibility of the layout provider is to merge layout with the content/content functions it is handed.
  • The provider is expected to attach its content to a System.Web.UI.Page object – a asp.net web forms Page. This is by design as we wish Composite C1 to have support to both Web Forms, MVC (through the MvcPlayer), XSLT (through XSLT Functions) etc. This requirement is not hindering XML or Razor based layouts, it just sets requirements on how the markup is communicated back to the core, once the page markup has been determined.

Common

We will introduce a new ‘application block’ configurable through Composite.config where page layout providers are registered. Multiple layout providers can live side by side and pages/content is not “permanently bound” to a specific layout or technology (i.e. you can edit a page and switch to another layout and thus provider). We will as a minimum ship with a provider that deliver the current XML based layout behavior and we aim at getting .master based templates with the release as well. Once we have shipped we will look into a Razor based layout provider and make it available in either in a service release or as a package.

Provider interface

The code here is what we are working with at the moment – it is not final and details are missing. The most interesting parts to notice is IPageLayoutProvider (where core and provider first meet), PageLayoutDescriotor (describing layouts to the core) and IPageLayoutRenderer (building pages at runtime).

 

/// <summary>
/// Expect your provider to be instantiated only once (singleton). Config/factory classes not included here. 
/// </summary>
public interface IPageLayoutProvider
{
    /// <summary>
    /// Used in general to have providers declare what page layouts (identified by Guid Id) it is handling. 
    /// At render time this info allow Composite C1 to route a page to the page layout provider that it 
    /// is currently using. This info should be expected to be cached by Composite C1 and changes to cached state
    /// require a call back to Composite C1 (not described here).
    /// </summary>
    /// <returns>Descriptions of layouts known to this provider</returns>
    IEnumerable<PageLayoutDescriotor> GetPageLayoutDescriptions();

    /// <summary>
    /// Factory that give Composite C1 a IPageLayouter capable of rendering a Composite C1 page with the specified layout ID.
    /// The factory will be called for each individual page rendering 
    /// </summary>
    /// <param name="pageLayoutId">Id ot the page layout, this Id must belong to the provider</param>
    /// <returns></returns>
    IPageRenderer BuildPageRenderer(Guid pageLayoutId);
}



/// <summary>
/// Describes a page layout to the Composite C1 core so it may set up editing UI
/// </summary>
public class PageLayoutDescriotor
{
    /// <summary>
    /// Used to identify page layouts. This has to be unique and immutable.
    /// </summary>
    public Guid Id { get; set; }
    public string Title { get; set; }
    public string Description { get; set; }
    /// <summary>
    /// The EntityToken will be used in the C1 Console tree and will allow devs to hook commands to
    /// specific templates (like 'Edit')
    /// </summary>
    public Composite.C1Console.Security.EntityToken EntityToken { get; set; }
    public IEnumerable<PageLayoutPlaceholderDescriptor> PlaceholderDescriptions { get; set; }
    /// <summary>
    /// The default is the placeholder to focus/use when users edit a page or a layout is used in ad hoc renderings.
    /// </summary>
    public string DefaultPlaceholderId { get; set; }
}



/// <summary>
/// Describe a placeholder on a layout template (key/value).
/// (existing class TemplatePlaceholdersInfo should be used+refactored).
/// </summary>
public class PageLayoutPlaceholderDescriptor
{
    /// <summary>
    /// Used to identify a layout placeholder. This has to be unique only within a single page layout.
    /// </summary>
    public string Id { get; set; }
    public string Title { get; set; }
}



/// <summary>
/// This class is responsible for rendering the provided job onto the provided asp.net web forms page. The Render method is called at page construction
/// and is expected to hook on to asp.net page events (like PreInit) to drive the rendering. This happens early enough to enable the renderer to attach
/// a Master page to the page if desired.
/// </summary>
public interface IPageRenderer
{
    void AttachToPage(System.Web.UI.Page renderTaget, PageRenderingJob renderJob);
}


/// <summary>
/// Describe the page and content desired to be rendered.
/// </summary>
public class PageRenderingJob
{
    public PageRenderingJob(IPage page, IEnumerable<IPagePlaceholderContent> contents)
    {
        this.Page = page;
        this.Contents = contents;
    }

    public IPage Page { get; private set; }
    public bool IsPreview { get; private set; }
    public IEnumerable<IPagePlaceholderContent> Contents { get; private set; }
}

 

Frontend

When an incoming request  is caught by Composite C1 as matching a page request there is some bookkeeping like security, page caching, setting correct data scope and language, locating page data – all this is done prior to any calls to page layout providers. Once a page request is deemed ready to be rendered the pages Layout Template ID is used to identify the responsible provider and it is called with the current asp.net page (so the provider can hook on to the page as it pleases) along with page data.

Here is an example of how a XML based template provider could handle a rendering – all the work is handled via a call to PageRenderer.Render() – a method that exists in the current source code already if you wish to dig that deep.

 

/// <summary>
/// Sample
/// </summary>
public class XmlLayoutPageRenderer : IPageRenderer
{
    private Page _aspnetPage;
    private PageRenderingJob _job;

    public void Render(System.Web.UI.Page renderTaget, PageRenderingJob renderJob)
    {
        _aspnetPage = renderTaget;
        _job = renderJob;

        _aspnetPage.PreInit += new EventHandler(RendererPage);
    }

    private void RendererPage(object sender, EventArgs e)
    {
        Control renderedPage;
        using (Profiler.Measure("Page build up"))
        {
            renderedPage = PageRenderer.Render(_job.Page, _job.Contents);
        }

        using (Profiler.Measure("ASP.NET controls: PagePreInit"))
        {
            _aspnetPage.Controls.Add(renderedPage);
        }
    }
}

 

Backend (Managing layouts in the C1 Console)

When you browse the “Page Templates” folder in the Layouts perspective the list of layouts is provided by a common element provider, which aggregate layouts pulled from layout providers using the GetPageLayoutDescriptions() method. The common element provider will not attach any actions like Edit or Delete on to the elements, rather the layout provider developers can use the “attaching actions” feature to hook Edit, Delete etc. actions to the layout elements they are responsible for – this is all glued together by the EntityToken that is part of the GetPageLayoutDescriptions() results.

Editing a Page Layout Template can thus vary from one type (layout provider) to another. XML based layouts would look pretty much like they do today, while Master Page based layouts could have two editors (markup and code behind) and a UI to declare Placeholders explicitly.