Since
the release date of the UI Update 2012 (Experience Manager or XPM) I have
noticed some challenges during implementations while integrating XPM Session
Preview with dynamic web sites or when some kind of URL Routing is used.
When I refer to dynamic web sites, I
refer to web sites using MVC and any kind of URL Routing.
The Problem
- Implementers are not aware that Tridion Content Delivery API (Java or .Net) and Tridion Content Delivery Web Service (since Tridion 2013 only) already understand when a session preview token is available and will return the data from the right data store (preview or regular broker) automatically.
- Implementers consider that Page/Binary filters (Java) or the Session Preview Module (.Net) are mandatory in an XPM set up while they should be considered as optional and just useful in some scenarios. These filters and Modules are very useful when our site is static and there is no URL routing involved.
The Solution
The
solution is always to identify what is your current scenario in order to select
the correct setup and implementation, in this post I will describe some
scenarios but of course they are not a final list.
·
Web Site with
fixed URLs
The Page/Binary filters or Session Preview Module are ideal for
this scenario, XPM will serialize temporary files by appending the session
preview token to the file names forwarding requests to these files in case a
Session Preview Toke is enabled.
Scenarios that fall in this category are:
o
ASP .Net Web Forms without Routing.
o
Classic JSP Web Application without routing.
o
Static HTML Web Site.
·
Web Site with different Context
Locations
When
I refer to different context locations I consider scenarios when in the Web
Application we use a context location that does not match with the current URL,
this scenario is not URL routing but URL forwarding. In this case the static
files exist in the file system but they are stored in a different location so
the web application needs to map the URL with the real file path and forward
the request. There is a hot fix available for this scenarios, the hot fix will
allow you to define 4 claims as defined below:
o
full_url:
The full http request URL.
o
real_path:
The path in the file system for the http request.
o
root_path:
The context path.
o
forwarding:
Indicates if the http request should be forwarded or not.
These
Ambient Data Framework claims should be filled using some custom logic like a
java filter or a .net http module.
Scenarios that fall in this category are:
o
Java
Web Application with a different context location defined in the Web
Application Description.
o
Java
Web Application that uses a Content Rendition Framework.
·
Dynamic Web Sites
In
this category I will group all the web sites using MVC as an implementation
pattern as well as any website using URL Routing.
For
this kind of web sites I found that the Page/Binary filters or the Session
Preview Module are not good options and we should avoid them, the solution for
those scenarios can vary depending on the nature of the implementation, it
could be as simple as just remove the filters or module from the web
application to develop mechanisms to get information from the preview data
store.
Scenarios
that fall in this category are:
o
ASP
.Net MVC Web Sites
o
Spring
MVC Web Sites
o
ASP
.Net Web Forms using Routing
The Example
For this
example I am using ASP .Net MVC 4 and I will show how to accomplish session
preview for a fully dynamic web site using a Virtual Path Provider and a
extending the existing Razor View Engine.
- Create a Virtual File
public class
TridionVirtualFile : VirtualFile {
private static readonly int publicationId = 7;
public
TridionVirtualFile(string virtualPath) : base(virtualPath) {
}
public static bool
FileExist(string virtualPath) {
if (Path.GetExtension(virtualPath).Equals(".cshtml")) {
PageMetaFactory
pageMetaFactory = new PageMetaFactory(publicationId);
return
pageMetaFactory.GetMetaByUrl(publicationId, virtualPath) != null;
}
return false;
}
public bool FileExist(string
virtualPath, out IPageMeta
pageMeta) {
PageMetaFactory
pageMetaFactory = new PageMetaFactory(publicationId);
pageMeta =
pageMetaFactory.GetMetaByUrl(publicationId, virtualPath);
return
pageMeta != null;
}
public override Stream
Open() {
IPageMeta
pageMeta;
if
(FileExist(VirtualPath, out pageMeta)) {
PageContentAssembler
assembler = new PageContentAssembler();
string
content =
assembler.GetContent(pageMeta.PublicationId,
pageMeta.Id);
byte[]
bytes = Encoding.UTF8.GetBytes(content);
return
new MemoryStream(bytes);
}
return new MemoryStream();
}
}
- Create a Virtual Path Provider
public class
TridionVirtualPathProvider : VirtualPathProvider {
public override bool
FileExists(string virtualPath) {
if (TridionVirtualFile.FileExist(virtualPath)) {
return
true;
}
else {
return
Previous.FileExists(virtualPath);
}
}
public override VirtualFile
GetFile(string virtualPath) {
if (TridionVirtualFile.FileExist(virtualPath)) {
return
new TridionVirtualFile(virtualPath);
}
else {
return
Previous.GetFile(virtualPath);
}
}
public override CacheDependency
GetCacheDependency(
string
virtualPath,
IEnumerable
virtualPathDependencies,
DateTime
utcStart) {
if (TridionVirtualFile.FileExist(virtualPath)) {
return
new CacheDependency(
new
string[] { HostingEnvironment.MapPath("~/Web.config") },
new
string[] { virtualPath }
);
}
else {
return
Previous.GetCacheDependency(
virtualPath,
virtualPathDependencies, utcStart);
}
}
}
- Register the Virtual Path Provider
public static
void RegisterVirtualPathProvider() {
HostingEnvironment.RegisterVirtualPathProvider(new
TridionVirtualPathProvider());
}
- Extend the Razor View Engine
public class
TridionViewEngine : RazorViewEngine
{
private const int
publicationId = [PublicationId];
public override ViewEngineResult
FindView(
ControllerContext controllerContext,
string
viewName,
string
masterName,
bool
useCache) {
ViewEngineResult
result =
base.FindView(controllerContext,
viewName, masterName, useCache);
if
(result.View != null) {
string
viewPath =
((RazorView)result.View).ViewPath.Replace("~", string.Empty);
PageMetaFactory
pageMetaFactory = new PageMetaFactory(publicationId);
IPageMeta
pageMeta =
pageMetaFactory.GetMetaByUrl(publicationId,
viewPath);
if
(pageMeta != null) {
if
(HttpContext.Current.Cache[viewPath] == null) {
HttpContext.Current.Cache[viewPath]
=
pageMeta.LastPublicationDate;
}
else
{
DateTime
lastPublishedDate =
(DateTime)HttpContext.Current.Cache[viewPath];
if
(lastPublishedDate < pageMeta.LastPublicationDate) {
HttpContext.Current.Cache.Remove(viewPath);
result =
base.FindView(controllerContext,
viewName, masterName, useCache);
HttpContext.Current.Cache[viewPath] =
pageMeta.LastPublicationDate;
}
}
}
}
return
result;
}
}
- Register our new View Engine
public static
void RegisterViewEngines() {
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new TridionViewEngine());
}
- Handle Images and Binaries
public class
SessionPreviewBinaryModule : IHttpModule {
private readonly int
publicationId = 7;
private readonly string
SessionPreviewToken =
"taf:claim:contentdelivery:webservice:preview:sessionid";
private readonly string
defaultVariantId = "default";
public void Init(HttpApplication
context) {
context.PreRequestHandlerExecute +=
new EventHandler(OnPreRequestHandlerExecute);
}
private void OnPreRequestHandlerExecute(object sender, EventArgs
e) {
HttpContext
httpContext = HttpContext.Current;
if
(httpContext != null) {
string
url = httpContext.Request.Url.AbsolutePath;
string
relativeUrl = httpContext.Request.Url.PathAndQuery;
if
(Path.GetExtension(url).ToLower().Equals(".jpg") ||
Path.GetExtension(url).ToLower().Equals(".gif") ||
Path.GetExtension(url).ToLower().Equals(".png")) {
ClaimStore
claimStore = AmbientDataContext.CurrentClaimStore;
Dictionary<Uri, object>
claims =
(Dictionary<Uri,
object>)claimStore.GetAll();
Uri
sessionTokenUri = new Uri(SessionPreviewToken);
if
(claims.ContainsKey(sessionTokenUri)) {
BinaryMetaFactory
binaryMetaFactory = new BinaryMetaFactory();
BinaryMeta
binaryMeta =
binaryMetaFactory.GetMetaByUrl(publicationId,
relativeUrl);
if
(binaryMeta != null) {
BinaryFactory binaryFactory = new
BinaryFactory();
BinaryData binaryData = binaryFactory.GetBinary(publicationId,
binaryMeta.Id, defaultVariantId);
HttpResponse response = httpContext.Response;
response.Clear();
response.ContentType =
binaryMeta.Type;
response.BinaryWrite(binaryData.Bytes);
response.Flush();
}
}
}
}
}
public void Dispose() {
}
}
- Use our views
public ActionResult
Index() {
return
View();
}