Saturday, May 28, 2011

Publishing pages auto save mechanism in Sharepoint when user leaves edit mode. Part 1

In this series of posts I would like to describe the internal mechanisms used by Sharepoint for auto saving publishing pages. Publishing pages are part of publishing infrastructure feature of Sharepoint (basic WCM feature) and content producers may add content on these pages on behalf of business needs. In order to add content on a page user should switch page to Edit mode (Site Actions > Edit Page). When all necessary changes are done user should save his changes by choosing ribbon Page > Check In, Save & Close or Publish.

If user forgot to save changes Sharepoint has useful feature which prevents loosing of content: publishing pages auto save. Most probably you saw javascript confirmation dialog when tried to leave page edit mode without saving, like this:

image

or this

image

If you familiar with onbeforeunload event handling in IE, you most probably know that this is standard IE javascript confirmation dialog which prevents user to leave the page. Header “Are you sure you want to navigate away from this page” and footer “Press OK to continue, or Cancel to stay on the current page” are standard texts shown by IE. But middle part text “The page could not be saved because your changes conflict with recent changes made by another user. If you continue, your changes will be lost” is custom message which can be set in your javascript handler. This functionality is not Sharepoint-specific, it is standard IE behavior. As we will see below Sharepoint utilizes this feature for auto save feature. Let’s look under the hood of its implementation.

Key element in auto save feature is Publishing Console control. In most cases it is added with feature into master page of the publishing site:

   1: <asp:ContentPlaceHolder ID="SPNavigation" runat="server">
   2:     <SharePoint:DelegateControl runat="server" ControlId="PublishingConsole" Id="PublishingConsoleDelegate"/>
   3: </asp:ContentPlaceHolder>

Control itself is located in "14\Template\ControlTemplates\PublishingConsole.ascx". Let’s look inside:

   1: <%@ Control Language="C#"   %>
   2: ...
   3: <SharePoint:UIVersionedContent id="publishingConsoleV4" UIVersion="4" runat="server">
   4:     <ContentTemplate>
   5:         <PublishingInt:PublishingRibbon id="publishingRibbon" runat="server" />
   6:     </ContentTemplate>
   7: </SharePoint:UIVersionedContent>
   8: <SharePoint:UIVersionedContent id="publishingConsoleV3" UIVersion="3" runat="server">
   9:     <ContentTemplate>
  10:         ...
  11:     </ContentTemplate>
  12: </SharePoint:UIVersionedContent>

As you can see it contains 2 different consoles for different UI versions:

  • UI version = 4 – this is for new UI with ribbons which is used by default in Sharepoint 2010;
  • UI version = 3 – this is for old style UI which used in Sharepoint 2007.

In Sharepoint 2010 you can use old style for UI – see my previous blog post: Use Sharepoint 2007 sites look and feel in Sharepoint 2010. In context of the current article it is important that there are 2 different versions of the Publishing Console. And auto save feature is implemented differently for them. For UI version = 4 it uses ribbon javascript API (SP.Ribbon.PageState.PageStateHandler), and for UI version = 3 SaveBeforeNavigationControl is used. In this part I will describe how auto save is implemented in UI version = 3 (Sharepoint 2007). In the next part I will show mechanism used for UI version = 4.

Publishing Console in Sharepoint 2007 (UI version = 3) looks like this:

image

