Adding meta data content to SiteMapXML

Topics: XSLT
Aug 22, 2011 at 1:06 PM

How do I retrieve data from some meta data along with the sitemap xml. I need the sitemap xml because of the treestructure

I have made a function with two function calls like this(Guids replaced with int's for readaility)
So I need to get the sitemapXML along with the Attributes in the GetMetaDataXml. They match with @Id and @PageId....

<in:result name="SitemapXml">       
<Page Id="1" Title="Medlemmer" MenuTitle="Medlemmer"  URL="/Forside/slaegtstrae/Medlemmer.aspx" Depth="3" isopen="true" iscurrent="true">           
<Page Id="2" Title="test1"  Description="" URL="/Forside/slaegtstrae/Medlemmer/test1.aspx?>               
<Page Id="3" Title="test2" URL="/Forside/slaegtstrae/Medlemmer/test/test2.aspx? Depth="5"/>
<Page Id="4" Title="test3" URL="/Forside/slaegtstrae/Medlemmer/test/test2.aspx? Depth="5"/>       
</Page>   
</Page> 
</in:result>
<in:result name="GetMetaDataXml">       
<MetaData PageId="2" Name="ShowMe with PageXML1" />       
<MetaData PageId="3" Name="ShowMe with PageXML2"/>
<MetaData PageId="4" Name="ShowMe with PageXML3"/>   
</in:result>

Coordinator
Aug 23, 2011 at 11:17 AM
Edited Aug 23, 2011 at 11:23 AM

We really should add this as a build in feature, but for now you can do it by introducing a new function and then use that when you need a sitemap structured view of data:

I created a small utility function which will eat XML that relate to pages, and return it below sitemap nodes – it takes a flat XML data set and returns a hierarchical data set, where the flat data hangs below pages.

Here is how to create the function (one time only):

  1. Go to the Functions perspective and add a new “Inline C# Function”
  2. Give it a name and namespace that fit you, like “MergeIntoSitemap” (name) and “Tools.Xml” (namespace)
  3. Press “Finish”. Document view opens.
  4. Delete the boiler plate source code and paste in the source code shown below.
  5. Go to the ‘Input Parameters’ tab and add the following:
    Name: pageDataElements, label: Page Data Elements, type: IEnumerable<XElement>
    Name: pageKeyAttributeName, label: Page Key Attribute Name, type: string, default value: PageId
  6. Optional: Give a test value to parameter pageDataElements where you select some XML containing page data (include PageId column in selection). This allow you to preview the function.
  7. Save

With this function now in your system, you can use it to wrap any Get(Data)Xml call and get that data ordered into a sitemap.

Here is how to use the function:

Example, before (the original Get(Data)Xml call)

 

      <f:function name="Layout.Navigation.TopLink.GetTopLinkXml">
        <f:param name="PropertyNames">
          <f:paramelement value="PageId" />
          <f:paramelement value="Page.Title" />
        </f:param>
      </f:function>

Example, after (the original is wrapped in the new function)

 

 

  <f:function name="Tools.Xml.MergeIntoSitemap" localname="MergeIntoSitemap">
    <f:param name="pageDataElements">
      <f:function name="Layout.Navigation.TopLink.GetTopLinkXml">
        <f:param name="PropertyNames">
          <f:paramelement value="PageId" />
          <f:paramelement value="Page.Title" />
        </f:param>
      </f:function>
    </f:param>
  </f:function>

The C# source code for the Inline C# Function:

 

using System;
using System.Collections.Generic;
using System.Linq;
using Composite.Data;
using System.Xml.Linq;

namespace Tools.Xml
{
  public static class InlineMethodFunction
  {
    public static IEnumerable<XElement> MergeIntoSitemap(IEnumerable<XElement> pageDataElements, string pageKeyAttributeName)
        {
            XElement resultContainer = new XElement("root");

            using (var dc = new DataConnection())
            {
                foreach (XElement pageDataElement in pageDataElements)
                {
                    if (pageDataElement.Attribute(pageKeyAttributeName) != null)
                    {
                        Guid pageId = Guid.Parse(pageDataElement.Attribute(pageKeyAttributeName).Value);
                        XElement pageElement = IncludeInSitemap(resultContainer, dc.SitemapNavigator, pageId);
                        pageElement.Add(pageDataElement);
                    }
                    else
                    {
                        resultContainer.Add(pageDataElement);
                    }
                }
            }

            return resultContainer.Elements();
        }
        
        

        private static XElement IncludeInSitemap(XElement sitemapContainer, SitemapNavigator sitemapNavigator, Guid pageId)
        {
            XElement matchPage;

            var matchPageAttribute = sitemapContainer.Elements("Page").Attributes("Id").Where(a => a.Value == pageId.ToString()).FirstOrDefault();

            if (matchPageAttribute == null)
            {
                PageNode pageNode = sitemapNavigator.GetPageNodeById(pageId);

                if (pageNode == null)
                    throw new InvalidOperationException("Got unknown guid in a from pageKeyAttributeName attriubute - no page with that id. Did you set the right pageKeyAttributeName?");

                XElement parent = sitemapContainer;
                if (pageNode.ParentPage != null)
                {
                    Guid parentId = pageNode.ParentPage.Id;
                    parent = IncludeInSitemap(sitemapContainer, sitemapNavigator, parentId);
                }

                matchPage = new XElement(pageNode.SitemapXml);
                matchPage.Elements().Remove();
                parent.Add(matchPage);
            }
            else
            {
                matchPage = matchPageAttribute.Parent;
            }

            return matchPage;
        }

  }
}
      


Coordinator
Aug 23, 2011 at 12:03 PM
Edited Aug 31, 2011 at 7:51 AM

You can achieve the same objective with pure Xslt. You can use an xslt template that would merge results from those function calls. 

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
	xmlns:in="http://www.composite.net/ns/transformation/input/1.0"
	xmlns:lang="http://www.composite.net/ns/localization/1.0"
	xmlns:f="http://www.composite.net/ns/function/1.0"
	xmlns:msxsl="urn:schemas-microsoft-com:xslt"
	xmlns="http://www.w3.org/1999/xhtml"
	exclude-result-prefixes="xsl in lang f">
          
	<xsl:variable name="SiteMap" select="/in:inputs/in:result[@name='SitemapXml']/Page" />
	<xsl:variable name="TeaserSelectors" select="/in:inputs/in:result[@name='GetTeaserSelectorXml']/TeaserSelector" />


	<xsl:variable name="SiteMapWithMetaData">
		<xsl:apply-templates select="$SiteMap" mode="MergeMetaData">
			<xsl:with-param name="MetaData" select="$TeaserSelectors" />
		</xsl:apply-templates>
	</xsl:variable>

	<!-- Template that merges SiteMap with meta data -->
	<xsl:template match="@* | node()" mode="MergeMetaData">
		<xsl:param name="MetaData" />
		
		<xsl:copy>
			<xsl:apply-templates select="@*" mode="MergeMetaData"/>

			<xsl:variable name="PageId" select="@Id" />
			<xsl:for-each select="$MetaData[@PageId=$PageId]">
				
				<xsl:for-each select="@*">
					<xsl:attribute name="MetaData.{name()}">
						<xsl:value-of select="."/>
					</xsl:attribute>
				</xsl:for-each>
							  
			</xsl:for-each>

			<xsl:apply-templates select="node()" mode="MergeMetaData">
				<xsl:with-param name="MetaData" select="$MetaData" />
			</xsl:apply-templates>
		</xsl:copy>
	</xsl:template>

	<xsl:template match="/">
		<html>
			<head></head>
			<body>
				<xsl:copy-of select="msxsl:node-set($SiteMapWithMetaData)" />
			</body>
		</html>
	</xsl:template>

</xsl:stylesheet>

 

In your example the result would be like:

 

<Page Id="1" Title="Medlemmer" MenuTitle="Medlemmer"  URL="/Forside/slaegtstrae/Medlemmer.aspx" Depth="3" isopen="true" iscurrent="true">
	<Page Id="2" Title="test1"  Description="" URL="/Forside/slaegtstrae/Medlemmer/test1.aspx?" MetaData.Name="ShowMe with PageXML1">
		<Page Id="3" Title="test2" URL="/Forside/slaegtstrae/Medlemmer/test/test2.aspx?" Depth="5" MetaData.Name="ShowMe with PageXML2"/>
		<Page Id="4" Title="test3" URL="/Forside/slaegtstrae/Medlemmer/test/test2.aspx?" Depth="5" MetaData.Name="ShowMe with PageXML3" />
	</Page>
</Page>
Aug 24, 2011 at 8:08 PM

Tnx a lot for answering

I have added the C# function with a xml test value containing the XML for my meta-data, added PageId and other attributes in the selection. the preview then shows: 

System.Xml.Linq.XContainer+<GetElements>d__11

Note! the default value of pageKeyAttributeName is of the type Composite.Constant.String with value PageId, correct?

My function markup is: 
<f:function xmlns:f="http://www.composite.net/ns/function/1.0" name="Slaegt.Familytree" />

Wrapped:
<f:function name="Tools.Xml.MergeIntoSitemap" localname="MergeIntoSitemap">   
<f:param name="pageDataElements">       
<f:function xmlns:f="http://www.composite.net/ns/function/1.0" name="Slaegt.Familytree" />   
</f:param> 
</f:function>

It outputs an error in the browser: <span class="c1error">[ ERROR ]</span>

Any ideas to where the bug is?


Aug 24, 2011 at 8:16 PM

Hey Napernik

Great xslt, it renders just fine. 

But the result is XML in the output window. As I see it I can't access it as an XML document and therefor not transform it with xslt. Or?

I tried with <xsl:document along with other posibilities without success

I'm thinking the Output window in the preview tab is after the transformation so I'm not sure how to handle it

Coordinator
Aug 24, 2011 at 9:47 PM

>> But the result is XML in the output window. As I see it I can't access it as an XML document and therefor not transform it with xslt. Or?

The output structure is kept inside an xslt variable $SiteMapWithMetaData.In the example I just put it to output by calling:

<xsl:copy-of select="$SiteMapWithMetaData"/>

Buy you can process the variable with xslt to get the output you need.


Usually when you transform sitemap, you have some code like:

<xsl:apply-templates select="/in:inputs/in:result[@name='SitemapXml']/Page" />


The same code can be rewritten to use a variable

<xsl:variable name="SiteMap" select="/in:inputs/in:result[@name='SitemapXml']/Page" />
....
<xsl:apply-templates select="$SiteMap" />


Through defining xslt variables (which are in fact constansts, since they don't change value after being defined :) ), you can apply transormations to dynamicly formed XML, not just the one that comes from input

Example. if you want to have a div for each page, that would look something like:

	<!-- root template -->
	<xsl:template match="/">
		<html>
			<head></head>
			<body>
				<xsl:apply-templates select="$SiteMapWithMetaData"/>
			</body>
		</html>
	</xsl:template>

<xsl:template match="Page">
	<div style="padding-left: 10px;">
		PageId: <xsl:value-of select="@Id" />
		<br />
		Title:  <xsl:value-of select="@Title" />
		<br />
		Name:  <xsl:value-of select="@MetaData.Name" />
		<br />

		<xsl:apply-templates select="./Page"/>
	</div>
</xsl:template>

Coordinator
Aug 24, 2011 at 11:11 PM

>> Note! the default value of pageKeyAttributeName is of the type Composite.Constant.String with value PageId, correct?

Corrent - PageId is a sensible default value - expecting that the XML elements passed via the first parameter (pageDataElements) has an attribute @PageId, since PageId is the default foreign key column name on data types that attach to pages.

>> It outputs an error in the browser: <span class="c1error">[ ERROR ]</span>

Try viewing that using a browser that is logged into the C1 Console - if you are logged in, the error message will be much more detailed (I'm guessing you preview pages using another browser than the one running the C1 Console). Also, try previewing your XSLT Function - it should probably also give you a good error. With the error message it is easier to diagnose.

My guess is that this construct is the problem:

<f:function name="Tools.Xml.MergeIntoSitemap" localname="MergeIntoSitemap" xmlns:f="http://www.composite.net/ns/function/1.0">   
  <f:param name="pageDataElements">       
    <f:function name="Slaegt.Familytree" />   
 < /f:param> 
</f:function>

Your function "Slaegt.Familytree" has to be a function that returns IEnumerable<XElement> - if it is not, then that is a cause for error. Note that all the Get(Data)Xml functions you have for your data types do that, and you can use them. For instance, if you had a data type named "Slaegt.Familytree", then you would use the function "Slaegt.Familytree.GetFamilytreeXml"and remember to also select the PageId attribute from it.

Please send the error message if the above info don't bring you to a solution. Also, some info on Slaegt.Familytree.


Aug 26, 2011 at 5:57 AM

Hey Mawtex

This is the full error: Failed to get value for function 'Tools.Xml.MergeIntoSitemap' - No conversion from Composite.Core.Xml.XhtmlDocument to System.Collections.Generic.IEnumerable`1[System.Xml.Linq.XElement] could be found

I'm not quite sure what you mean with returning  an IEnumerable. Should it be output type XML instead of XHTML?  

The Slaegt.Familytree is a XHTML output. It has two function calls, the SitemapXML and GetSlaegtstraeXML

It has no input parameter(We agree that PageId is input parameter on the C# function right?)

The input XML is like this(Guids replaced with int's for readability):
<in:result name="SitemapXml">        
<Page Id="1" Title="Medlemmer" MenuTitle="Medlemmer"  URL="/Forside/slaegtstrae/Medlemmer.aspx" Depth="3" isopen="true" iscurrent="true">            
<Page Id="2" Title="test1"  Description="" URL="/Forside/slaegtstrae/Medlemmer/test1.aspx?>                
<Page Id="3" Title="test2" URL="/Forside/slaegtstrae/Medlemmer/test/test2.aspx? Depth="5"/> 
<Page Id="4" Title="test3" URL="/Forside/slaegtstrae/Medlemmer/test/test2.aspx? Depth="5"/>        
</Page>    
</Page> 
</in:result>
<in:result name="GetMetaDataXml">        
<MetaData PageId="2" Name="ShowMe with PageXML1" />        
<MetaData PageId="3" Name="ShowMe with PageXML2"/> 
<MetaData PageId="4" Name="ShowMe with PageXML3"/>    
</in:result> 

Aug 30, 2011 at 3:50 PM

Hey Napernik

I'm aware of the xslt you're writing and I tried it out. The reason i wrote But the result is XML in the output window. As I see it I can't access it as an XML document and therefor not transform it with xslt. Or?

is that I got the following error when trying to access the attributes in the XML output

Failed to get value for function 'Slaegt.Familytree' - Hvis du vil bruge et resultattræfragment i et stiudtryk, skal du først konvertere det til et nodesæt vha. funktionen msxsl:node-set().

Any ideas?

Coordinator
Aug 31, 2011 at 7:54 AM

>> Any ideas?

I updated the example. Add an xml namespace

xmlns:msxsl="urn:schemas-microsoft-com:xslt"

Wrap result variable usage with a call to msxsl:node-set(...)

<xsl:copy-of select="msxsl:node-set($SiteMapWithMetaData)" />