Wednesday, 2 March 2011

Using XML Serialization to render front-end web pages from a custom CMS built in MVC2

This post is about a technique I developed on a recent project which may be of interest.  This project seemed naturally to call for SharePoint 2010 as it had a CMS component, but it needed to be hosted in the Windows Azure cloud.  As of this writing, (although some seem to have tried with an Azure VM role) it generally doesn’t seem to be possible to host SharePoint in Windows Azure; I guess it’s because the kind of “wrapper” that they have you put around an Azure application (the “role entry point”) so they can manage it in the cloud, and the stateless nature of Windows Azure apps, doesn’t really lend itself to SharePoint’s architecture.  So I decided to do the project using ASP.NET’s MVC2 Framework and building a custom CMS, in which I used XML serialization to enable a simple publishing capability to provide users with “preview” and “live” versions of web pages. 

I needed to ramp up quickly, and as I’ve generally been impressed with the quality of Apress technical books in the past, I looked at what they were offering in terms of books on MVC2.  I was pleased to discover this book, “Pro ASP.NET MVC2 Framework” (2nd edition) by Steven Sanderson.  (There’s already a version available for MVC3, but it’s not out on Kindle yet.  I may extoll the virtues of my Kindle in another post, which I have become totally addicted to, but suffice it to say these thick technical books are a lot easier to carry around in Kindle form, and they’re generally about 10 GBP cheaper in the UK).  Anyway Sanderson’s MVC2 book is brilliant; he’s very clear and thorough, and I highly recommend his books.  He taught me a lot about MVC2, which seems light-years ahead of traditional ASP.NET, but also a lot about TDD and associated TDD tools such as Nunit, Ninject and Moq.  I read the book, went through the walk-through on how to build the sample “SportsStore” app, then built my own application based on the architecture he uses on “SportsStore”.  As he does in the book, I used LINQ to SQL for ORM; I used Visual Studio 2010 as my development environment.

My idea to address the CMS requirement was that users would edit data in a “SiteObjects” table (analagous to Sanderson’s “Products” table), where each “SiteObject” record represents a single piece of data that would appear on a web page.  Then they would “publish” that data by clicking a hyperlink in the “Admin” area.  This would serialize the data to XML, which would then be stored in a column in a “PageObjects” table, where each “PageObject” record represents a single web page in the site.  There was a column in the PageObjects table for the “preview” version, and another for the “live” version.  A user could request the preview version by including a parameter in the URL’s query string (e.g. “?preview=true”).

Incidentally this setup also made it possible to send the data out for translation by a translation service, which would receive the English version of the XML file, and would then return to us a translated version of the file (e.g. Spanish); the Spanish file could then be uploaded, deserialized to populate the appropriate Spanish records in the SiteObjects table; at the same time the “preview” field would be updated; then when the CMS admin person was ready, they could click another hyperlink in the Admin area and “publish” (i.e. copy) that XML to the “live” field which was visibile by default when viewing the site.

A single main method in my controller acted as the entry point to the application, determining which page the user wanted, and which language they wanted to display it in, based on the URL and the routing configuration.  Based on those parameters, the method would then make a call to a factory method in my “repository” class which would find the appropriate “PageObjects” record and return the XML containing the data needed to render the page.

Here’s a screenshot of a portion of a “PageObject” record as seen in SQL server where you can see the start of the XML data that resides in the “LiveXMLData” field and the “PreviewXMLData” field.

HubPageEnglish

Here’s a bit more about how the XML looks:

<?xml version="1.0" encoding="utf-8"?>

<PageDataDefinition xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

  <PageObjectID>15</PageObjectID>

  <PageObjectName>HubPage-English</PageObjectName>

  <PageTypeID>1</PageTypeID>

  <PageTypeName>HubPage</PageTypeName>

  <LanguageID>1</LanguageID>

  <LanguageName>Irish English</LanguageName>

  <LanguageCode>en-ie</LanguageCode>

  <RegionCode>ie</RegionCode>

  <AltLangCode>en</AltLangCode>

  <ArtistID>8</ArtistID>

  <ArtistName>No Specific Artist</ArtistName>

  <PageNameUrlFragment>hub-page</PageNameUrlFragment>

  <Modules>

    <Module ModuleID="1" ModuleName="HubPage">

      <SiteObjects>

        <SiteObject SiteObjectID="1" SiteObjectName="HubPage_PageTitle" SiteObjectType="TextWithoutURL" ModuleID="1" LanguageID="1" ArtistID="8" EasyAccessShortKey="hp_pgtitle">

          <SODescription>This should contain the page title, if it is a title that is specific to the hub page.</SODescription>

          <LastUpdated>1/17/2011 2:22:12 PM</LastUpdated>

          <UpdatedBy>Super_1</UpdatedBy>

          <Value>My Really Cool Hub Page Title</Value>

        </SiteObject>