As I told above basic element of our investigation for UI version = 3 is SaveBeforeNavigationControl class (there is also SaveBeforeNavigateHandler, but it is used for non-publishing pages, like Wiki pages. Probably I will also describe how it works in one of the next articles). This control implements ICallbackEventHandler interface. In order to fully understand the logic of the control I will briefly describe how ASP.Net uses this interface. It allows to use control as a target handler for the javascript calls. Common usage is the following:

  1. Place control on the page
  2. Call ClientScriptManager.GetCallbackEventReference method and pass reference to the control as first argument. It will return javascript method which can be called from client side without postbacks. Request will be sent to the server and control will handle it
  3. When javascript method (from step 2) is called on the client side ICallbackEventHandler.RaiseCallbackEvent method of the control is triggered. It is important to understand that this method is called on server side, although it was initiated from client side via javascript
  4. Then ICallbackEventHandler.GetCallbackResult method is triggered as part of the same methods calls chain on the server. You can return string with the status of callback processing. State of the control between RaiseCallbackEvent and GetCallbackResult method calls is preserved, i.e. they are called on the same object instance. So you can use e.g. class member variables in order to save state between the calls:

       1: public class MyControl : WebControl, ICallbackEventHandler
       2: {
       3:     private int i = 0;
       4:  
       5:     public void RaiseCallbackEvent(string eventArgument)
       6:     {
       7:         i = 1;
       8:     }
       9:  
      10:     public string GetCallbackResult()
      11:     {
      12:         return i.ToString();
      13:     }
      14: }

  5. Result of GetCallbackResult function is passed to the javascript handler which you can specify during the call to GetCallbackEventReference (see step 1)

       1: <script type="text/javascript">
       2:     function CallBackHandler(result) {
       3:         alert(result);
       4:     }
       5: </script>

Now when we saw how mechanism of callback handlers works in ASP.Net lets see how it is used inside SaveBeforeNavigationControl.RaiseCallbackEvent method is empty and all work is done in GetCallbackResult (for simplifying I removed all logging methods):

   1: string ICallbackEventHandler.GetCallbackResult()
   2: {
   3:     try
   4:     {
   5:         if ((ConsoleUtilities.FormContextMode == SPControlMode.Edit) &&
   6:             (WebPartManager.GetCurrentWebPartManager(this.Page).Personalization.Scope == PersonalizationScope.Shared))
   7:         {
   8:             this.Page.Validate();
   9:             if (!this.Page.IsValid)
  10:             {
  11:                 return SPHttpUtility.NoEncode(Resources.GetString("SaveBeforeNavigateValidationErrorEncounteredWarning"));
  12:             }
  13:             SPListItem item = SPContext.GetContext(HttpContext.Current).Item as SPListItem;
  14:             if (item != null)
  15:             {
  16:                 if (ConsoleContext.AuthoringItemVersion != ConsoleContext.CurrentItemVersion)
  17:                 {
  18:                     return SPHttpUtility.NoEncode(Resources.GetString("ErrorFileVersionConflict"));
  19:                 }
  20:                 string currentItemVersion = ConsoleContext.CurrentItemVersion;
  21:                 if (!string.IsNullOrEmpty(currentItemVersion))
  22:                 {
  23:                     try
  24:                     {
  25:                         currentItemVersion = 
  26:                             (int.Parse(currentItemVersion, CultureInfo.InvariantCulture) + 1).ToString(CultureInfo.InvariantCulture);
  27:                     }
  28:                     catch (FormatException)
  29:                     {
  30:                         currentItemVersion = string.Empty;
  31:                     }
  32:                     catch (OverflowException)
  33:                     {
  34:                         currentItemVersion = string.Empty;
  35:                     }
  36:                 }
  37:                 item.Properties["SBN_SaveSucceededField"] = currentItemVersion;
  38:                 item.Properties["SBN_SaveSucceededRequestDigest"] = HttpContext.Current.Request.Form.Get("__REQUESTDIGEST");
  39:                 item.Update();
  40:                 ConsoleContext.AuthoringItemVersion = ConsoleContext.CurrentItemVersion;
  41:             }
  42:             else
  43:             {
  44:                 // log
  45:             }
  46:         }
  47:         else
  48:         {
  49:             return "save succeeded";
  50:         }
  51:     }
  52:     catch (SPException exception)
  53:     {
  54:         return SPHttpUtility.NoEncode(Resources.GetFormattedString("ConsoleSaveErrorMessageWithException",
  55:             new object[] { exception.Message }));
  56:     }
  57:     return "save succeeded";
  58: }

