Literales y variables de contexto

En más de una ocasión se ha comentado en la comunidad la conveniencia de utilizar variables compartidas entre las plantillas de página y las plantillas de componente. Estrictamente hablando, el renderizado de una plantilla de componente, no deberia “conocer” ni depender de ningún valor de la plantilla de página y viceversa.
Esto suele ser cierto pero hay ocasiones en que compartir información entre la plantilla de página la plantilla de componente puede ser beneficioso.
La gestión de literales se puede llevar a cabo de diferentes maneras; no vamos a tratar ahora de la mejor forma de realizarlo, pero la manera en que lo solemos realizar se puede beneficiar de este espacio compartido entre el renderizado de plantillas. Las dos aproximaciones básicas de gestión de literales en Tridion, son la de crear un componente por cada literal, basado en un esquema simple de nombre/valor, o crear un componente multivalor. Nosotros optamos por la primera opción, por la posibilidad de gestionar independientemente los valores, por ejemplo para la localización y posterior traducción o la agrupación de literales relacionados.
Estos componentes pueden ser utilizados para publicar un fichero de recursos a consumir en la capa de presentación, o ser utilizados directamente en la publicación de cada página o componente. En nuestro caso, como regla general, utilizamos la segunda opción.
En este paso es cuando las variables de contexto pueden sernos útiles.
Al principio solíamos utilizar una función interna de Tridion para obtener el valor del literal correspondiente:
using System;

/// <summary>
/// Summary description for Class1
/// </summary>
public class Class1 {

	public string fcs_GetLiteralOld(string LiteralName) {
		string currentPubID = fcs_GetCurrentPublicationID();
		//partimos del URI de la carpeta donde se guardan los literales
		//cuyo id se guardará en un fichero de configuracion
		string strLiteralesFolderURI = "tcm:xx-xxxx-2";

		OrganizationalItemItemsFilter listItemsFilter = new OrganizationalItemItemsFilter(fcs_Engine.GetSession());
		listItemsFilter.ItemTypes = new ItemType[] {
			ItemType.Component
		};
		Publication oPub = GetPublication();
		Folder literalesFolder = (Folder) oPub.GetObject(strLiteralesFolderURI);
		XmlElement literalesFolderListItems = literalesFolder.GetListItems(listItemsFilter);
		XmlElement xmlLiteral = (XmlElement) literalesFolderListItems.SelectSingleNode(String.Format("/tcm:ListItems/tcm:Item[@Title='{0}']", LiteralName), GetXmlNamespaceManager(literalesFolderListItems.OwnerDocument));
		if (null != xmlLiteral) {
			Component literalComponent = (Component) oPub.GetObject(xmlLiteral.GetAttribute("ID"));
			if (null != literalComponent) {
				XmlNamespaceManager nsmgr = GetXmlNamespaceManager(literalComponent.Content.OwnerDocument);
				nsmgr.AddNamespace("fcscontent", literalComponent.Content.NamespaceURI);
				XmlElement literalValue = (XmlElement) literalComponent.Content.SelectSingleNode("//fcscontent:value", nsmgr);
				if (null != literalValue) {
					return literalValue.InnerXml;
				} else {
					_Log.Error(String.Format("Cannot find Literal value for [{0}]: Cannot find with Xpath expression /Literal/value.", LiteralName));
				}
			}
		}

		return String.Format("[{0}]", LiteralName);
	}
}
Esta función es poco eficiente, ya que recorre todos los componentes de tipo literal para encontrar el que necesitamos y obtener su valor, pero para la publicación de una página habitual no debería suponer una gran perdida de tiempo. Sin embargo, siempre existen páginas con gran contenido de literales, que pueden llegar a ser un problema en cuanto a tiempos de publicación. Y sobre todo, con el aumento del uso de Experience Manager para la edición y publicación inline, el tiempo de publicación puede llegar a ser un problema para la experiencia de usuario.
Si conseguimos hacer la lectura de los literales una sola vez, durante todo el renderizado de la página, y utilizar los valores leídos, puede suponer un ahorro de tiempo importante. Con ese objetivo parece que las variables de contexto son la solución, ya que según las funciones disponibles (Built In Functions) dentro del lenguaje de plantillas Dreamweaver, está disponible la siguiente función:
SetRenderContextVariable(Object name, Object value)
Añade (o actualiza) una variable en el contexto de renderizado. Use este método parqa pasar información entre renderizadores (como una Plantilla de Componente y una plantilla de página).
Según parece, por la definición, esta función añade una variable en una plantilla, y sería accesible desde otras plantillas dentro del mismo renderizado, pero hemos observado en diferentes ocasiones que esto no se cumple en la mayoría de los casos, y los valores que se asignan a las variables, suelen mostrar valores “inesperados”, la mayoría de las ocasiones por el orden de renderizado de los elementos dentro de la plantilla, y sobre todo por el comentario de @dominic_cronin en su artículo sobre el Context Bag” que dice que el diccionario de variables de contexto en cada renderizador de componente, es una copia del renderizador de la página, por lo que son elementos de sólo lectura, pero se pueden pasar las referencias a otros objetos que sí sean “modificables”.
En nuestra implementación práctica del Context Bag vamos a utilizar un documento XML con el listado de componentes de tipo literal que tenemos en nuestra publicación, que se leerá una sola vez, y se irá pasando a través de los distintos renderizadores de página y componente de una página.
En la función GetLiteral primero consultamos si existe la variable del Context Bag creada, y si no existe, se leen los valores y se crea el objeto que meteremos en el Context Bag.
Si existe, se consulta el literal requerido simplemente.
public string fcs_GetLiteral(string LiteralName) {
	_Log.Debug("lit: " + LiteralName);
	Publication oPub = GetPublication();

	// First we check to see if the Literales have been loaded into memory
	//var packageItem_LiteralesFolderListItems =
	XmlElement literalesFolderListItems = fcs_GetParameterFromBag(PACKAGE_ITEM_LITERALES_FOLDER_LIST_ITEMS);;

	if (literalesFolderListItems != null) {
		// Literales have been retrieved from memory. No need to read config from disk.
		_Log.Debug("literales del bag");
	} else {
		_Log.Debug("leer literales");
		// Seems to be the first time in. Need to read Literales and store them in memory.
		string currentPubID = oPub.Id.ItemId.ToString();
		XmlElement configFolderElement = (XmlElement) _TCMFunctionsConfig.SelectSingleNode(XPATH_CONFIG_LITERALFOLDER + "[@pub='" + currentPubID + "']");

		if (null != configFolderElement) {
			//obtenemos el URI de la carpeta donde se guardan los literales
			//cuyo id se guardará en el fichero de configuracion
			string strLiteralesFolderURI = "tcm:xx-xxxx-2";

			try {
				Folder literalesFolder = (Folder) oPub.GetObject(strLiteralesFolderURI);

				OrganizationalItemItemsFilter listItemsFilter = new OrganizationalItemItemsFilter(fcs_Engine.GetSession());
				listItemsFilter.ItemTypes = new ItemType[] {
					ItemType.Component
				};
				listItemsFilter.BaseColumns = ListBaseColumns.IdAndTitle;

				literalesFolderListItems = literalesFolder.GetListItems(listItemsFilter);
				_Log.Debug("añade diccionario");
				fcs_AddParameterToBag(PACKAGE_ITEM_LITERALES_FOLDER_LIST_ITEMS, literalesFolderListItems);
			} catch (Exception ex) {
				_Log.Error("Error reading folder [" + strLiteralesFolderURI + "]." + ex.Message);
				return String.Format("[{0}]", LiteralName);
			}
		} else {
			_Log.Error("Cannot find Id value for Literales folder in " + XMLCONFIG_FILELOCATION);
			return String.Format("[{0}]", LiteralName);
		}
	}

	XmlElement xmlLiteral = (XmlElement) literalesFolderListItems.SelectSingleNode(String.Format("/tcm:ListItems/tcm:Item[@Title='{0}']", LiteralName), GetXmlNamespaceManager(literalesFolderListItems.OwnerDocument));
	if (null != xmlLiteral) {
		Component literalComponent = (Component) oPub.GetObject(xmlLiteral.GetAttribute("ID"));
		if (null != literalComponent) {
			XmlNamespaceManager nsmgr = GetXmlNamespaceManager(literalComponent.Content.OwnerDocument);
			nsmgr.AddNamespace("fcscontent", literalComponent.Content.NamespaceURI);
			XmlElement literalValue = (XmlElement) literalComponent.Content.SelectSingleNode("//fcscontent:value", nsmgr);
			if (null != literalValue) {
				return literalValue.InnerXml;
			} else {
				_Log.Error(String.Format("Cannot find Literal value for [{0}]: Cannot find with Xpath expression /Literal/value.", LiteralName));
			}
		}
	}

	return String.Format("[{0}]", LiteralName);
}

