Tuesday, May 31, 2011

Get .Net assembly full name using built-in Windows features without Reflector

If you work with Sharepoint then most probably quite often you need to know assembly full name (e.g. for adding it to the Assembly directive on your application layout page). Until the recent time preferred way for me to investigate assembly full name was .Net Reflector: load assembly under question in it and check full name in the bottom of the window:

image

However as you probably know Red gate announced that starting from version 7.0 Reflector will be payable. They leave version 6.8 as last version available for free. However I was a bit surprising when didn’t find this feature in version 6.8:

image

May be I missed something but I didn’t find how to enable this feature in v.6.8. Or it was made intentionally in order to attract more users to the new version – I don’t know. And although I personally don’t like idea to make Reflector payable, I won’t claim to the Red gate. I just wanted to find a workaround for the missing functionality using built-in tools.

Of course you can go to the GAC (C:\Windows\assembly)  and check properties of the assembly:

image

Then combine full name from file name, version, culture and public key token. However this way is not very convenient. The better way is to copy full name at one operation.

It can be done with PowerShell – built-in script engine in Windows. Go to Start and type PowerShell in the “Search programs and files”. Then add one line of code:

   1: PS C:\> [System.Reflection.AssemblyName]::GetAssemblyName("MyAssembly.dll").FullName

It will print full name of the assembly into the console. Also you can use path to the GAC:

   1: PS C:\> [System.Reflection.AssemblyName]::GetAssemblyName("C:\Windows\assembly\GAC_MSIL\Microsoft.SharePoint.Publishing\14.0.0.0__71e9bce111e9429c\Microsoft.SharePoint.Publishing.dll").FullName
   2: Microsoft.SharePoint.Publishing, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c

May be this method is not so convenient as graphic UI tool, however it doesn’t require neither installation of external software nor development of custom tool. Hope it will be helpful. If you will find more easy ways to investigate assemblies full names it will be cool if you will share it in comments to this post as well.

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.