Here’s the PageDataDefinition class:

using System;

using System.Collections.Generic;

using System.IO;

using System.Text;

using System.Xml;

using System.Xml.Serialization;

 

namespace ClientProjectName.Domain.Entities

{

    [Serializable]

    public class PageDataDefinition

    {

        #region constructors

 

        public PageDataDefinition()

        {

            ModuleDataDefinitions = new List<ModuleDataDefinition>();

        }

 

        public PageDataDefinition(List<ModuleDataDefinition> moduleDataDefinitions)

        {

            ModuleDataDefinitions = moduleDataDefinitions;

        }

 

        #endregion

 

        #region properties

 

        [XmlElement("PageObjectID")]

        public string PageObjectID { get; set; }

 

        [XmlElement("PageObjectName")]

        public string PageObjectName { get; set; }

 

        [XmlElement("PageTypeID")]

        public string PageTypeID { get; set; }

 

        [XmlElement("PageTypeName")]

        public string PageTypeName { get; set; }

 

        [XmlElement("LanguageID")]

        public string LanguageID { get; set; }

 

        [XmlElement("LanguageName")]

        public string LanguageName { get; set; }

 

        [XmlElement("LanguageCode")]

        public string LanguageCode { get; set; }

 

        [XmlElement("RegionCode")]

        public string RegionCode { get; set; }

 

        [XmlElement("AltLangCode")]

        public string AltLangCode { get; set; }

 

        [XmlElement("ArtistID")]

        public string ArtistID { get; set; }

 

        [XmlElement("ArtistName")]

        public string ArtistName { get; set; }

 

        [XmlElement("PageNameUrlFragment")]

        public string PageNameUrlFragment { get; set; }

 

        [XmlArray("Modules"), XmlArrayItem("Module")]

        public List<ModuleDataDefinition> ModuleDataDefinitions { get; set; }

 

        #endregion

 

        public static PageDataDefinition Deserialize(string xmlString, Type type)

        {

            XmlSerializer serializer = new XmlSerializer(type);

 

            using (StringReader reader = new StringReader(xmlString))

            {

                return (PageDataDefinition)serializer.Deserialize(reader);

            }

        }

 

        public static string Serialize(PageDataDefinition pageDataDefinition)

        {

            XmlSerializer serializer = new XmlSerializer(pageDataDefinition.GetType());

 

            using (MemoryStream ms = new MemoryStream())

            {

                using (XmlTextWriter xmlWriter = new XmlTextWriter(ms, Encoding.UTF8))

                {

                    serializer.Serialize(xmlWriter, pageDataDefinition);

 

                    xmlWriter.Flush();

 

                    ms.Seek(0, SeekOrigin.Begin);

 

                    using (StreamReader reader = new StreamReader(ms, Encoding.UTF8))

                    {

                        return reader.ReadToEnd();

                    }

                }

            }

        }

 

    }

}

 

 

Here’s a bit of code used in the main MVC2 controller to get the desired PageObject:

        // get the PageObject based on the passed (or default) parameters

        var pageObjectToShow = pageObjectsRepository.PageObjects.FirstOrDefault(x =>

            ((x.Language.LanguageCode == languageCode) && x.PageNameUrlFragment == pageName));

Here’s the call from the controller to a method in the repository that tells whether to get the preview or live version of the XML:

        // get the PageDataDefinition object by deserializing the XML in the PageObject.LiveXMLData field

        var pageDataDefinitionToShow = pageObjectsRepository.GetPDD(pageObjectToShow,

            usePreview ? Constants.XmlDataState.Preview : Constants.XmlDataState.Live);