Las funciones auxiliares que acceden al Context Bag para incluir una variable o leerla son las siguientes:

public XmlElement fcs_GetParameterFromBag(string KeyName) {
	_Log.Debug("leemos del bag " + KeyName);
	// the bag is the main object
	if (fcs_Engine.PublishingContext == null) {
		return null;
	} else {
		RenderContext renderContext = fcs_Engine.PublishingContext.RenderContext;
		if (renderContext == null || renderContext.ContextVariables == null) {
			return null;
		} else {
			if (renderContext.ContextVariables.Contains("bag")) {
				_Log.Debug("tiene bag");
				var dicc = (Dictionary < string, XmlElement > ) renderContext.ContextVariables["bag"];
				if (dicc.ContainsKey(KeyName)) {
					_Log.Debug("el dicc tiene valores?" + dicc.Count);
					return (XmlElement) dicc[KeyName];
				} else {
					_Log.Debug("el dicc no tiene valores?" + dicc.Count);
					return null;
				}
			} else return null;
		}
	}
}

public void fcs_AddParameterToBag(string KeyName, XmlElement KeyValue) {
	if (fcs_Engine.PublishingContext == null) {
		return;
	} else {
		RenderContext renderContext = fcs_Engine.PublishingContext.RenderContext;
		if (renderContext == null || renderContext.ContextVariables == null) {
			return;
		} else {
			if (renderContext.ContextVariables.Contains("bag")) {
				_Log.Debug("existe bag");
				var contextBag = (Dictionary < string, XmlElement > ) renderContext.ContextVariables["bag"];
				contextBag[KeyName] = KeyValue;
			} else {
				_Log.Debug("no existe, creamos bag");
				Dictionary < string, XmlElement > contextBag = new Dictionary < string, XmlElement > ();
				contextBag.Add(PACKAGE_ITEM_LITERALES_FOLDER_LIST_ITEMS, KeyValue);
				renderContext.ContextVariables.Add("bag", contextBag);
			}
			_Log.Debug(String.Format("Render context variable (en Bag) '{0}' set to '{1}'", KeyName, KeyValue.OuterXml));
			return;
		}
	}
}
Como se puede ver, El elemento que se va a intercambiar entre renderizados es el “contextBag” que consiste en un diccionario de elementos nombre/valor que son los que podemos modificar entre renderizados.
De esta manera podemos intercambiar valores entre diferentes plantillas en el renderizado completo de una página.