Prexens team dev' corner

How-to: Replace ‘Submit for approval’ custom quick access button

Goal of this post

The goal of this post is to guide you through the different steps for replacing the ‘Submit for Approval’ quick access button in a WCM environment.


Problematic overview

‘Submit for Approval’ is a native button which aims at firing a ‘Parallel Approval’ workflow when Publishing features are activated  in a WCM web site:

 

Native Approval button

 

When deploying a custom approval workflow, you’ll probably need to overwrite the logic of this button to start your custom workflow correctly.
You would also be able to customize the icon and the text of your custom button to achieve something like this:

 

Custom approval button

 

Write a custom class CustomSubmitForApproval

 

1. Set pre-requisites:

 

  • First, add those tworeferences : Microsoft.SharePoint, Microsoft.SharePoint.Publishing
  • Create a public sealed class that inherits from:
    Microsoft.SharePoint.Publishing.WebControls.EditingMenuActions.ConsoleAction
  • Add a constant used as argument of post back events:
private const string PostBackEventArg = "submitForApproval";

 

2. Add a constructor and customize your custom quick access button:

public CustomSubmitForApprovalButton()
            : base()
{
      this.DisplayText = "Custom Submit for Approval";
      this.ImageUrl = "/_layouts/images/workflows.gif";
}

 

3. Override NavigateUrl property and fire post back events:

public override string NavigateUrl
{
     get
     {
          return String.Format("javascript:{0}", Page.ClientScript.GetPostBackEventReference(
this, CustomSubmitForApprovalButton.PostBackEventArg));
     }
}

 

4. Override RaisePostBackEvent method, and implement your redirection logic

 

In our case, we force the page check in and we redirect toward check-in page:

public override void RaisePostBackEvent(string eventArgument)
{
    try
    {
        if (eventArgument == CustomSubmitForApprovalButton.PostBackEventArg)
        {

            SPFile myFile = SPContext.Current.File;

            // Auto checkin page before redirecting to 'Publish Major Version'
            if (myFile.CheckOutStatus != SPFile.SPCheckOutStatus.None)
            {
                myFile.CheckIn("Default comment", SPCheckinType.MinorCheckIn);
            }

            string redirectUrl = String.Format(
                    "{0}_layouts/Checkin.aspx?FileName={1}&Publish=true&List={2}&Source={3}",
                    SPContext.Current.Web.ServerRelativeUrl,
                    SPContext.Current.ListItemServerRelativeUrl,
                    Convert.ToString(SPContext.Current.List.ID),
                    Page.Request.Url.OriginalString);

            SPUtility.Redirect(redirectUrl, SPRedirectFlags.Default, HttpContext.Current);
        }

    }
    catch (Exception ex)
    {
        ShowError(ex, new ConsoleError(ex.Message));
    }
}

 

You can override some properties to control what is the minimum level of permission required to access the feature, and what are the states causing your button to appear in the page editing toolbar. In samples below, users have to be granted the permission of adding items and page has to be a publishing page waiting for approval.

 

 

5. Override SPBasePermissions UserRights property

public override SPBasePermissions UserRights
{
    get { return SPBasePermissions.AddListItems; }
}
6. Override AuthoringStates RequiredStates property
public override AuthoringStates RequiredStates
{
    get
    {
        return AuthoringStates.IsPublishingPageTrue |
            AuthoringStates.SaveConflictExistsFalse |
            AuthoringStates.IsMajorVersionFalse |
            AuthoringStates.IsApprovalWorkflowConfiguredTrue |
            AuthoringStates.InEditModeFalse |
            AuthoringStates.IsApprovalWorkflowRunningFalse;
    }
}


 

Overwrite CustomQuickAccess.xml file in master pages gallery


When your custom class is compiled and deployed on your SharePoint server, you will have to overwrite CustomQuickAccess.xml located into MOSS master pages gallery.


In a live scenario, we recommend to deploy this file through activation event of a custom feature. Simply make sure not to override an already existing file.
In our sample scenario, we are going to edit this file directly from SharePoint Designer :


1. Open CustomQuickAccess.xml from /_catalogs/masterpage/Editing Menu


 

2. Add a reference to our custom class (mind to adjust assembly reference with your own assembly qualified name):

<references>
 <reference TagPrefix="Prexens"
      assembly="Prexens.SharePoint.CustomQuickAccessButton, 
