Morphfolia Tutorial: the ‘Bare Bones’ PageLayout
PageLayouts are a key extension point for Morphfolia; in this article we’ll introduce how to build a PageLayout by looking at the absolute bare minimum you have to do to get one un and running, line by line. The Morphological.Kudos project (part of the standard Morphfolia relase) is a library of PageLayouts, it serves both as an SDK and it is actually used ‘in production’ on morphological.geek.nz. My only regret with the current release of Kudos (v2.4.1.0) is that some of the code is a bit old and hasn’t been refactored to a high degree of polish; it certainly works – but it’s not as clear and clean as it could be. If you want the ‘definative’ guide to build PageLayouts then this article is the place to start.
First, we have the using statements and namespace declaration – nothing terribly complex about that. You can namespace your layouts anyway you like, but it is a good idea to think about this carefully in advance – any subsequent refactoring of the namespace will affect users who are actively using the PageLayout.
using System.Web.UI;
using System.Web.UI.WebControls;
using Morphfolia.Common.BaseClasses;
using Morphfolia.Common.Info;
using Morphfolia.PageLayoutAndSkinAssistant;
using Morphfolia.PageLayoutAndSkinAssistant.Attributes;
namespace Morphological.Kudos.Layouts
{
the class declaration is fairly standard, but there are a couple of things you need to do:
- Decorate the class with IsLayoutWebControl attribute (found in the Morphfolia.PageLayoutAndSkinAssistant.Attributes namespace).
- Inherit from Morphfolia.Common.BaseClasses.BasePageLayout
- Finally, its good practice when developing WebControls to include the System.Web.UI.INamingContainer interface.
Stricly speaking we don’t need to worry about INamingContainer as this PageLayout isn’t going to be functional (it won’t hold controls that need to do PostBacks, etc). Generally speaking the PageLayouts are used as templates for serving content not applications – but they can.
Decorating the class with IsLayoutWebControl means that the PageLayout will be available to users when adding and editing pages.
[IsLayoutWebControl]
public class BareBones : BasePageLayout, INamingContainer
{
This ‘BareBones’ example is going to be very basic; we’ll display the page title and content, only. In order to implement this we’ll declare a couple of controls to display the information – a Literal into which we’ll put the page title (formatted as a heading), and a Panel to hold whatever content is assigned to the page. Obviously you can put whatever you want in here – the specific controls used are relevant only to your PageLayout and aren’t required specifically to get the PageLayout working.
private Literal pageTitle;
private Panel contentPanel;
This next property is required by the base class (BasePageLayout). It allows callers to pass in WebControls for inclusion in the PageLayout, for example a site map, search results or tag-cloud. How you implement the property is up to you, but this is what I would normally do.
private Morphfolia.Common.WebControlCollection childControls;
public override Morphfolia.Common.WebControlCollection ChildControls
{
get{
EnsureChildControls();
if( childControls == null )
{
childControls = new Morphfolia.Common.WebControlCollection();
}
return childControls;
}
set{
childControls = value;
}
}
Next we have the contructors: the standard arguement-less one, and one that can take a WebControl. You can dispense with the constructors if you wish, although you will need them both if you want to pass a WebControl straight in.
public BareBones()
{
}
public BareBones( WebControl childWebControl )
{
ChildControls.Add( childWebControl );
}
One of the main reasons the PageLayouts exist is to support custom-properties – to enable end-users to be able to configure their pages how they want them. Here’s an example CustomProperty – in this case it allows users to set a width, which will control how wide the content is.
Currently the admin UI allows any text-based input, however, you can type custom-properties any way you like – all you need to do is validate the user input (which we’ll cover soon).
A CustomProperty is any ‘normal’ class property that is decorated with the IsCustomProperty attribute, it can be called whatever you like (subject to normal language restrictions). In addition to the IsCustomProperty attribute are several other attributes that provide metadata to the system, which drives the UI provided to the user when they work with the PageLayout whilst setting up a page.
What these additional attributes are, and what they do, should be self evident. All the attributes you see here take a string arguement, which is simply the value you want to assign (e.g: PropertyFriendlyName = “Content Width”). The Morphfolia.PageLayoutAndSkinAssistant.Attributes namespace provides a couple of classes (such as SuggestedUsageNotes and Descriptions) which contain pre-defined messages for some of the CustomProperties commonly used.
I’ve used plain text in this example, but you’re better off defining some constant values for consistency, especially if you’re going to develop a comprehensive PageLayout library.
private Unit overalWidth = Unit.Pixel(650);
[IsCustomProperty,
PropertyFriendlyName("Content Width"),
PropertyDescription("Use this property to set the width of the content area."),
PropertySuggestedUsage("Fixed width in pixels (e.g: 220px) or a percentage (e.g: 50%).")]
public Unit OveralWidth
{
get{ return overalWidth; }
set{ overalWidth = value; }
}
Defining content-containers (places for the user to assign content to the page) is pretty much identical to custom-properties: a ‘normal’ property decorated with the appropriate attributes, and it can be called whatever you like (subject to normal language restrictions). There are three attributes you can use, but only IsContentContainer is required. ContentContainerDescription allows you to describe what the content container is for (main content, navigation, footer, and so on) and ContentContainerColour allows you to color code the content containers – useful if you have a few. When the content container is displayed in the edit page UI (for users to assign content) the background of the content container is colored as per this setting (otherwise it’s a pale blue).
private string userContent = " ";
[IsContentContainer,
ContentContainerDescription("Main page content."),
ContentContainerColour("#ddeeff")]
public string UserContent
{
get{ return userContent; }
set{ userContent = value; }
}
If you provide a ‘Current Layout Icon’ you can indicate to the user what the selected PageLayout will look like and give visual clues as to how the content will be layed out; this icon is simply an 100 pixel square JPG which is optional – but strongly recommended. Here are some example PageLayout icons. The name of the file is based on the full type name and assembly name, syntax: “[type name], [assembly name].jpg” e.g: “Morphological.Kudos.Layouts.QuadPageLayout, Morphological.Kudos.jpg”
 |
A simple PageLayout implemented as a 3 column table. |
 |
A PageLayout consisting of a table with 2 rows and 2 columns. |
 |
The PageLayout for a Blog: it has one content container that sits under the main blog area. |
 |
A space for content under a picture with properties. |
 |
A table with 10 content areas, all color coded for ease of content assignment in the admin section. |
The FormTemplatePresenterType property defines the FormTemplate provider to be used by the PageLayout. This property is not required by the PageLayout base class but it is needed if you want to display FormTemplate structured content.
FormTemplate providers deserve documentation of their own (which will be forth coming), in a nutshell: FormTemplate Providers allow you to display structured data – as formatted by the provider, this is useful for lists or collections of things. I use a FormTemplate Provider to render items on my Product Backlog. Normally content is input and stored as raw HTML (via the WYSIWYG editor), but you can also input structured content via a form; the user input form is generated automatically based on a simple XML file – you can have as many of these as you like. All the XML file has to do is confrom to the simple FormTemplate schema.
When a user enters content via a FormTemplate the resulting is content is stored as XML; then when the page is served it is presented (i.e: formatted) via the specified FormTemplate Provider rather than being simply inserted into the output stream as raw HTML.
private string formTemplatePresenterType
= "Morphological.Kudos.FormTemplatePresenters.LivewireProblem, Morphological.Kudos";
public override string FormTemplatePresenterType
{
get
{
return formTemplatePresenterType;
}
set
{
formTemplatePresenterType = value;
}
}
Anyone who has built WebControls will be familiar with the CreateChildControls method – it’s where the majority of the WebControl is usually constructed. We don’t have to do anything specific here as far as a PageLayout is concerned, but it is the best place to start constructing the WebControl. As you can see – this example doesn’t require much work, we just need to instatiate a Literal for the page title and a Panel for the content.
protected override void CreateChildControls()
{
pageTitle = new Literal();
Controls.Add( pageTitle );
contentPanel = new Panel();
Controls.Add( contentPanel );
}
InitializeContent is a method we have to implement for a PageLayout to work, it’s where the PageLayout receives the content to be displayed and inserts it into the correct place in the PageLayout. In this example the PageLayout has only one content container so teh work we do here is fairly trivial, however, you could have multiple content containers to assign content to and you could also incorporate additional logic regarding the location and presentation of it if you wanted to.
public override void InitializeContent()
{
EnsureChildControls();
string propertyType;
int temp;
if (CustomProperties != null)
{
for (int i = 0; i < CustomProperties.Count; i++)
{
propertyType = CustomProperties[i].PropertyType.PropertyTypeIdentifier;
if (propertyType.Equals(
Morphfolia.Common.ControlPropertyTypeConstants.PropertyTypeIdentifiers.CONT))
{
temp = GetContentInfoIndexById(int.Parse(CustomProperties[i].PropertyValue));
if (temp != Morphfolia.Common.Constants.SystemTypeValues.NullInt)
{
AddContentToContentContainer(tdContentContainer,
Page.ContentItems[temp].ContentEntryFilter,
Page.ContentItems[temp].Content);
}
}
}
}
}
Let’s review the important statements. EnsureChildControls() ensures that the CreateChildControls method has been called. In this implementation we are going to insert content into the anticipated control structure of the PageLayout – if it’s not there the process will fail. You could use the InitializeContent method to get the content only – and not affect the control structure then you don’t need to call EnsureChildControls, but you’ll need to do this eventually and this is generally the right place to do it.
if (CustomProperties != null) simply ensures that we have some information worth continuing with.
propertyType = CustomProperties[i].PropertyType.PropertyTypeIdentifier; is where we grab the Property Type of the CustomProperty so that we can ensure that only ‘content’ is added; that check is the next line of code – the if statement:
if (propertyType.Equals(Morphfolia.Common.ControlPropertyTypeConstants.PropertyTypeIdentifiers.CONT))
The PropertyTypeIdentifiers class has constants that help keep the types of CustomProperties consistently defined.
All the content for a page is provied as a single collection (a ContentInfoCollection object), the problem is that this content by itself has no knowledge of where in the page it needs to go – because pages and content are loosely-coupled; the way they are coupled is via a collection of CustomProperties which includes not only all the content / content container ‘bindings’ but also all other CustomProperties as well.
The GetContentInfoIndexById method helps us arrange the content for the page correctly. Given the id of a content item (which is stored in the CustomProperty collection items as the PropertyValue property) the GetContentInfoIndexById method will scan the associated content collection (the underlying ContentInfoCollection object) and get the matching content item.
The code temp = GetContentInfoIndexById(int.Parse(CustomProperties[i].PropertyValue)); ensures that the content item was found, as we only want to add content we know should be there. If the result is equal to Morphfolia.Common.Constants.SystemTypeValues.NullInt we know that the content item we were looking for is not present in the ContentInfoCollection – e.g: the ContentInfoCollection and CustomPropertyCollection are out of sync.
Finally we call the AddContentToContentContainer method which performs the actual job of inserting the content where we want it.
AddContentToContentContainer takes three arguements:
- contentContainer – the control that we want to append the content to.
- contentEntryFilter – the content entry filter specifies how the content was entered and how it should be presented – see FormTemplates.
- content – the content to add.
As mentioned earlier, content can be input as raw HTML or via what’s known as a FormTemplate, in which case the content is structered as XML. The AddContentToContentContainer method will append the raw HTML to the target control – or if the content is XML it will use a FormTemplate provider to present the content appropriately and append it to the target control as a WebControl.
The SetCustomProperties method is virtually identical in general purpose to the InitializeContent method – except that it deals with all the CustomProerties that aren’t content bindings.
The private CustomProperties field holds the internally used CustomPropertyInstanceInfoCollection which holds all the CustomProperties passed into the PageLayout. You don’t have to have this field but it’s a convenient way of working with the CustomProperties passed in.
Just as with the InitializeContent method, SetCustomProperties is usually used to accept the CustomPropertyInstanceInfoCollection passed in and use the contained values to ‘configure’ the PageLayout.
The CustomProperties passed in will typically contain values applicable to formatting (widths, colors, alignments and so on) but it can contain pretty much anything you want.
As with the InitializeContent method we call EnsureChildControls to ensure the expected control structure in instansiated.
Once we’ve established that the customProperties are worht working with we get to the heart of the method; first we get hold of the propertyKey and propertyValue. I then typically have a switch statement that evaluates the propertyKey, and for each propertyKey I want to handle I have a branch of code that uses the propertyValue that comes with the propertyKey. In this example I’m only expecting a CustomProperty that sets the ‘OveralWidth’.
All propertyKeys and propertyValues are strings, but I make use of constants for any properties which get common use – to encourage consistencey, hence: Constants.CommonPropertyKeys.OveralWidth.
As the CustomProperties sub-system only deals with strings – but you typcially code aganist specifically typed properties you need to do some validation and conversion; in this example I’m taking a string value which I expect to be able to convert into a unit (to set the ‘OveralWidth’), and if it fails I set a default value – defensive programming.
private CustomPropertyInstanceInfoCollection CustomProperties = null;
public override void SetCustomProperties(
CustomPropertyInstanceInfoCollection customProperties)
{
EnsureChildControls();
CustomProperties = customProperties;
string propertyKey;
string propertyValue;
if (CustomProperties != null)
{
for (int i = 0; i < CustomProperties.Count; i++)
{
propertyKey = CustomProperties[i].PropertyKey;
propertyValue = CustomProperties[i].PropertyValue;
switch (propertyKey)
{
case Constants.CommonPropertyKeys.OveralWidth:
if (!propertyValue.Equals(string.Empty))
{
try
{
OveralWidth = Unit.Parse(propertyValue);
}
catch
{
OveralWidth = Unit.Pixel(650);
}
}
break;
}
}
}
}
The final step is to implement the RenderContents method, this is where our WebControl / pageLayout is fianlly committed to the output stream. If you want to do any final adjustments – this method is your last chance.
The EnsureChildControls method is called – just in case. We then check the Visible property – no point doing a bunch of work if the WebControl is not going to be visible (this probably won’t matter when serving the WebControl from an HTTP Handler – but it’s good practice as you may decide to use the WebControl elsewhere). You haven’t forgotten, of course, that a Pagelayout is bascially a WebControl, have you.
The ChildControls property is not on the critical path when developing a PageLayout – but it’s very handy. The BasePageLayout class provides the ChildControls property so that callers can add WebControls to the PageLayouts; as an implementer your only real choice is what to do with them. You can ignore them completely if you wish, or far more usefully you can put them somewher in your PageLayout. You could even be ultra clever and write some logic that put different controls in different places – assuming you knew what to expect.
For this example we’re just appending any controls passed in to the sole content container. Real life examples of this property in action are where Morphfolia adds the SiteMap WebControl, Search Results, Tag-Cloud and WordIndexListPresenter WebControls. See the Morphological.Kudos.Layouts.QuadPageLayout PageLayout as another example.
The very last thing we do in this method is commit the WebControl to the output stream, by calling the RenderContents mehtod, however, just before we do that we ‘connect the final set of dots’.
The pageTitle Literal is finally given it’s value: the title of the page (wrapped up in an HTML <H1> element), courtesy of the PageLayouts reference to the current pages’ underlying implementation of the Morphfolia.Common.Interfaces.IPage interface. This is basically an object that gives us access to the basic properties of the ‘page’ (Title, PageID and so on).
Then we set the width of the contentPanel content container to the value stored in the OveralWidth variable. As I hope I made sufficiently clear earlier – we can set the values against the controls (contentPanel.Width = OveralWidth) either here or back in SetCustomProperties where we processed the CustomProperties initially.
protected override void RenderContents(HtmlTextWriter writer)
{
EnsureChildControls();
if( Visible )
{
if( ChildControls.Count > 0 )
{
for(int c = 0; c < ChildControls.Count; c++)
{
tdContentContainer.Controls.AddAt(c, ChildControls[c]);
}
}
pageTitle.Text = string.Format("<h1>{0}</h1>", base.Page.Title);
contentPanel.Width = OveralWidth;
base.RenderContents (writer);
}
}
|