March 2007 Entries

Note: This article only refers to content placeholders, ie text, although much of the article would apply equally to attachment placeholders, albeit requring a little more effort.

When handling postings in MCMS using the Word Authoring Connector, there is no built-in way to target multiple or specific placeholders. The only way to ensure that the correct placeholder content is populated is to create the template definition with an OfficeHtmlPlaceholder definition or only place one HtmlPlaceholder definition on the page (the 'first' one is used if there are multiple HtmlPlaceholders - if there are multiple OfficeHtmlPlaceholders the content is placed in all of them).

Whilst this will meet most needs adequately, there are several cases where this could be a limitation. For example:

  • Targetting custom placeholders (the WAC will only target HtmlPlaceholder and OfficeHtmlPlaceholder)
  • A requirement to target multiple placeholders... don't expect a clean solution for this one :)
  • Requirement to target a specific HtmlPlaceholder which isn't 'first'
  • Requirement to target something which isn't a placeholder (possibly a custom property, for example)

Although some customisation of the WAC is possible, essentially it's a 'black box' and what you see is what you get. It's targetting logic is buried within and can't be tweaked. Fortunately, there's a fairly easy way to get around the limitations above.

The core concept is to add an OfficeHtmlPlaceholder on the template which remains hidden. This placeholder will always be targetted by the WAC (if there's more than one on the template, they'll all be targetted). The placeholder will be used as a 'holding area' for the content.

When the WAC is used to update/create a posting, the posting will then be at a 'Saved' state awaiting Approval. This is where the little bit of magic comes in.

As the posting should always be opened up to be approved (or rejected), then we can make use of the posting load event to take the content from our OfficeHtmlPlaceholder holding area and put it into the correct place/s.

A practical example of this follows. A template has two HtmlPlaceholders defined, and the code shown targets the second one. This could occur if, for instance, you want the body to be added using the WAC and then the title to be added at the approval stage. The steps can be followed to demonstrate the technique (reasonable prior experience of MCMS and the WAC is assumed).

  1. Create a Template Definition with two HtmlPlaceholder definitions called 'HtmlTitle' and 'HtmlBody'. Also create an OfficePlaceholderDefinition called 'OfficeHtmlHoldingContainer'. There's no need to put any restrictions on any of these placeholders as that will only complicate matters.
  2. Create a .ASPX template file and hook it up to the template definition created in (1).
  3. Add three HtmlPlaceholderControl controls to the template, and hook each up to one of the placeholder definitions. Ensure that the control hooked up to OfficeHtmlHoldingContainer has the Visible and EnableAuthoring properties both set to False.
  4. Create an empty class called AuthoringConnectorSupport.cs.
  5. Paste the following code into the new class created in (4):

      public AuthoringConnectorSupport() // just an empty constructor
      {
      }
      
      ///
      /// Swap the Office placeholder content into targetted placeholder, if valid
      ///
       public void SwapOfficeContentInIfValid()
      {
       Posting oPosting = CmsHttpContext.Current.Posting;   if(oPosting.Placeholders["OfficeHtmlHoldingContainer"] != null && oPosting.Placeholders["HtmlBody"] != null) // ensure basic required placeholders are present
       {
        if(oPosting.Placeholders["OfficeHtmlHoldingContainer"].Datasource.RawContent != "")
        {
         oPosting.Placeholders["HtmlBody"].Datasource.RawContent = oPosting.Placeholders["OfficeHtmlHoldingContainer"].Datasource.RawContent;
         oPosting.Placeholders["OfficeHtmlHoldingContainer"].Datasource.RawContent = "";     // uncomment this next line if you want the above to happen anyway, even if authoring is cancelled
         //CmsHttpContext.Current.CommitAll();
        }
       }
      }
  6. Add the following code into the Page_Load event of the template created in (2)

       // create an AC support object
       [insert namespace].AuthoringConnectorSupport oACSupport = new [insert namespace].AuthoringConnectorSupport();
       
       // ensure Office content is swapped in, if required
       oACSupport.SwapOfficeContentInIfValid();

  7. Set up a publishing task for the WAC in the PublishingTasks.xml file.
  8. Create a posting of this template type using the WAC.
  9. Approve the posting.