It validates the page (e.g. checks that all required fields are specified) and checks that current version is still the same as was on the moment when page was switched to the edit mode. It is done in order to prevent overriding of the other users changes (optimistic lock). Here the list of exact error message (from Microsoft.SharePoint.Publishing.Intl.dll assembly) which may come from SaveBeforeNavigationControl (not only from GetCallbackResult method, but also from OnPreRender – see below):

Resource Key Value
SaveBeforeNavigateErrorEncounteredWarning The following error was encountered while attempting to save the page:
ErrorFileVersionConflict The page you are attempting to save has been modified by another user since you began editing. Choose one of the following options:
SaveBeforeNavigateNotCheckedOutWarning           To save your changes before continuing, click "OK". To continue without saving changes, click "Cancel".
SaveBeforeNavigateErrorEncounteredWarning The following error was encountered while attempting to save the page:
SaveBeforeNavigateUnknownErrorEncounteredWarning The page took too long to save. You can click "Cancel", and then try to save the page again. If you click "OK", you might lose unsaved data.
SaveBeforeNavigateCurrentlySavingStatus Saving Page Content...
ConsoleSaveErrorMessage This page contains content or formatting that is not valid. You can find more information in the affected sections.

Then it stores increased version number into item properties collection. But where the actual saving of the page content occurs? In order to answer this question we need to investigate second important part – SaveBeforeNavigateHandler.OnPreRender method. I don’t want to explain all details of these method – not all of them are important. Briefly it adds necessary javascript on the page. Exactly this method registers handler for the window.onbeforeunload event. Lets check most important part of javascript registered in the OnPreRender() method:

   1: window.onbeforeunload = cms_handleOnBeforeUnload;
   2:  
   3: function cms_handleOnBeforeUnload() {
   4:     if (g_bWarnBeforeLeave && browseris.ie6up) {
   5:         if (useSyncCallback) { MakeCallbacksSynchronous(); }
   6:         g_recentCallBackResult = "";
   7:         ShowContentSavingBusyMessage(true);
   8:         __theFormPostData = "";
   9:         _spSuppressFormOnSubmitWrapper = true;
  10:         try {
  11:             WebForm_OnSubmit();
  12:             event.returnValue = undefined;
  13:         }
  14:         finally {
  15:             _spSuppressFormOnSubmitWrapper = false;
  16:         }
  17:         WebForm_InitCallback();
  18:         WebForm_DoCallback('ctl00$SPNavigation$ctl01$publishingConsoleV3$sbn1', '', SBN_CallBackHandler, null, null, false);
  19:         if (!useSyncCallback) {
  20:             var count = 0;
  21:             while (g_recentCallBackResult == "" && count < 150) {
  22:                 count++;
  23:                 WaitForCallback(100);
  24:             }
  25:         } else {
  26:             ResetCallbackMode();
  27:         }
  28:         ShowContentSavingBusyMessage(false);
  29:         if (g_recentCallBackResult != "save succeeded") {
  30:             g_bWarnBeforeLeave = true;
  31:             if (g_recentCallBackResult != "") {
  32:                 var validationString = 'The following error was encountered while attempting to save the page:' + '  ' + g_recentCallBackResult;
  33:                 return validationString;
  34:             }
  35:             else {
  36:                 return 'The page took too long to save. You can click \u0022Cancel\u0022, and then try to save the page again. If you click \u0022OK\u0022, you might lose unsaved data.';
  37:             }
  38:         }
  39:         var saveComplete = document.forms['aspnetForm'].MSO_PageAlreadySaved;
  40:         if (saveComplete != null) {
  41:             saveComplete.value = "1";
  42:         }
  43:     }
  44:     g_bWarnBeforeLeave = false;
  45: }
  46:  
  47: var g_recentCallBackResult = "";
  48: function SBN_CallBackHandler(callBackResult) {
  49:     g_recentCallBackResult = callBackResult;
  50: }