Version=1.0.0.0, Culture=neutral, PublicKeyToken=9f4da00116c38ec5"
namespace="Prexens.SharePoint.CustomQuickAccessButton" /> </references>

3. Add a ConsoleNode that override native qaPublish Node ID:


<structure>
 <ConsoleNode 
Sequence="65"
  ConfigMenu="Replace"
  Action="Prexens:CustomSubmitForApprovalButton"
  ID="CustomQuickAccessButton_SubmitForApproval"
  ChangedNodeID="qaPublish" />
</structure>

 

4. Add two ConsoleNode that remove native Approve and Decline buttons (those two feature should probably be inconsistent with your custom publishing workflow):

<ConsoleNode ConfigMenu="Delete" ChangedNodeID="qaWorkflowDecline" />
<ConsoleNode ConfigMenu="Delete" ChangedNodeID="qaWorkflowApprove" />


5. Save, publish, and approve this file in master pages gallery


How-to: insert a Data View Web Part in a WCM publication page

Goal of this post
This post guides you through the steps for inserting the famous DVWP, which is an exclusive part of SharePoint Designer, in a publishing page of a WCM site.

Problematic overview
As a reminder, when trying to open a publishing page with SharePoint Designer, the following message box appears:
 

Edit in browser

Because we can't open the page in SharePoint Designer, we cannot configure and add the DVWP…

Solution
SharePoint Designer contains a very handy feature for WCM scenarios: " Detach from Page Layout ". This will break the inheritance between the publishing content page and its page layout.

First, start by duplicating the publishing page:
 

Duplicate page


Right click on the copied page and select " Detach from Page Layout ": this will embed the layout in the content page.
We can now open the content page as a standard " collaborative " page, configure and insert our Data View Web Part:
 

Configure Data View Web Part


Once you've configured the Web Part, you can export it from the browser interface…
 

Export Web Part


… then re-import it in the SharePoint Web Part Gallery:
 

Upload to Web Part Gallery


You can now open the target content page again, and insert your custom DVWP directly in a Web Part zone.

Tip: we recommend you to keep the duplicated page in the library for further modifications!

Ultimate solution to get rid of "The Web site wants to run the following add-on: 'Name ActiveX Control'"

Download setup files (21.1 KB)
Download sources (926 KB)
 
Problematic overview
 
When you are developing internet or extranet website with SharePoint, a common issue you have to resolve is to fix the following message displayed to end-users running IE7 and later
 
The Web site wants to run the following add-on: 'Name ActiveX Control'
 
This message is caused by presence features of SharePoint requiring this ActiveX control to be installed on clients computer. Many solutions can be found on the Web. Even Microsoft gives some workarounds to get rid of this behavior. But to me, none of them are really reliable.
 
Let's study a bit solutions from Microsoft. First and second methods require doing something on clients computer... Hmmm, seems to be a bit tricky in case of an internet or extranet Web site!
 
Solution #3 is to duplicate and rename the JS file which contains the presence script, comment out some code and register this new version by specifying its name through the name attribute of a ScriptLink object. Great, it does the trick! But what if I want to be a good guy and do not load core.js for my branding website? Let's remind the solution for this tweak: I need to empty script link of any name attribute... a dog eating his tail. And what if I want to have presence features enabled when my users are Windows authenticated?
 
A true Best-of-Breed solution: Control Adapters
 
Control Adapter is a very handy solution when you want to update the renderings of WebControls you are not responsible for. In our case, the trick is to create a control adapter for Microsoft.SharePoint.WebControls.ScriptLink and change the rendering behavior when authentication method is not Windows based. If the user is anonymous or form-based authenticated, we load a light version of the presence JS script and we suppress core.js.
 
Let's talk about code
 
First, we create a class library project in Visual Studio, sign the assembly and create a class ScriptLinkAdapter inheriting from System.Web.UI.Adapters.ControlAdapter.
 