What should have happened is that at (9), the content which you entered in Word has been moved into the HtmlBody placeholder, which was the second HtmlPlaceholder.

This technique can be used to target any placeholder which will take textual content, such as a custom placeholder, and can be tweaked in the AuthoringConnectorSupport class if different properties are used for content.

An extension of this technique can be used to target multiple placeholders, although the solution is not particularly 'clean'. In the Word document 'tags' can be added that will be read by the AuthoringConnectorSupport class. These tags could be placeholder names, or just words that get picked up and checked in the class via a switch statement, for example. Macros could even be used in Word to insert these 'tags'. The AuthoringConnectorClass would merely scan through the incoming content for these tags, split the string up and then share it out across the placeholders as required.

 

 


Bookmark with :
Digg It! DZone StumbleUpon Technorati Reddit Del.icio.us Newsvine Furl Blinklist

I know that others have mentioned this one before, including Stefan, but if you find that your tree-view control isn't loading, it's likely that your website isn't running under the wwwroot default folder. This means that you probably don't have the webctrl_client folder under your web root folder. Copy it over from wwwroot and that should sort you out.

You might also need aspnet_client if you're using anything that utilises client-side ASP.NET generated scripting, such as validators.


Bookmark with :
Digg It! DZone StumbleUpon Technorati Reddit Del.icio.us Newsvine Furl Blinklist

Microsoft Content Management Server provides a fairly powerful revision history tool within the Web Author. It shows a list of versions, and can compare them for differences. However, MCMS doesn’t really help too much when trying to present older revisions to subscribers outside of the Web Author.

 

Typically, this situation could occur when a user might want to see what a page looked like a week, month or even a year ago. Whilst this situation would be rare in an Internet scenario, in an Intranet (or Extranet) scenario it would be more common. If an end-user had to contact one of the MCMS authors every time they needed to check the historical details of a page then it would rapidly become a fairly tedious and time-consuming task using the Web Author.

 

Ideally, there would be an extra flag that we could pass on the page QueryString to obtain a page version from a specific point in time. We could then show a list of revisions as hyperlinks and the end-user could click through to the version they wanted. Unfortunately, MCMS doesn’t provide that. Out-the-box, the pages you can ask for using URLs will give you either the published or (if present) unpublished version, and that’s the lot.

 

Thankfully, using the MCMS API and a little bit of trickery, there is a fairly simple way to get an older revision.

 

To get an old version of a current posting, the technique is to create a new posting based on the same template, and then copy across placeholder data as required. (I’ve tested this with HTML placeholders and it works fine. It may well work with image or attachment placeholders as well, although some tweaking may be required in the code.) The new posting can then be referenced by its ‘inner’ URL and you’re in business.

 

Here’s a step-by-step how to do it:

 

1)      Create a new .aspx in the root of the website called “RevisionHistory.aspx. (Note: if preferred, you can create it as a MCMS template and create a posting from it. That works perfectly well. You’ll just need to change the page reference in the code fragments below.)

2)      Create a new channel from your channel root called “_Historical”. Ensure that the properties of this channel are set to not crawlable and not indexable otherwise you might get some interesting search/navigation conundrums. Write access to this channel should be granted to everyone.

3)      In the Page_Load of RevisionHistory.aspx, put the following code (you’ll need to add a using directive for Microsoft.ContentManagement.Publishing, if it’s not on the page already):

 

if(Request.QueryString["COPYGUID"] != null) // check there's a GUID (won't be when creating posting if this is driven as a Posting from a Template)

