Tuesday, April 24, 2012

Hi All, I just want to assure you that I have indeed NOT forgotten about this little site, and that in the upcoming couple months we'll be seeing lots of new content from what I've been working on at Ektron posted here as well as ektron.com. :) I think you'll like it!

Tuesday, January 31, 2012

Custom Data Lists in SmartForms


Intro

Ektron Smartforms are one of the key content types that all Ektron customers take advantage of in one way or another. They are reusable, extendable, provide a great way of structuring data, and provide your content authors with easy to input forms for content entry.  But out of the box, the Ektron smartforms aren’t customized to fit with YOUR information architecture.  Yes, we’ve made great recent improvements especially regarding the Resource Selector which allows you to associate content, folder, and taxonomy items to your content item. But what if you want to associate other organizational items or custom types not defined within the selector? The following guide will provide you with code and instructions on extending your SmartForms to allow for custom list types in an easy to extend class that uses our newest Framework API functionality.

Getting Ready

To get ready there’s a few things you’ll need to make this easier:
·         Visual Studio 2010 (Express will work)
·         Write access to: \Workarea\ContentDesigner\DataListSpec.xml
·         Write access to: \Workarea\ContentDesigner\Resources\DataListSpec[.*].resx

First Step

Alright, to get started I have created a fairly straightforward ashx file that is really at the heart of all this. What this file does, is provide a XML structured string to your SmartForm select field with your list data.  I have created the following basic lists and hopefully have made it easy enough to extend for you to further customize for your own site architecture:
·         Content List
·         Folder List
·         Taxonomy List
·         Collection List
·         Menu List
Copy the following code and paste it in a new .ASHX file in your site root or somewhere that makes sense within your site architecture:


<%@ WebHandler Language="C#" Class="CustomList" %>

using System;
using System.Web;
using System.Collections.Generic;
using System.Text;
using Ektron.Cms.Content;

public class CustomList : IHttpHandler {
    
    

    public void ProcessRequest (HttpContext context) {
        string type = context.Request.QueryString["type"] ?? "ContentList";
        long parentId = QueryString("parentId", 0, context);

        context.Response.ContentType = "text/xml";
        
        context.Response.Write("");
    }
 
    public bool IsReusable {
        get {
            return false;
        }
    }

    public static long QueryString(string paramName, int defaultValue, HttpContext context)
    {
        long value;
        if (!long.TryParse(context.Request.QueryString[paramName], out value))
            return defaultValue;
        return value;
    }
}

public class EktLists
{

    long _parentId;
    
    public EktLists(long parentId)
    {
        _parentId = parentId;
    }
    
    public string ContentList()
    {
        StringBuilder options = new StringBuilder();

        Ektron.Cms.Framework.Content.ContentManager cm = new Ektron.Cms.Framework.Content.ContentManager();
        ContentCriteria criteria = new ContentCriteria(Ektron.Cms.Common.ContentProperty.Id, Ektron.Cms.Common.EkEnumeration.OrderByDirection.Ascending);
        criteria.AddFilter(Ektron.Cms.Common.ContentProperty.FolderId, Ektron.Cms.Common.CriteriaFilterOperator.EqualTo, _parentId);
        
        List list = cm.GetList(criteria);

        foreach (Ektron.Cms.ContentData item in list)
        {
            options.AppendFormat("", item.Id, item.Title);
        }
        
        return options.ToString();  
    }

    public string FolderList()
    {
        StringBuilder options = new StringBuilder();

        Ektron.Cms.Framework.Organization.FolderManager fm = new Ektron.Cms.Framework.Organization.FolderManager();
        Ektron.Cms.FolderCriteria criteria = new Ektron.Cms.FolderCriteria(Ektron.Cms.Common.FolderProperty.Id, Ektron.Cms.Common.EkEnumeration.OrderByDirection.Ascending);
        criteria.AddFilter(Ektron.Cms.Common.FolderProperty.Id, Ektron.Cms.Common.CriteriaFilterOperator.EqualTo, _parentId);
        
        List list = fm.GetList(criteria);

        foreach (Ektron.Cms.FolderData item in list)
        {
            options.AppendFormat("", item.Id, item.Name);
        }

        return options.ToString();  
    }

    public string TaxonomyList()
    {
        StringBuilder options = new StringBuilder();

        Ektron.Cms.Framework.Organization.TaxonomyManager tm = new Ektron.Cms.Framework.Organization.TaxonomyManager();
        Ektron.Cms.Organization.TaxonomyCriteria criteria = new Ektron.Cms.Organization.TaxonomyCriteria(Ektron.Cms.Organization.TaxonomyProperty.Name, Ektron.Cms.Common.EkEnumeration.OrderByDirection.Ascending);
        criteria.AddFilter(Ektron.Cms.Organization.TaxonomyProperty.Id, Ektron.Cms.Common.CriteriaFilterOperator.EqualTo, _parentId);
        
        List list = tm.GetList(criteria);

        foreach (Ektron.Cms.TaxonomyData item in list)
        {
            options.AppendFormat("", item.Id, item.Name);
        }

        return options.ToString();  
    }

