Creating a Custom Placeholder to render and edit XML data using XSL templates
XMLPlaceholder Controls are a useful technique to handle structured data within a CMS Posting.
Suppose you have a Book template containing an Authors placeholder. When a book has more than one author, you could store a comma-separated list of authors in a HtmlPlaceholder. The authoring interface would look like:
Authors List:
But ideally, the list could be expressed as an XML document stored in a Custom XML Placeholder. This would allow storing additional structured data, such as the Nationality of each author. Something like:
<AuthorList id="AuthorsPlaceholder">
<Author id="Author.1">
<Name>António Ribeiro</Name>
<Nationality>Portuguese</Nationality>
</Author>
<Author id="Author.2">
<Name>Cristina Carvalho</Name>
<Nationality>Spanish</Nationality>
</Author>
</AuthorList>
The issue around this approach is the development effort required to implement an Authoring interface (a backoffice) to manage the authors XML document. Although a DataGrid could be used, some specific coding would be necessary. Also, an HTML form is a more appropriate choice.
Proposed Solution
Creating a Custom XML Placeholder that renders HTML for Authoring and Presentation using different XSL templates can be a cost-effective solution.
The Custom Control implementation can be generic enough to render any list of Items using the same XML schema, just using different XSL tempates for Authoring and Presentation interfaces. The Presentation template should render an appropriate presentation HTML while the authoring interface should display Text Boxes for editing content cells, allowing updates and deletes of existing rows or additions of new rows (one row at a time).
This solution would leverage the declarative support for XML / XSL transformations and would require no other coding changes when used with other type of information. A full example is shown bellow
The XML document stored in the XML Placeholder:
<ItemList id="ItemsPlaceholder">
<Item id="Item.1">
<Item id="Name">António Ribeiro</Item>
<Item id="Nationality">Portuguese</Item>
</Item>
<Item id="Item.2">
<Item id="Name">Cristina Carvalho</Item>
<Item id="Nationality">Spanish</Item>
</Item>
</ItemList>
The XSL Transformations for Presentation and Authoring:
Rendered for Presentation (applying Presentation.xsl) |
Rendered for Authoring (applying Authoring.xsl) |
||||||||
Authors List António RibeiroPortuguese Cristina CarvalhoSpanish
|
…SaveSave and Exit |
Presentation.xsl
<? xml version="1.0"?>
<xsl:stylesheet xmlns:xsl ="https://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="html"></xsl:output >
<xsl:template match="/">
<div>Authors List</div>
<hr></hr>
<xsl:apply-templates />
</xsl:template>
<xsl:template match="Item">
<div>
<B><xsl:value-of select="Cell[@id='Name']"/></B>
</div>
<div>
<xsl:value-of select="Cell[@id='Nationality']"/>
</div>
<br></br>
</xsl:template>
</xsl:stylesheet>
Authoring.xsl
<? xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="https://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="html"></xsl:output>
<xsl:template match="/">
<table width="100%" style="BORDER-RIGHT: thin solid; BORDER-TOP: thin solid; BORDER-LEFT: thin solid; BORDER-BOTTOM: thin solid">
<tr>
<td><b>Name:</b></td>
<td><b>Nationality:</b></td>
</tr>
<xsl:apply-templates />
</table>
<br></br>
</xsl:template>
<xsl:template match="Item">
<tr>
<td>
<input size="25">
<xsl:attribute name="name"><xsl:value-of select="../@id"/>:<xsl:value-of select="@id"/>:Name</xsl:attribute>
<xsl:attribute name="value"><xsl:value-of select="Cell[@id='Name']" /></xsl:attribute>
</input>
</td>
<td>
<input size ="30">
<xsl:attribute name="name"><xsl:value-of select="../@id"/>:< xsl:value-of select="@id"/>:Nationality</xsl:attribute>
<xsl:attribute name="value"><xsl:value-of select="Cell[@id='Nationality']" /></xsl:attribute>
</input>
</td>
</tr>
</xsl:template>
</xsl:stylesheet >
How it Works
When the user selects Save or Save and Exit in the Web Author console, the XML Placeholder Control must have the logic necessary to check the HTTP form post fields, append new Items to the XML document where new Names or Nationalities were filled in, and delete Items for rows where all text boxes were cleaned up. This can be easily achieved applying a special naming convention to these TextBoxes and rebuilding the XML document after the PostBack based on its contents.
Suppose your XmlPlaceholder is MyAuthorsList, the TextBox names would be:
MyAuthorsList:Author.n:Name
MyAuthorsList:Author.n:Nationality
(where n should be a different value for each Author)
A full implementation of this XML Custom Placeholder is shown bellow.
[ SupportedPlaceholderDefinitionType( typeof (XmlPlaceholderDefinition) ) ]
public class MyXmlPlaceholderControl : BasePlaceholderControl
{
private Xml xmlAuthoringControl;
private Xml xmlPresentationControl;
private string configUrl;
[
Browsable (true),
Description("A relative url of the folder containing XSLT templates and XML template data"),
Category("Data"),
DefaultValue ("")
]
public string ConfigUrl
{
get
{
return this.configUrl;
}
set
{
this.configUrl = value;
}
}
public MyXmlPlaceholderControl()
{
configUrl = "";
}
protected override void CreateAuthoringChildControls(BaseModeContainer authoringContainer)
{
this.xmlAuthoringControl = new Xml();
this.xmlAuthoringControl.ID = "XmlAuthoringControl";
authoringContainer.Controls.Add ( this.xmlAuthoringControl );
}
protected override void CreatePresentationChildControls(BaseModeContainer presentationContainer)
{
this.xmlPresentationControl = new Xml();
this.xmlPresentationControl.ID = "XmlPresentationControl";
presentationContainer.Controls.Add ( this.xmlPresentationControl );
}
protected override void LoadPlaceholderContentForAuthoring(PlaceholderControlEventArgs e)
{
EnsureChildControls ();
try
{
//load existing xml document stored in the placeholder
XmlDocument xml = new XmlDocument ();
try
{
xml.LoadXml (((XmlPlaceholder)this.BoundPlaceholder).XmlAsString);
}
//if is empty or invalid, create a default xml document
catch
{
xml.LoadXml ("<ItemList id=\"\"></ItemList>");
}
//apply a control id to the xml document
xml["ItemList"].Attributes["id"].Value = this.ID;
//append the xml template for a default item
XmlDocument xmlTemplate = new XmlDocument ();
xmlTemplate.Load (Context.Server.MapPath(ConfigUrl + "/Template.xml"));
xml["ItemList"].InnerXml = xml["ItemList"].InnerXml + xmlTemplate
"ItemList"].InnerXml;
//render the transformation
this.xmlAuthoringControl.DocumentContent = xml.InnerXml;
string configUrl = this.ConfigUrl;
if( ConfigUrl != String.Empty )
{
XslTransform myTrans = new XslTransform ();
string xsltPath = Context.Server.MapPath(ConfigUrl + "/Authoring.xsl");
myTrans.Load ( xsltPath);
this.xmlAuthoringControl.Transform = myTrans;
}
}
catch (Exception exp)
{
// show the error as placeholder content
this.xmlAuthoringControl.DocumentContent = "<error>" + exp.Message + "</error>";
}
}
protected override void LoadPlaceholderContentForPresentation(PlaceholderControlEventArgs e)
{
EnsureChildControls ();
try
{
//render the transformation
this.xmlPresentationControl.DocumentContent = ((XmlPlaceholder)his.BoundPlaceholder).XmlAsString;
string configUrl = this.ConfigUrl;
if( ConfigUrl != String.Empty )
{
XslTransform myTrans = new XslTransform ();
string xsltPath = Context.Server.MapPath(ConfigUrl + "/Presentation.xsl");
myTrans.Load ( xsltPath);
this.xmlPresentationControl.Transform = myTrans;
}
}
catch (Exception exp)
{
// show the exception as placeholder content
this.xmlPresentationControl.DocumentContent = "<error>" + exp.Message + "</error>";
}
}
protected override void SavePlaceholderContent(PlaceholderControlSaveEventArgs e)
{
EnsureChildControls ();
try
{
//initialize xml document
XmlDocument xml = new XmlDocument ();
xml.LoadXml ("<ItemList id=\"" + this.ID + "\"></ItemList>");
//rebuild xml document using form field data
foreach (string f in Context.Request.Form)
{
//look for form field named "PlaceholderId:ItemId:CellId"
//such as "MyAuthorsList:Author.1:Name"
string[] ff = f.Split(':');
if((ff.Length == 3) && (ff[0] == this.ID))
xml = AppendItem(xml, ff[1], ff[2], Context.Request.Form[f].ToString());
}
//remove empty items from the xml document
foreach (XmlNode n in xml.SelectNodes("/ItemList/Item"))
if(n.InnerText.Trim().Equals(""))
xml["ItemList"].RemoveChild(n);
//remove the template item if it was not filled
XmlDocument xmlDefault = new XmlDocument ();
xmlDefault.Load (Context.Server.MapPath(ConfigUrl + "/Template.xml"));
xmlDefault ["ItemList"].Attributes["id"].Value = this.ID;
int p = xml.InnerXml.IndexOf(xmlDefault["ItemList"].InnerXml);
if(p >= 0)
xml.InnerXml = xml.InnerXml.Remove (p, xmlDefault["ItemList"].InnerXml.Length);
//or apply a new id for the new item
else
{
string id = xmlDefault["ItemList"]["Item"].Attributes["id"].Value;
xml["ItemList"].LastChild.Attributes["id"].Value = id + "." + xml["ItemList"].ChildNodes.Count.ToString();
}
//stored xml in the placeholder
((XmlPlaceholder)this.BoundPlaceholder).XmlAsString = xml.InnerXml;
}
catch (Exception exp)
{
// Possible validation exceptions
// If you want, you can override it with your own custom exception
string newExceptionMessage = String.Format( "[{0} \"{1}\"] Error saving placeholder contents: {2}", this.GetType().Name, this.ID, exp.Message );
Exception overriddenException = new Exception(newExceptionMessage, exp);
// raise new exception so the BasePlaceholderControl can handle it
throw overriddenException;
}
}
private XmlDocument AppendItem(XmlDocument xml, string item, string cellName, string cellValue)
{
//get or create item element
XmlNode itemElement = xml.SelectSingleNode ("/ItemList/Item[@id=\"" + item + "\"]");
if(itemElement == null)
{
itemElement = xml.CreateElement("Item");
XmlAttribute itemIdAttribute = xml.CreateAttribute("id");
itemIdAttribute.Value = item;
itemElement.Attributes.Append (itemIdAttribute);
xml["ItemList"].AppendChild(itemElement);
}
//get or create cell element
XmlElement cellElement = xml.CreateElement("Cell");
XmlAttribute idAttribute = xml.CreateAttribute ("id");
idAttribute.Value = cellName;
cellElement.Attributes.Append (idAttribute);
cellElement.InnerText = cellValue;
itemElement.AppendChild (cellElement);
return xml;
}
protected override void OnPopulatingDefaultContent(PlaceholderControlCancelEventArgs e)
{
try
{
string xmlPath = Context.Server.MapPath(ConfigUrl + "/Default.xml");
XmlDocument xml = new XmlDocument ();
xml.Load (xmlPath);
xmlAuthoringControl.DocumentContent = xml.InnerXml;
}
catch (Exception f)
{
string message = f.Message;
}
}
}
Summary
This sample XML Custom Placeholder renders its XML content using different XSL templates for Authoring and Presentation mode. Defining special XSL templates for authoring is an easy approach to create an authoring interface for editing that XML content.
To use this special XML Custom Placeholder, just drag it into an XML template. Then create a folder in your Web Application containing 3 files:
Template.xml
The piece of xml representing a new item template
<ItemList id="MyPlaceholderControl1">
<Item id="Author">
<Cell id="Nationality">[type nationality here]</Cell>
</Item>
</ItemList>
Authoring.xsl
The XLST template that should be applied in Authoring Mode
<xsl:template match="Item">
...
<input size="10">
<xsl:attribute name="name">
<xsl:value-of select="../@id" />:<xsl:value-of select="@id" />:Nationality
</xsl:attribute>
<xsl:attribute name="value">
<xsl:value-of select="Cell[@id='Nationality']" />
</xsl:attribute>
</input>
...
</xsl:template>
Presentation.xsl
The XLST template that should be applied in Presentation Mode
<xsl:template match="Item">
...
<div>
<xsl:value-of select="Cell[@id='Nationality']"/>
</div>
...
</xsl:template>
Finnaly, set the ConfigUrl property for the Placeholder pointing the folder you created. The control does all the rest for you.
To get the full source code for this example, e-mail the author aribeiro@microsoft.com