{

  CmsApplicationContext oAppContext = new CmsApplicationContext(); // get an application context

  // the app context is required as the HTTP context will not (probably) be in Update mode

 

  // get current NT-authenticated user

  System.Security.Principal.WindowsIdentity ident = (System.Security.Principal.WindowsIdentity) HttpContext.Current.User.Identity;

 

  // use NT user to authenticate against site

  oAppContext.AuthenticateUsingUserHandle(ident.Token, PublishingMode.Update);

 

  // get handle to posting referred to by GUID

  Posting oPosting = ((Posting) oAppContext.Searches.GetByGuid(Request.QueryString["COPYGUID"])).RevisionForDate(new DateTime(Convert.ToInt64(Request.QueryString["REVISIONTICKS"])));

 

  // get Channel object for _Historical channel

  Channel chDest = (Channel) oAppContext.Searches.GetByPath("/Channels/_Historical");

 

 

  // following code ensures that all postings in the destination channel created more than two hours ago are cleaned out (may only be 1 hour during BST because of the way 'created date' is stored for a Posting)

  foreach(Posting oCheckPosting in chDest.Postings)

  {

    if(oCheckPosting.CreatedDate < DateTime.Now.Subtract(new TimeSpan(2, 0, 0)) && oCheckPosting.Name != "_HistoricalDefault") // leave default channel page, if there

    {

      oCheckPosting.Delete();

    }

  }

 

 

  // create posting in _Historical channel

  Posting oNewPosting = chDest.CreatePosting(oPosting.Template);

 

  // set page properties for new posting

  oNewPosting.Name = "TemporaryRevisionPosting";

  oNewPosting.DisplayName = "Temporary Revision Posting";

 

  // these properties stop the temporary page from being hyperlink-followed or indexed

  oNewPosting.IsRobotFollowable = false;

  oNewPosting.IsRobotIndexable = false;

 

  // loop through placeholders on new posting

  foreach(Placeholder oPlaceholder in oNewPosting.Placeholders)

  {

    string sName = oPlaceholder.Name;

 

    if(oPosting.Placeholders[sName] != null) // check to make sure placeholder exists on original page

    {

      oPlaceholder.Datasource.RawContent = " Note: This is a HISTORICAL version. Click here to see the CURRENT version." + oPosting.Placeholders[sName].Datasource.RawContent;

    }

  }

 

  // commit all content

  oAppContext.CommitAll();

 

  // navigate away to new posting 

  Response.Redirect(oNewPosting.UrlInner); // ensure posting is navigated by GUID as many pages with same name

}

 

4)      Create a user control called “RevisionLinks.ascx” or something similar

5)      Copy the following code into the Page_Load of the user control (you’ll also need to add a using directive for Microsoft.ContentManagement.Publishing, if it’s not on the control already):

 

Literal oHyperlinks = new Literal();

 

foreach (Posting oHistoricalPostingItem in CmsHttpContext.Current.Posting.Revisions(true, false)) // iterate through historical items

{

  oHyperlinks.Text = oHyperlinks.Text + "" + oHistoricalPostingItem.DisplayName + " on " + oHistoricalPostingItem.RevisionDate.ToString() + "
";

}

 

this.Controls.Add(oHyperlinks);

 

6)      Drag-and-drop the user control on any template where you want to be able to view and click-through to the older revisions.

7)      Try it out by viewing a posting that uses that template!

 

What should happen is that the user control renders out a list of hyperlinks of older versions. When clicked, those hyperlinks take the user to the RevisionHistory.aspx page, which creates a new posting based on the correct template and then copies across all placeholder data from the older revision to the new posting. The RevisionHistory.aspx page then redirects to the new posting. The redirect is a server redirect and so the user won’t even see it happen.

 

Note that the most recent revision in the history link will actually be the current version. Note also that there is a crucial limitation with this technique – the posting is shown based on the current version of the template. The out-the-box Web Author revision history suffers from exactly the same limitation. It shouldn’t be a problem though, as long as there’s a policy to create a new template whenever anything more than a cosmetic change is required, rather than editing and saving over the old one.

 

There’s some more trickery in the code, like on-the-fly cleaning out of old temporary postings, but the code is commented so it should make sense.

 


Bookmark with :
Digg It! DZone StumbleUpon Technorati Reddit Del.icio.us Newsvine Furl Blinklist