Then, we override the Render method and we paste the code below.
 
   1:  // Retrieve authentication type
   2:  string authenticationType = HttpContext.Current.User.Identity.AuthenticationType;
   3:   
   4:  // Replace output with a minimal output if authentication is anonymous or forms-based
   5:  if (!HttpContext.Current.Request.IsAuthenticated ||
   6:      String.IsNullOrEmpty(authenticationType) ||
   7:      authenticationType.Equals("Forms", StringComparison.InvariantCultureIgnoreCase))
   8:  {
   9:   
  10:      // Load the ouputted HTML in memory
  11:      using (MemoryStream memoryStream = new MemoryStream())
  12:      using (StreamWriter streamWriter = new StreamWriter(memoryStream))
  13:      {
  14:          using (HtmlTextWriter htmlTextWriter = new HtmlTextWriter(streamWriter))
  15:          {
  16:              Control.GetType().InvokeMember(
  17:                  "RenderControl",
  18:                  BindingFlags.InvokeMethod 
  19:                  | BindingFlags.Instance 
  20:                  | BindingFlags.NonPublic,
  21:                  null,
  22:                  Control,
  23:                  new object[] { htmlTextWriter, null });
  24:          }
  25:   
  26:          // Flush the writer and rewind the stream
  27:          streamWriter.Flush();
  28:          memoryStream.Position = 0;
  29:   
  30:          // Read the stream as a string
  31:          using (StreamReader streamReader = new StreamReader(memoryStream))
  32:          {
  33:              string output = streamReader.ReadToEnd();
  34:   
  35:              // Remove core.js
  36:              string corePattern = @"\<script.+src\=.+core\.js.+\<\/script\>";
  37:   
  38:              output = Regex.Replace(output, corePattern, "", RegexOptions.IgnoreCase);
  39:   
  40:              // Replace init.js by a version excluding presence script
  41:              string initPattern = @"(\<script.+src\=\"")(init\.js)(\"".+\<\/script\>)";
  42:   
  43:              if (Regex.IsMatch(output, initPattern))
  44:              {
  45:                  output = Regex.Replace(output,
  46:                      initPattern,
  47:                      String.Format("$1{0}$3", Page.ClientScript.GetWebResourceUrl(
  48:                              typeof(ScriptLinkAdapter),
  49:                              "ScriptLinkAdapter.ClientResources.SPInitNoPresence.js")),
  50:                      RegexOptions.IgnoreCase);
  51:              }
  52:   
  53:              // Flush new content to response
  54:              writer.Write(output);
  55:          }
  56:      }
  57:   
  58:      // Registrations are done, so exit.
  59:      return;
  60:  }
  61:   
  62:  // Standard control rendering
  63:  Control.GetType().InvokeMember(
  64:      "RenderControl",
  65:      BindingFlags.InvokeMethod 
  66:      | BindingFlags.Instance 
  67:      | BindingFlags.NonPublic,
  68:      null,
  69:      Control,
  70:      new object[] { writer, null });
 
First, we retrieve the authentication type to apply transformation only for anonymous and forms authenticated users (line 1 to 8)
 
A key thing to know about control adapters is how to render the adapted control in standard way if no transformation has to be applied. A common mistake is to make a call to the RenderControl of the adapted control. If you do so you will enter an infinite loop: all controls are designed to look for an adapter before rendering themselves, if an adapter is found then the adapter is used and if the adapter call then render method again, the control will look for an adapter which calls the render... the dog and his tail again!

The solution is to use reflection to call a protected version of RenderControl that take a ControlAdapter as an input parameter. If we pass null, no adapter is assumed and the normal rendering cycle is called (line 14 to 24).
 
We are now able to get the original HTML content produced when rendering a ScriptLink control normally. We load this content into a MemoryStream, make some changes on it to suppress core.js and replace the old version of init.js and we flush this new content to the HtmlTextWriter attached to the Response object (line 33 to 54).
 
How to deploy
  1. Register assembly Prexens.SharePoint.WebControls.Adapters.ScriptLink.dll to the GAC of your SharePoint server
  2. Copy file Prexens.SharePoint.WebControls.Adapters.ScriptLink.browser to the folder App_Browsers of the application folder of your Web application (typically C:\Inetpub\wwwroot\wss\VirtualDirectories\<Your App Pool>)
  3. IMPORTANT: open the file App_Browsers\compat.browser in Notepad and save it (no need to change anything). This will force reloading the browser files. If you have multiple browser files already present you might need to repeat for each one.
  4. Restart IIS services (iisreset.exe in a command prompt)

Categories
Links
  Prexens Web Site
  Charting for SharePoint
  Charting for SharePoint (Codeplex)
  Charting for SharePoint Starter kit

Microsoft Certified Partner