Here’s the “GetPDD” method:

        // we only want to deserialize when a page is requested, for performance reasons

        public PageDataDefinition GetPDD(PageObject po, Constants.XmlDataState xmlDataState)

        {

            var httpContext = HttpContext.Current;

            PageDataDefinition pageDataDefinition = null;

 

            try

            {

                // assumes XML exists in the database and is in the correct format.         

                pageDataDefinition = PageDataDefinition.Deserialize(

                        xmlDataState == Constants.XmlDataState.Live

                        ? po.LiveXMLData

                        : po.PreviewXMLData,

                        typeof(PageDataDefinition));

            }

            catch (Exception e)

            {

                // If we fail to get the XML, it's fatal.

                var utilities = new Utilities.Utilities();

                var pageObjectRef = String.Format("PageObjectID = {0}, PageObjectName={1}",

                                            po.PageObjectID,

                                            po.PageObjectName);

 

                utilities.LogEvent(connectionString,

                                   Constants.AppEvents.Error,

                                   Constants.LogShortDescriptions.ErrorGettingPDDInPageObjectsRepository +

                                   " - Exception msg = " + e.Message,

                                   pageObjectRef,

                                   httpContext.User.Identity.Name);

            }

 

            return pageDataDefinition;

        }

The “PageDataDefinition” object is a deserialized complex type containing a hierarchy of “page”, “modules” and “site objects” where the page is the highest-level container; the “module” represents a lower level container (which can be rendered as a partial-page view in MVC2, i.e. an ASCX file); and each “module” contains a set of site objects.

Then, rather than forcing front-end developers to make complex calls to navigate the hierarchy of the PageDataDefinition class, in my Model class (this is a “ViewModel” object or “Data Transfer” object as described by Sanderson in his book), I flatten the hierarchy by creating a “Get( )” method which allows them to simply pass in the parameters for the object they want, and it returns the desired object from a dictionary.  Here’s the code that sets up the dictionary:

public class MyViewModel

{

    public Dictionary<string, SiteObjectDataDefinition> SODDLookup { get; set; }

// etc… and here’s the method:

public void PopulateSODDLookup(PageDataDefinition thePDD)

{

    foreach (var mdd in thePDD.ModuleDataDefinitions)

    {

        foreach (var sodd in mdd.SiteObjectDataDefinitions)

        {

            var soddKey =

                sodd.EasyAccessShortKey +

                "-M" + sodd.ModuleID.Trim() +

                "-L" + sodd.LanguageID.Trim() +

                "-A" + sodd.ArtistID.Trim();

 

            try

            {

                SODDLookup.Add(soddKey, sodd);

            }

            catch (Exception ex)

            {

                // this is mainly useful during development;

                // this should never happen in production as it would imply

                // a bad setup in the database (normally such errors would occur if

                // the app is attempting to add a key that's already in the dictionary)

                Utilities.LogEvent(ConnectionString,

                                Constants.AppEvents.Error,

                                Constants.LogShortDescriptions.ErrorPopulatingSODDLookup,

                                "SODDKey = " + soddKey + ", Error = " + ex.Message,

                                "G7Con");

            }                   

        }

    }

}

… and here’s the “Get( )” method… note the rendering of the error message, this enabled front-end devs to know if their call was incorrect with respect to the data in the database:

public SiteObjectDataDefinition Get(string theKey)

{           

    SiteObjectDataDefinition retVal = null;

 

    // if the language is not specified, get it from the context

    if (theKey.Contains("L?"))

    {

        theKey = theKey.Replace("L?", "L" + PDD.LanguageID.Trim());

    }

 

    // if the artist is not specified, get it from the context

    if (theKey.Contains("A?"))

    {

        theKey = theKey.Replace("A?", "A" + PDD.ArtistID.Trim());

    }

 

    if ( !SODDLookup.TryGetValue(theKey, out retVal) )

    {

        var errMsg = "SiteObject not found, bad call to Get() method:  " + theKey;

        retVal = new SiteObjectDataDefinition { URL = errMsg, Value = errMsg, ClickthroughUrl = errMsg };

    }

 

    return retVal;

 

}

So the front-end markup for a call to this method (typically in an ASCX file) would look like this (note the “EasyAccessShortKey” attribute in the XML example above; that acts as the key to the dictionary):

<%@ Control Language = "C#" Inherits = "System.Web.Mvc.ViewUserControl<ClientProjectName.WebUI.Models.MyViewModel>" %>

<section id="about">

    <div class="pos1">

        <header>

            <h2><%: Model.Get("hpb_abouttitle-M9-L?-A2").Value %></h2>

 

Anyway you get the idea.  Perhaps this will be food for thought for somebody!  Best wishes, –Dave

No comments:

Post a Comment