    public string CollectionList()
    {
        StringBuilder options = new StringBuilder();

        Ektron.Cms.Framework.Organization.CollectionManager cm = new Ektron.Cms.Framework.Organization.CollectionManager();
        Ektron.Cms.CollectionCriteria criteria = new Ektron.Cms.CollectionCriteria(Ektron.Cms.Common.ContentCollectionProperty.Title, Ektron.Cms.Common.EkEnumeration.OrderByDirection.Ascending);
        criteria.AddFilter(Ektron.Cms.Common.ContentCollectionProperty.Id, Ektron.Cms.Common.CriteriaFilterOperator.EqualTo, _parentId);

        List list = cm.GetList(criteria);
        foreach (Ektron.Cms.Organization.ContentCollectionData item in list)
        {
            options.AppendFormat("", item.Id, item.Title);
        }

        return options.ToString();
    }

    public string MenuList()
    {
        StringBuilder options = new StringBuilder();

        Ektron.Cms.Framework.Organization.MenuManager mm = new Ektron.Cms.Framework.Organization.MenuManager();

        Ektron.Cms.Organization.MenuData list = mm.GetTree(_parentId);
        foreach (Ektron.Cms.Organization.MenuItemData item in list.Items)
        {
            options.AppendFormat("", item.Id, item.Text);
        }

        return options.ToString();
    }
Now that you’ve pasted this in, let’s go over it a little.  You’ll notice the first Class, “CustomList” is simply a generic ashx handler, which looks at two arguments, nothing too special:
string Type: Used to define the type of list being loaded, current acceptable values are ContentList, FolderList, TaxonomyList, CollectionList, and MenuList.
long parentId: Used to define the containing organizational identifier. E.g. A parent folder id.
The second class “EkLists” however is where all the action happens. This class is instantiated right above the switch case and given the parentId for the container your list will be in.  The proper method which fills the list is then called. Your resulting string which is received by the editor will look a lot like:
<select>
<option value="30">Sample Content Block</option>
<option value="32">testing</option>
</select>

But we still need to tell the editor about our lists!

So now that we have our new super ektlist class outputting all the xml lists we can imagine, the editor still has no idea this file or our cool lists exist.  The good news is, all our list information is read in from a configuration file called DataListSpec.xml located in \Workarea\ContentDesigner\.
Open this file, and right before the ending </datalists> paste:
  <datalist name="ContentList" localeRef="ContentList" src="[path]/CustomList.ashx?type=ContentList&amp;parentId=0" cache="false" select="/select/option" captionxpath="." valuexpath="@value" validation="select-req">
    <item value="" localeRef="sSel"/>
  </datalist>

If you read the above statement, there are a few key parts:
  •  Name: Can be whatever you’d like localeRef: Name Key referenced in DataListSpec[.*].res
  • src: “[path]/CustomList.ashx” Location to file you imported earlier
  • type: Defaults to ContentList, can be modified to any list type defined.
  • parentId: Containing element of list items
  • validation: Default is select-req, which makes this field required

And that’s it; the rest of the values will normally not change. 

The only piece left is adding the reference into the resx file and we’re ready to start viewing our list(s).

Open “\Workarea\ContentDesigner\Resources\DataListSpec[.*].resx” which is a two column spreadsheet of a Name/Value pair. Create a new entry for any datalists you defined, where the Name equals “localeRef” and the Value can be whatever you’d like. For the above configuration I entered:

Name: ContentList, Value: ContentList(FolderID 0)

OK! If everything was put in the right spot, when we go into the SmartForm data designer we should now be able to see our new list for a “Choices Field” option.





Thanks for reading, please leave comments below for ideas or extensions on this. Also feel free to talk with me on twitter @andrew_eddy about this article or Ektron in general!


Tuesday, January 10, 2012

Expiring Content from the v8.5 Cache Framework


We in Ektron Engineering have been spending the past several years making our APIs and developer framework more user friendly and powerful with every release (see a webinar Framework API:Consistency, Discoverability, and Simplicity). We are extremely excited about the release of 8.5, and are working on anew era of development tools to complement our existing product set.
In v8.5, a transparent caching layer has been implemented. Thisnew caching layer sits below the Framework API and caches frequently accessed objects (see related webinar: v8.5 &Caching Best Practices). One common question is: "How do Invalidate the cache of a content object upon publish or update?"
Using Ektron CMS Extensions (plug-ins) you can modify the publish process to invalidate the framework cache. For those of you not familiar with either concept, allow me to provide a brief rundown on what this functionality can do.

CMS Extensions and the Strategy Pattern

Strategy Pattern is an design pattern that lets you select an implementation at runtime. For Ektron, CMS Extensions are a piece of code that is executed when a certain event fires, for example if you want to append a date to the title of your content, you could tie code to do this on the OnBeforePublish event. Now every time content is published it is executed through the strategy and a date will be appended.

The Framework API

Ektron's newest API that exposes the majority of feature sets in a straight forward way with the ability to use criteria to pull data, perform impersonation, integrate with WCF, and more.
With the most recent improvements to the Framework API, we have implemented an entire caching strategy to specify which objects we would like to automatically be stored in cache, and for how long.By doing this we greatly increase the framework performance by avoiding database hits until the cache is expired. Now this is all well and good, until we run into an instance where we need something to display immediately. Currently there are limited options to expire the cache in this scenario, you would either need a script that expires the cache entirely or by key (which would be difficult to discover), or (YIKES!) have to reset the IISapplication pool to see your content change immediately.
With the introduction of Unity into our Framework, we expose some extremely advanced caching options that solve this. The following is a step by step tutorial on creating a strategy that expires an individual cache based on content changes making cache keys dependent on changes, rather than time elapsed.
To get started let's make sure we have the necessary requirements setup within our CMS400 instance:
  • CMS400 version 8.5
  • Folder in workarea with more than one piece of content
  • Framework Caching turned on
While the first two steps are straight forward, the third step implies a little additional help. To turn on framework caching within your 8.5 I'll introduce you to a new config file in the root of your site, "ektron.cms.framework.unity.config". Upon opening this file you'll notice a few things, a list of framework class definitions, and 3 sections that define WCF, Cache, andDefault. Right now we're focused on using the Cache definition, since we're looking to cache our Framework functions. To turn on the framework caching, there should be a section in your web.config that looks like:
  <ektron.framework.services>
    <unity configSource="ektron.cms.framework.unity.config" />
    <framework defaultContainer="Default" childContainer="BusinessObjects" />
  </ektron.framework.services>
By simply changing the defaultContainer to "Cache" we have enabled it. You're framework api calls should now all be caching!You can set the timeouts individually on items by looking back inektron.cms.framework.unity.config, but the default should be okay for now.
So we have our framework caching now which is all well and good, but if we go back to our earlier scenario, we don't want to publish an item and wait around for the cache to expire, or have to expire the cache ourselves which could drastically hurt your site performance if the cache is emptied at once. With 8.5 fear no more, we can now expire the cache of items pulled through the framework api individually! We accomplish this by setting up a strategy, and for those of you that have done this before it's easier than ever in 8.5.
You can get an Ektron strategy up and running faster than ever ,you no longer need to compile the code, or use a utility to add it to your site. In a few painless steps you'll have as many hooks into your CMS system as you need. We'll start by creating anew Class file under your App_Code\CSCode, copy the code below replacing all text in the newly created CS file:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Ektron.Cms;
using Ektron.Cms.Common;
using Ektron.Cms.Instrumentation;
using Ektron.Cms.Extensibility;
using Ektron.Cms.Extensibility.Content;

namespace Cms.Extensions.Custom
{
  public class InvalidateFrameworkCache : ContentStrategy
  {
    public override void OnAfterPublishContent(ContentData contentData, CmsEventArgs eventArgs)
    {
      //Cache manager class to access Framework Cache
      Ektron.Cms.BusinessObjects.Caching.CacheManager cmanager = 
          new Ektron.Cms.BusinessObjects.Caching.CacheManager();

      //Fill cachekey in
      string CackeKey = string.Format("Ektron:content:{0}", contentData.Id);

      //Remove key from cache
      cmanager.Remove(CackeKey);

      //Write results to log
      Log.WriteInfo(string.Format("InvalidateFrameworkCache.OnAfterAddContent: {0}::{0}", 
         contentData.Id, contentData.Title));
    }
  }
}
Looking at the above code we immediately can see how simple this task is using our API's. The other thing you'll probably notice is how the cache key is formatted, we follow a very simple naming convention: "Ektron:object:id". We follow this convention and use our API's to make this "smart" functionality.What I mean by that is that all the places in the cache where this object shows, are now removed. So in three lines of code, based on the piece of content we just published, it has been removed from all the caches it resided in, this includes any list caches the content was in as well. The final step is to register this extension in the object factory so that it knows to execute. To register the extension you simply need to openObjectFactory.config (also in the root of your website) and change the content section to look like the following assuming this is anew install:
<add name="Content">
  <strategies>
    <add name="GoogleGeoCoder"       
      type="Cms.Extensions.GoogleGeoCoder.ContentStrategy, Cms.Extensions.GoogleGeoCoder"/>
    <add name="InvalidateFrameworkCache" type="Cms.Extensions.Custom.InvalidateFrameworkCache" />
   </strategies>
</add>
And there you have it! With a few very simple steps we have created a strategy to invalidate the cache of a content object upon publish. Hopefully by now your imagination is cooking up more ideas on how to improve your site performance and content delivery at the same time, please leave those ideas, or other questions in the comments below!