As I already said it registers handler for the window.onbeforeunload event - cms_handleOnBeforeUnload. This method is executed when page is unloaded – e.g. when user leaves the edit mode without saving. There is a lot of code, but most of it – just handling of different browsers support level for XMLHttpRequest object (IE supports callback timeouts, so variable useSyncCallback = true in IE, but e.g. in FF it is false and instead of one call to the XMLHttpRequest with specified timeout in FF it will use loop with periodical calls to http://example.com/_vti_bin/PublishingService.asmx Wait() method – see WaitForCallback(100) method call above).

For us only 2 lines of code are important:

   1: WebForm_OnSubmit();
   2: ...
   3: WebForm_DoCallback('ctl00$SPNavigation$ctl01$publishingConsoleV3$sbn1', '', SBN_CallBackHandler, null, null, false);

First line (WebForm_OnSubmit()) is exactly the line which causes saving of the page content. You can try to override the cms_handleOnBeforeUnload with your realization and comment the first line – you will see that changes now are not saved when user leaves the edit mode. And second line – is call to SaveBeforeNavigationControl.GetCallbackResult() method. If you remember this method returns "save succeeded" string. So cms_handleOnBeforeUnload handler checks that if returned string is not "save succeeded" – method returns string with validation errors. And this message will be shown in the middle of the IE javascript window as we already saw above (http://msdn.microsoft.com/en-us/library/ms536907(v=vs.85).aspx).

That’s how auto saving works in Sharepoint 2007 and in Sharepoint 2010 with UI version = 3. In the next part I will describe how it works in Sharepoint 2010 with UI version = 4.

Use Sharepoint 2007 sites look and feel in Sharepoint 2010

During upgrade from Sharepoint 2007 to Sharepoint 2010 you may want to preserve look and feel of some of your sites in order to use previous UI version. Most of changes in Sharepoint 2010 were made in UI:

image

Sharepoint 2010 uses ribbons in UI and it makes its look and feel similar to other Office products. However in your business web application you may still want to have old UI used in Sharepoint 2007. This is quite easy to do. Lets create OTB Publishing Portal site on Sharepoint 2010. Initially it will have new ribbon-style design:

image

Now in order to revert UI look and feel to the Sharepoint 2007 style we need to run the following program:

   1: using (var site = new SPSite("http://example.com"))
   2: {
   3:     site.RootWeb.UIVersion = 3;
   4:     site.RootWeb.Update();
   5: }

After that the same site will look like this:

image

No ribbons, no new style – we are back to Sharepoint 2007 now. After playing with old UI lets return back to the modern version:

   1: using (var site = new SPSite("http://example.com"))
   2: {
   3:     site.RootWeb.UIVersion = 4;
   4:     site.RootWeb.Update();
   5: }

And our site again has new UI version with ribbons.

Sunday, May 8, 2011

Add Enterprise keywords field into custom content type

In this post I would like to describe how to add OTB Enterprise keywords field into custom content type declaratively. Enterprise keywords – is a standard managed metadata field which is binded to the standard term set in Term store: System > Keywords. It allows users to specify keywords in content metadata, e.g. users can apply some keywords to the document, to the page or to the list item. These keywords will be saved in the Keywords term store and will be available for other users for selection.

You can add Enterprise keywords to the content type via UI (this field is located under Enterprise keywords group). As you probably know when you need to add some managed metadata field to the custom content type declaratively you need to add 2 fields: one for taxonomy field itself and other is hidden field of Note type. In order to add Enterprise keywords into content type declaratively for provisioning you also need to add 2 fields: one is the keywords “TaxKeyword” (id = 23F27201-BEE3-471E-B2E7-B64FD8B7CA38) and another is hidden field “TaxKeywordTaxHTField” (id = 1390A86A-23DA-45F0-8EFE-EF36EDADFB39) for the keywords (both fields has the same predefined id on all Sharepoint installations), You need to use the following code:

   1: <ContentType
   2:     ID="0x..."
   3:     Name="MyContentType"
   4:     Description=""
   5:     Group="MyGroup"
   6:     Inherits="TRUE"
   7:     Version="0">
   8:   <FieldRefs>
   9:     ...
  10:     <FieldRef ID="{1390A86A-23DA-45F0-8EFE-EF36EDADFB39}" Name="TaxKeywordTaxHTField" DisplayName="TaxKeywordTaxHTField" />
  11:     <FieldRef ID="{23F27201-BEE3-471E-B2E7-B64FD8B7CA38}" Name="TaxKeyword" DisplayName="$Resources:osrvcore,field_KeywordsFieldName;" Required="FALSE" />
  12:   </FieldRefs>
  13: </ContentType>

With this code you will be able to use OTB Enterprise keywords field in your content types.

Sunday, May 1, 2011

Sharepoint variations guide. Part 2– propagation use cases

In the 1st part of this series I made overview of basic features and configuration settings of the variations in Sharepoint. In this part I will describe basic use cases and show how configuration settings affect them. As you remember from 1st part there is possibility to specify how content (pages and sites) will be propagated: manually or automatically (Site settings > Variations > Automatic Creation). However there are another settings which affects this behavior: using PowerShell you can disable automatic propagation of the pages (not sites), so setting “Automatic Creation” will be ignored for the pages, although sites will continue to follow it (see Manage automatic propagation of variation pages for details). So we have 2 options: manage variations propagation via UI (Site settings > Variations > Automatic Creation) and using PowerShell. What is the difference between e.g. manual propagation specified in UI and PowerShell? In this post I’m going to answer these and other questions.

As you also remember from 1st part there are several timer jobs related with variations. For convenience I will show them as well:

Job title Description

Variations Create Hierarchies Job Definition 

Creates a complete variations hierarchy by spawning all sites and pages from the source site hierarchy for all Variation labels.
Variations Create Page Job Definition Creates peer pages in variant sites.

Variations Create Site Job Definition

Creates variant sites when the Variations Automatic Creation setting is disabled.
Variations Propagate Page Job Definition Updates peer pages in variant sites.
Variations Propagate Site Job Definition Creates variant sites when the Variations Automatic Creation setting is enabled.

I will show when each of these jobs are used depending on what settings are configured for automatic propagation and how you propagate variations. As there are many parameters which affect variation usage I think that it will be more clear to show them in tables.

Lets start from creation of variations hierarchy. Go to Site settings > Variations labels and create new label. Check “Source Variation” option for new label. The create at least one variation target. All sites in variations hierarchy will use the same site template as site template of variation source (site template dropdown list is available only for source when “Source Variation” checkbox is checked. For variation targets it will be disabled). After you created all labels click “Create hierarchies” button. Variations hierarchy will be created by time job “Variations Create Hierarchies Job Definition” – you can force via Central Administration > Monitoring > Job Definitions > Variations Create Hierarchies Job Definition (for your web application) > Run now. After that variation hierarchy will be created and you can test pages and sites variations propagation with it.

At first lets check how sites propagation works, because it is more simple in sense that they don’t depend from PowerShell settings.

Sites:

Automatic Creation = On Automatic Creation = Off
In order create new site and propagate it on variation targets you need to perform the following steps:

1. Create publishing site under variation source (e.g. from OTB Site manager page)

2. As Automatic Creation = On, newly created site will be propagated automatically using timer job “Variations Propagate Site Job Definition”. You can force it if will go to Central Administration > Monitoring > Job Definitions > Variations Propagate Site Job Definition (for you web application) > Run now

3. As result new sub site will be propagated to all variation labels. I.e. all variation labels will now have the same sub site as variation source
When Automatic Creation = Off you need to do the following:

1. Create publishing site under variation source (e.g. from OTB Site manager page)

2. In Site manager select newly created site and from menu choose New > Variation site

3. New window will be opened when you can specify under what variation label you need to propagate this site. This is the difference between automatic propagation and manual propagation: in case of automatic propagation site is propagated to all variation labels, in case of manual propagation – only to one variation label. Of course you can propagate it manually to all labels one by one.

4. When variation is created you can force its creation from Central Administration > Monitoring > Job Definitions > Variations Create Site Job Definition (for you web application). I.e. other job is used in order to propagate sites when manual creation is selected.

Now lets check similar cases for pages. They are more complicated because both UI configuration settings and PowerShell settings affects their propagation. Also for pages we need to separately check how new pages are propagated and how updates of existing pages are copied to variation labels.

First of all disable automatic variation propagation by the following PowerShell script (see Manage automatic propagation of variation pages):

   1: $site = Get-SPSite "<SiteURL>"
   2: $folder = $site.RootWeb.Lists["Relationships List"].RootFolder
   3: $folder.Properties.Add("DisableAutomaticPropagation", $true)
   4: $folder.Update()
   5: $site.Close()

So now we are considering the case when Automatic propagation (PowerShell) = Off:

  Automatic Creation (UI) = On Automatic Creation (UI) = Off
New page propagation 1. Create new publishing page under variations source (Site Actions > Create Page) and publish it

2. In order to propagate this page to variation targets in ribbon Publish in Variations category you can choose one of the 2 options:

2.1. Create – it will propagate page to only one selected variation target. You can specify different URL for the variation page. Copied page will have Draft status initially. In order to force page propagation you need to use “Variations Create Page Job Definition”. If page already exists on all target sites it will add the following message in variations log:
”All variation sites of the current site already have a variation of this page. Cannot create a new page variation”

2.2. Update – it will copy the page to all variation labels. All copied pages will have the same URL as source page and will have Draft status initially. In order to force pages propagation you need to use “Variations Propagate Page Job Definition”
When you disable automatic variations propagation via PowerShell then Automatic Creation UI setting is ignored for pages. It only affects sites. So all behavior will remain the same as for Automatic Creation (UI) = On.
Updates of existing page propagation 1. Update content on variation source page (e.g. some rich html field) and publish the page

2. Choose in ribbon Publish > Variations > Update. It will propagate changes on all variation pages on target sites

3. In order to force pages propagation you need to use “Variations Propagate Page Job Definition”
The same as for new pages - Automatic Creation UI setting is ignored and all behavior will remain the same as for Automatic Creation (UI) = On.

Now lets enable automatic pages propagation in PowerShell and will make the same test:

   1: $site = Get-SPSite "<SiteURL>"
   2: $folder = $site.RootWeb.Lists["Relationships List"].RootFolder
   3: $folder.Properties.Remove("DisableAutomaticPropagation")
   4: $folder.Update()
   5: $site.Close()

Now Automatic propagation (PowerShell) = On:

  Automatic Creation (UI) = On Automatic Creation (UI) = Off
New page propagation 1. Create new publishing page under variations source (Site Actions > Create Page) and publish it

2. In order to propagate this page to variation targets in choose Publish > Variations > Update – it will copy the page to all variation labels. All copied pages will have the same URL as source page and will have Draft status initially. In order to force pages propagation you need to use “Variations Propagate Page Job Definition”
1. Create new publishing page under variations source (Site Actions > Create Page) and publish it

2. Now if you will try to propagate new page by forcing “Variations Propagate Page Job Definition” – it won\t propagate it. Instead it will show the following message in Variations log (Site settings > Variation logs):
”Variation Site and Page Auto-Spawn is off so no new pages will be created”

3. In order to propagate this page to variation targets in ribbon Publish in Variations category you can choose one of the 2 options:

3.1. Create – it will propagate page to only one selected variation target. You can specify different URL for the variation page. Copied page will have Draft status initially. In order to force page propagation you need to use “Variations Create Page Job Definition”

3.2. Update – it will copy the page to all variation labels. All copied pages will have the same URL as source page and will have Draft status initially. In order to force pages propagation you need to use “Variations Propagate Page Job Definition”
Updates of existing page propagation 1. Update content on variation source page (e.g. some rich html field) and publish the page

2. In order to propagate changes you don’t need to click any buttons in ribbon Publish > Variations. It will be automatically propagated by “Variations Propagate Page Job Definition”
For updates behavior is the same as for Automatic Creation (UI) = On.

That’s all what I wanted to write about variation use cases. Hope it will help you to understand the big picture and you will be able to use it in your every day work.