Why are my publishing pages saying I have so many spelling errors?


In my publishing site, I have a basic custom content type that is based on the SharePoint out the box Page Content type. I have created a custom page layout. My layout contains a PublishingRollupImage, PublishingPageContent, and I’ve created a section on my page layout that only shows when you edit the page which contains PublishingContact. The publishing contact is mainly for search.

When my page is in edit mode, the screen looks like the following:

So I add a picture and some content to the page, and then go to publish it. The Page Content has been double checked and there is no spelling mistakes in there. However when I click publish, I get a pop-up box saying there are 73 spelling error(s) found. How is this possible? There isn’t even 73 words on the page.

The clue why this is happening is actually shown on the page. Where I have Page Owner at the bottom of the page, it clearly states “Spelling Errors Found.” But why? It’s just my name. If you click on the red text, it brings up a pop up, showing you what the spelling mistakes are.

What is actually happening is the user control HTML is being read. The user control has <textarea> which is hidden by css. This textarea is used by SharePoint to correctly display the User on the page, with the link to the user profile information. It is this text that is being spell checked.

There is a simple fix that you can add to your page layouts (assuming your page has jQuery already loaded). The textarea needs an attribute added to it called “excludeFromSpellCheck” which makes the SharePoint spell checker ignore the text within it. On your page layout page, find the ContentPlaceHolderID=”PlaceHolderMain” and right at the bottom of this section add a PublishingWebControls:EditModePanel with the following Javascript inside.

<PublishingWebControls:EditModePanel ID="SpellCheckEditModePanel" runat="server">
<script type="text/javascript">
jQuery(".ms-usereditor textarea").attr("excludeFromSpellCheck", "true");
</script>
</PublishingWebControls:EditModePanel>

The reason why we put it inside an EditModePanel is because the spell checking is only carried out when the page is in edit mode, so there is no need to run the JavaScript in display mode.

As you can see after implementing the jQuery change, we no longer get any spelling errors found.

My Feature Stapler doesn’t work, what to do?


Problem 1

You’ve been asked to add a new navigation link, remove a column from a list or maybe modify a webpart on a page to every team site within your farm, but there are over 200 of them, and you’ve got to ensure that any future team sites created will also incorporate these new changes. A Stapler won’t work for existing sites, therefore a PowerShell script / Console App will need to be run to fix existing sites, then create a stapler feature for all new sites, then you might encounter the next problem.

Problem 2

You are creating a new site and added your code as a feature stapler, but it throws an error when your site is being created. Why? It turns out that your feature is trying to access something like SharePoint Navigation or a list, before it has been created in the site. With feature staplers you cannot always guarantee the order they will be stapled to a site and activated. Therefore you get the error.

Solution

We need a way to ensure our feature code is run once the site is fully created. So the solution is to use a Control Template and a delegate control, with a self-deactivating feature so that it isn’t run multiple time.

Simply what will happen is there will be a delegate control sitting on the page, when the first user hits the page, it will run the code within the Page_Load method. This will add the Navigation Link, List, change content type of all Items etc, and then once run it will deactivate the feature containing the delegate control to prevent it running again. Everything is now in place, the first user might get 1 redirect and slower loading time, while processing, but if they hit the page again or any following user hits the page everything will be in place and load up at normal SharePoint speed.

Basic layout of project shown above

The following steps will show you the basic setup.

In your SharePoint project, create a Control Template, I’ve called my Initialiser.ascx. This control template on load performs the main guts of what you want your Stapler to do.

We have a FeatureGuid as a public property. This is to pass in the FeatureID of our delegate control that holds this Control Template in. We will build that bit later.

using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using Microsoft.SharePoint;
namespace CFSP.FeatureStaplerWithControlTemplate.CONTROLTEMPLATES
{
    public partial class Initialiser : UserControl
    {
        public string FeatureGuid { get; set; }
        protected void Page_Load(object sender, EventArgs e)
        {
            bool success = false;
            var currentWebUrl = SPContext.Current.Web.Url;
            SPSecurity.RunWithElevatedPrivileges(delegate()
            {
                using (SPSite site = new SPSite(currentWebUrl))
                {
                    using (SPWeb web = site.OpenWeb())
                    {
                        try
                        {
                            web.AllowUnsafeUpdates = true;
                            if (SomeCodeToPerform())
                            {
                             //Deactivate the feature that this control is linked to so that this code will not run again.
                            bool hasFeature = false;</span>
                             foreach (SPFeature feature in web.Features)
                             {
                              if (feature.DefinitionId.ToString().Equals(FeatureGuid, StringComparison.CurrentCulture))
                              {
                                hasFeature = true;
                                success = true;
                                break;
                              }
                             }

                             if (hasFeature)
                             {
                                 web.Features.Remove(new Guid(FeatureGuid));
                             }
                            }
                        }
                        catch (Exception ex)
                        {
                            //Log message in ULS.
                        }
                        finally
                        {
                            web.AllowUnsafeUpdates = false;
                        }
                    }
                }
            });

 //If code is affecting landing page, if success refresh the page. This only happens for the first user who hits this page.
 //Don't refresh if the success is false otherwise you will encounter endless loop!</span>
 if(success)
     Page.Response.Redirect(currentWebUrl, false);
}

private bool SomeCodeToPerform()
{
 /*A function that will perform something that might not work or cannot work in a feature stapler due to timing.
  * For Example:
  * Adding Navigation Item to List
  * Remove a Field from a content type
  * Add a list
  * Change a content type for all items in a list
  * etc
  */

 return true;
}
}
}

If you read through the code above, you’ll see that I’m running with elevated Privileges this is because we don’t know what permissions the first user who hits the page will have. They might be a site owner, but they most likely will be a site visitor or contributor. They wouldn’t have access to create, change or deactivate the feature. Once the changes have been made to the site, it looks for the Feature GUID and deactivates the feature. This feature GUID is itself, and by removing it, it will prevent any following users from running it on this site.

Next we will need to create a DelegateControl, add an empty element and in the Elements.xml file. We also need to give a FeatureGuid property. We will fill in this information in the next step.

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <Control Id="AdditionalPageHead"
           Sequence="100"
           ControlSrc="~/_controltemplates/CFSP/Initialiser.ascx">
    <Property Name="FeatureGuid">  TODO  </Property>
  </Control>
</Elements>

Add a web feature to the project. Make the feature hidden (to ensure no user can enable or disable it in manage features), and add the DelegateControl from the last step to it. In the property window of this feature find the Feature Id and copy it to the FeatureGuid of the DelegateControl property,

    <Property Name="FeatureGuid">ac5887fe-926f-426f-9c77-007d83f0b521</Property>
 

If you are stapling to an existing template of SharePoint such as mysite, blogs, teamsite etc follow the next step few steps to ensure you staple the delegate control to your template. If you have a custom WebTemplate then add the delegate control feature to your WebTemplate onet.xml file and you ready to skip to the powershell section.

Add another empty element to the project and in the Elements.xml file add a FeatureSiteTemplateAssociation. In my example I’m adding this to the blog template. Remember the ID is the Feature ID of the delegate control.

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <FeatureSiteTemplateAssociation Id="ac5887fe-926f-426f-9c77-007d83f0b521" TemplateName="BLOG#0" />
</Elements>

Add a WebApplication feature to the project, and add the stapler from the last step to it.

If you deploy this solution, you will find (in my example) that every new blog site will run my code, because of the stapler. However all current existing sites don’t. This is because the stapler only runs on newly created sites, and the delegate control feature hasn’t been activated on any existing site. To complete this solution we need to create a powershell script that will activate the delegate control feature for all existing blog sites. (Remember when the feature is activated, all it does is adds the delegate control. No code to change the site is made until a person hits the page).

PowerShell

$rootSite = New-Object Microsoft.Sharepoint.SPSite "http://cfsp/"
foreach ($site in $rootSite.WebApplication.Sites) {
foreach ($web in $site.AllWebs) {
if ($web.WebTemplate -ne "BLOG"){continue}
          EnableFeature "FeatureStaplerWithControlTemplate_Web - Initialiser" $web.Url
}
}

The full solution code can be found on my skydrive

System.Security.Cryptography.CryptographicException occurred in process…


On my SharePoint development machine where I have Visual Studio each morning when I get into work because my PC is left running overnight, I get the following Visual Studio Just-In-Time Debugger appearing on my screen.

This happens because every night SharePoint 2010 recycles the OWSTIMER.EXE process every night. This is to ensure that the timer service doesn’t run into any memory problems.

Recycling the OWSTIMER process is controlled by a service timer job called “Timer Service Recycle” which runs once a day. Mine runs at 6AM.

If you reset the SharePoint Timer Service manually you can make the Visual Studio Just-In-Time Debugger pop up.

If you find this pop up annoying each day then you can disable it.

Disabling in Visual Studio.

According to Microsoft http://msdn.microsoft.com/en-us/library/5hs4b7a6(v=vs.110).aspx you can disable the Just-In-Time debugger in Visual Studio. I attempted this but it didn’t seem to work. (Maybe I needed to reboot the PC). I’m putting the instructions here just for completeness.

  • Open Visual Studio as Administrator, on the Tools menu, click Options
  • In the Options dialog box, select the Debugging folder.
  • In the Debugging folder, select the Just-In-Time page.
  • In the Enable Just-In-Time debugging of these types of code box, clear the relevant program types: Managed, Native, or Script.

Disabling using Registry

I found this method worked for me.

  • Open up the registry edit. In run type regedit.exe
  • In the Registry Editor window, locate and delete the following registry keys:
    • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug\Debugger
    • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\DbgManagedDebugger
    • HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\AeDebug\Debugger
    • HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\.NETFramework\DbgManagedDebugger
  • Close the Registry Editor window.

Being a developer machine, you might actually want the JIT debugger available, if that is the case then I’d recommend putting up with the pop up once a day.

Creating Nintex Workflow Custom Actions


As promised in my previous blog, I am going to explain how to create your own custom action for Nintex Workflow.

(New Post Nintex Custom Action – Error Handling)

(Update: I have got this working in a 2013 SharePoint On Premise project, the C# code will indicate that some methods are deprecated now, but this is OK as Nintex Workflow still runs using the SharePoint 2010 Workflow engine, not the new 2013).

Nintex is a great product to create Workflows with within SharePoint. Much more flexible than SharePoint Designer, and far less complicated than using Visual Studio. There is an extensive list of Actions already for Nintex Workflow sometimes you need a Custom Action that is specific for your business such as bring back data from your bespoke application, or UNIX text file. This blog will explain the different parts of creating a custom action to you.

Above picture showing you collection of Actions that comes with Nintex Workflow.

To build a custom action, unfortunately, there is a collection of files you are require to create, even though there is only one real section that performs the actual business logic. The rest of it is all supporting code. Below is a basic project setup of all the files you need. I will explain each section throughout this post. With a walkthrough at the end of how to create a ReadFromPropertyBag Custom Action.

References

Before we can even start we require the following References.

  • System.Workflow.Activities.dll (Not shown in above picture. Forgot to add before took picture.)
  • Microsoft.SharePoint.dll
  • Microsoft.SharePoint.WorkflowActions.dll
  • System.Workflow.ComponentModel.dll
  • Nintex.Workflow.dll – Can be found at C:\Program Files\Nintex\Nintex Workflow 2010\Binaries.
  • Nintex.Workflow.ServerControls.dll – Can be found at C:\Program Files\Nintex\Nintex Workflow 2010\Binaries. (Not shown in above picture. Forgot to add before took picture.)

For SharePoint 2013, you will need to get the Nintex dll’s from the GAC. To do this you will need to browse to c:\Windows\Microsoft.NET\assembly\GAC_MSIL\ and find the Nintex DLL’s there.

Features

A WebApplication feature that when activated it will add the Custom Action to the Web Application and authorize it to be used within the web application.

CustomActions –ActionName – NWAFile

An element file, which holds an XML file which is all the required details for a NintexWorkflowActivity. It is this file that is read in by the Feature receiver to be able to add the custom action to the web application.

CustomActions – ActionName – ActionNameActivity

A class that is inherited by Nintex.Workflow.Activities.ProgressTrackingActivity. This file contains all the Dependency properties to the activity. The Dependency Properties are object properties that can be bound to other elements of a workflow, such as workflow variables or dependency properties of other activities. They are used to store the data that the activity will require or to output data from the activity. This is also the file that contains the execution of the actual business logic.

CustomActions – ActionName – ActionNameAdapter

A class that is inherited by Nintex.Workflow.Activities.Adapters.GenericRenderingAction. You will need to implement the abstract class of GenericRenderingAction. These implementations

  • GetDefaultConfig() – define the parameters that the user can configure for this action and sets the default label for the action.
  • ValidateConfig() – Adds logic to validate the configuration here, and will display any error messages to the user if there is an issue.
  • AddActivityToWorkflow() – Creates an instance of the Activity and set its properties based on config. Then it adds it to the parent activity.
  • GetConfig() – Reads the property from the context.Activity and update the values in the NWActionConfig.
  • BuildSummary() – Constructs an Action Summary class to display details about this action.

I find the Adapter is very similar for every Action. Once you have the basic of one, just by adding an extra parameter or removing one you can quickly put an adapter together for any Custom Action.

Layouts – NintexWorkflow – CustomActions – ActionName – Images

I have two icon .png files in here. One is sized at 49×49 pixels and the other at 30×30 pixels. These files are referenced in the NWAFile, and used to display the custom action to the user in the toolbox area (30×30), or in the actual workflow itself (49×49).

You could add a third one here for Warning Icon. This is where the custom action isn’t configured. This would be a 49×49 pixel too.

Layouts – NintexWorkflow – CustomActions – ActionName – ActionNameDialog

An application page inherited from Nintex.Workflow.ServerControls.NintexLayoutBase. The dialog is what appears to the user when they have to configure the custom action through the browser. Here there is no code behind. You mainly display the controls in the aspx and set up two javascript functions to read in and read out the configuration on load and save.

Walkthrough creating ReadFromPropertyBag Custom Action.

Now that you understand the basic roles of all the files required to make one custom action I will walk through creating a custom action that will read from the current SPWeb property bag. The user will pass in the “Property name” to obtain the value.

If you create a Solution with the similar layout my solution layout above, replacing “ActionName” with ReadFromPropertyBag. Your solution and file layouts should look similar to below.

ReadFromPropertyBagActivity.cs

Starting with the ReadFromPropertyBagActivity file. This inherits Nintex.Workflow.Activities.ProgressTrackingActivity. We will first add all the public static DependencyProperties. The default ones are __ListItem, __Context and __ListId. Then we will add 2 of our own, One to hold the the Property Name and the ResultOuput. You can delete the designer.cs file.

Each DependencyProperty will have its own public property.

using System;
using System.Workflow.ComponentModel;
using Microsoft.SharePoint.Workflow;
using Microsoft.SharePoint.WorkflowActions;
using Nintex.Workflow;
using Microsoft.SharePoint;
namespace CFSP.CustomActions.ReadFromPropertyBag
{
    public class ReadFromPropertyBagActivity : Nintex.Workflow.Activities.ProgressTrackingActivity
    {
        public static DependencyProperty __ListItemProperty = DependencyProperty.Register(&amp;quot;__ListItem&amp;quot;, typeof (SPItemKey), typeof (ReadFromPropertyBagActivity));
        public static DependencyProperty __ContextProperty = DependencyProperty.Register(&amp;quot;__Context&amp;quot;, typeof (WorkflowContext), typeof (ReadFromPropertyBagActivity));

        public static DependencyProperty __ListIdProperty = DependencyProperty.Register(&amp;quot;__ListId&amp;quot;, typeof (string),typeof (ReadFromPropertyBagActivity));
        public static DependencyProperty PropertyProperty = DependencyProperty.Register(&amp;quot;Property&amp;quot;, typeof (string), typeof (ReadFromPropertyBagActivity));
        public static DependencyProperty ResultOutputProperty = DependencyProperty.Register(&amp;quot;ResultOutput&amp;quot;, typeof (string), typeof (ReadFromPropertyBagActivity));

        public WorkflowContext __Context
        {
            get { return (WorkflowContext) base.GetValue(__ContextProperty); }
            set { base.SetValue(__ContextProperty, value); }
        }

        public SPItemKey __ListItem
        {
            get { return (SPItemKey) base.GetValue(__ListItemProperty); }
            set { base.SetValue(__ListItemProperty, value); }
        }

        public string __ListId
        {
            get { return (string) base.GetValue(__ListIdProperty); }
            set { base.SetValue(__ListIdProperty, value);}
        }

        public string Property
        {
            get { return (string) base.GetValue(PropertyProperty); }
            set { base.SetValue(PropertyProperty, value);}
        }

        public string ResultOutput
        {
            get { return (string) base.GetValue(ResultOutputProperty); }
            set {base.SetValue(ResultOutputProperty, value);}
        }

        public ReadFromPropertyBagActivity()
        {
        }

       protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
        {
        }
        protected override ActivityExecutionStatus HandleFault(ActivityExecutionContext executionContext, Exception exception)
        {
        }
    }
}

Now we will need to override the Execute() method. This is the main business logic of your Custom Action.

   protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
        {
            //Standard Nintex code to obtain context.
            ActivityActivationReference.IsAllowed(this, __Context.Web);
            NWWorkflowContext ctx = NWWorkflowContext.GetContext(
               this.__Context,
               new Guid(this.__ListId),
               this.__ListItem.Id,
               this.WorkflowInstanceId,
               this);

            base.LogProgressStart(ctx);
            //Get the property value.
            string resolvedProperty = ctx.AddContextDataToString(this.Property);

            var result = "";

            //Using the context get the property if it exists.
            if (ctx.Web.AllProperties.ContainsKey(resolvedProperty))
            {
                result = ctx.Web.AllProperties[resolvedProperty].ToString();
            }
            //store the result.
            this.ResultOutput = result;

            //End Execution.
            base.LogProgressEnd(ctx, executionContext);
            return ActivityExecutionStatus.Closed;
       }

The last thing we need to do in this class is to handle if there is a fault during execution. Overwrite the HandleFault code with the following. You can make the error say whatever you like. I’m just referencing the item that called the workflow.

protected override ActivityExecutionStatus HandleFault(ActivityExecutionContext executionContext, Exception exception)
        {
            Nintex.Workflow.Diagnostics.ActivityErrorHandler.HandleFault(executionContext, exception,
                this.WorkflowInstanceId, "Error Reading from Property Bag";, __ListItem.Id, __ListId, __Context);
            return base.HandleFault(executionContext, exception);
        }

ReadFromPropertyBagAdapter.cs

Moving onto the Adapter file now. This class inherits the Nintex.Workflow.Activies.Adapters.GenericRenderingAction and needs to implement 5 overrides. I have also included two private constants strings. These are the property names we declared in the Activity class. Ensure these names match, or you will encounter errors later which takes a while to debug.

using System;
using System.Collections.Generic;
using System.Workflow.ComponentModel;
using Microsoft.SharePoint;
using Nintex.Workflow;
using Nintex.Workflow.Activities.Adapters;

namespace CFSP.CustomActions.ReadFromPropertyBag
{
    public class ReadFromPropertyBagAdapter : GenericRenderingAction
    {
       //Values should match the property names in the ReadFromPropertyBagActivity class.
        private const string PropertyProperty = "Property";
        private const string ResultOutputProperty = "ResultOutput";

       public override NWActionConfig GetDefaultConfig(GetDefaultConfigContext context)
        {
            throw new NotImplementedException();
        }

        public override bool ValidateConfig(ActivityContext context)
        {
            throw new NotImplementedException();
        }
        public override CompositeActivity AddActivityToWorkflow(PublishContext context)
        {
            throw new NotImplementedException();
        }

        public override NWActionConfig GetConfig(RetrieveConfigContext context)
        {
            throw new NotImplementedException();
        }

        public override ActionSummary BuildSummary(ActivityContext context)
        {
            throw new NotImplementedException();
        }
   }
}

I will explain each override before showing you the code.

GetDefaultConfig sections allows you to set up the parameters for user input and outputs. If you wish the user to freely type a value use a PrimitiveValue. If you would like the user to use a predefined value that would be a variable somewhere in the workflow then use NWWorkflowVariable value. Typically the output would always be written back to a Workflow Variable so this will be a Variable type of NWWorkflowVariable. Add an ActivityParameter for each property.

public override NWActionConfig GetDefaultConfig(GetDefaultConfigContext context)
       {
            NWActionConfig config = new NWActionConfig(this);
            //define the number of parameters one for each custom parameter.
            config.Parameters = new ActivityParameter[2];
            //define the parameters that the user can configure for this action.
            config.Parameters[0] = new ActivityParameter();
            config.Parameters[0].Name = PropertyProperty;
            config.Parameters[0].PrimitiveValue = new PrimitiveValue();
            config.Parameters[0].PrimitiveValue.Value = string.Empty;
            config.Parameters[0].PrimitiveValue.ValueType = SPFieldType.Text.ToString();

            config.Parameters[1] = new ActivityParameter();
            config.Parameters[1].Name = ResultOutputProperty;
            config.Parameters[1].Variable = new NWWorkflowVariable();

            //set the default label for the action.
            config.TLabel = ActivityReferenceCollection.FindByAdapter(this).Name;
            return config;
       }

ValidateConfig section allows you to validate the values entered. Here I’m just ensuring the value are not blank. You would add a validation for each input property.

public override bool ValidateConfig(ActivityContext context)
{
            //Add logic to validate the configuration here.
            bool isValid = true;
            Dictionary<string, ActivityParameterHelper> parameters = context.Configuration.GetParameterHelpers();
            if (!parameters[PropertyProperty].Validate(typeof(string), context))
            {
                isValid &= false;
                validationSummary.AddError("Property Bag", ValidationSummaryErrorType.CannotBeBlank);
            }
            return isValid;
}

Validation is shown in image below.

AddActivityToWorkflow creates an instance of the Activity and set its properties based on config. You also bind the default properties. Assign each parameter you have here. Lastly attach the Activity Flags. Then add it all to the parent activity.

public override CompositeActivity AddActivityToWorkflow(PublishContext context)
        {
            Dictionary<string, ActivityParameterHelper> parameters = context.Config.GetParameterHelpers();
            ReadFromPropertyBagActivity activity = new ReadFromPropertyBagActivity();

            parameters[PropertyProperty].AssignTo(activity, ReadFromPropertyBagActivity.PropertyProperty, context);
            parameters[ResultOutputProperty].AssignTo(activity, ReadFromPropertyBagActivity.ResultOutputProperty, context);
            activity.SetBinding(ReadFromPropertyBagActivity.__ContextProperty, new ActivityBind(context.ParentWorkflow.Name, StandardWorkflowDataItems.__context));
            activity.SetBinding(ReadFromPropertyBagActivity.__ListItemProperty, new ActivityBind(context.ParentWorkflow.Name, StandardWorkflowDataItems.__item));

            activity.SetBinding(ReadFromPropertyBagActivity.__ListIdProperty, new ActivityBind(context.ParentWorkflow.Name, StandardWorkflowDataItems.__list));

            ActivityFlags f = new ActivityFlags();
            f.AddLabelsFromConfig(context);
            f.AssignTo(activity);

            context.ParentActivity.Activities.Add(activity);
            return null;
        }

GetConfig reads the properties from the context.Activity and updates the values in the NWActionConfig. Add a new parameter for each property. You can see when we RetrieveValue from our activity, we are grabbing the corresponding DependencyProperty from our activity.

public override NWActionConfig GetConfig(RetrieveConfigContext context)
        {
            //Read the properties from the context.ACtivity and update the values in the NWActionConfig

            NWActionConfig config = this.GetDefaultConfig(context);
            Dictionary<string, ActivityParameterHelper> parameters = config.GetParameterHelpers();
            parameters[PropertyProperty].RetrieveValue(context.Activity, ReadFromPropertyBagActivity.PropertyProperty, context);
            parameters[ResultOutputProperty].RetrieveValue(context.Activity, ReadFromPropertyBagActivity.ResultOutputProperty, context);

            return config;

        }

BuildSummary is the last implemented override method. The code here writes out the summary displayed to the user after that have configured the action and hovered the mouse over the custom action.

public override ActionSummary BuildSummary(ActivityContext context)
        {
           // Construct an ActionSummary class to display details about this action.

            Dictionary<string, ActivityParameterHelper> parameters = context.Configuration.GetParameterHelpers();
            return new ActionSummary("Retrieve the following Property bag: {0}", parameters[PropertyProperty].Value);

        }

BuildSummary is displayed below on mouse hover once item has been configured.

ReadFromPropertyBagDialog.aspx

The code behind for this aspx file inherits from Nintex.Workflow.ServerControls.NintexLayoutsBase. Apart from changing the inheriting type, there is no need to do anything else in the .cs file. In the aspx file we would have the basic structure. This structure contains the link up to your page behind, register all the Nintex controls required, the two main JavaScript functions to read and save the configuration, and lastly the display section of your page.

(Update:Please note to get this working for SharePoint 2013, please make sure ~/_layouts/NintexWorkflow is replaced with ~/layouts/15/NintextWorkflow)

<%@ Assembly Name="$SharePoint.Project.AssemblyFullName$" %>;
<%@ Page Language="C#" DynamicMasterPageFile="~masterurl/default.master" AutoEventWireup="true"; CodeBehind="ReadFromPropertyBagDialog.aspx.cs" EnableEventValidation="false"

    Inherits="CFSP.CustomActions.ReadFromPropertyBag.ReadFromPropertyBagDialog" %>
<%@ Register TagPrefix="Nintex" Namespace="Nintex.Workflow.ServerControls" Assembly="Nintex.Workflow.ServerControls, Version=1.0.0.0, Culture=neutral, PublicKeyToken=913f6bae0ca5ae12" %>;

<%@ Register TagPrefix="Nintex" TagName="ConfigurationPropertySection" src="~/_layouts/NintexWorkflow/ConfigurationPropertySection.ascx" %>

<%@ Register TagPrefix="Nintex" TagName="ConfigurationProperty" src="~/_layouts/NintexWorkflow/ConfigurationProperty.ascx&" %>;

<%@ Register TagPrefix="Nintex" TagName="DialogLoad" Src="~/_layouts/NintexWorkflow/DialogLoad.ascx" %>

<%@ Register TagPrefix="Nintex" TagName="DialogBody" Src="~/_layouts/NintexWorkflow/DialogBody.ascx" %>;

<%@ Register TagPrefix="Nintex" TagName="SingleLineInput" Src="~/_layouts/NintexWorkflow/SingleLineInput.ascx" %>

<%@ Register TagPrefix="Nintex" TagName="PlainTextWebControl" Src="~/_layouts/NintexWorkflow/PlainTextWebControl.ascx" %>

<asp:Content ID="ContentHead" ContentPlaceHolderID="PlaceHolderAdditionalPageHead" runat="server">

    <Nintex:DialogLoad runat="server" />
    <script type="text/javascript" language="javascript">
        function TPARetrieveConfig() {
       //To Do
        }

        function TPAWriteConfig() {
        //To Do
        }

        onLoadFunctions[onLoadFunctions.length] = function () {
            dialogSectionsArray["<%= MainControls1.ClientID >"] = true;
        };
    </script>
</asp:Content>

<asp:Content ID="ContentBody" ContentPlaceHolderID="PlaceHolderMain" runat="Server">
  <Nintex:ConfigurationPropertySection runat="server" Id="MainControls1">
             <TemplateRowsArea>
                   <!--ToDo-->
              </TemplateRowsArea>
 </Nintex:ConfigurationPropertySection>

  <Nintex:DialogBody runat="server" id="DialogBody">
  </Nintex:DialogBody>
</asp:Content>

First section we will fill in will be the Nintex:ConfigurationPropertySection within the ContentPlaceHolderID PlaceHolderMain. In here we need to create a Nintex:ConfigurationProperty for each configuration property. In our case here that will be the Property Bag Name and the Result. You can see from below for consistency I have given the ID of the controls the same name as the Dependency Properties. Also note that the output because I’m want the user to assign the results to a workflow property, I’m using the Nintex:VariableSelector control.

            <Nintex:ConfigurationProperty runat="server" FieldTitle="Property Bag Property" RequiredField="True">
 <TemplateControlArea>
 <Nintex:SingleLineInput clearFieldOnInsert="true" filter="number" runat="server" id="propertyProperty"></Nintex:SingleLineInput>
 </TemplateControlArea>
 </Nintex:ConfigurationProperty>
 <Nintex:ConfigurationProperty runat="server" FieldTitle="Result Output" RequiredField="False">
 <TemplateControlArea>
 <Nintex:VariableSelector id="resultOutput" runat="server" IncludeTextVars="True"></Nintex:VariableSelector>
 </TemplateControlArea>

 </Nintex:ConfigurationProperty>

Next we are going to look at the two JavaScript files. When the dialog page is rendered and saved it passed an XML file, known as the configXml. We need to read out and read into the XML file using XPath. Please note when you come to deploying, if you find that your dialog control loads, however the ribbon bar is disabled at the top of the dialog, it is most likely that you have an error in the JavaScript. This took me a while to diagnose, but now I know what causes the issue, it allowed me to fix it straight away.

From the TPARetrieveConfig code the [@Name=’ ‘] will always be the public property name you gave it in the ReadFromPropertyBagActivity.cs file. As you can see from the code below there is a different way to obtain the value depending if the configuration property is a PrimitiveValue or a WorkflowVariable. This you defined in the GetDefaultConfig() method within the ReadFromPropertyBagAdapter.cs file. Lastly if you are still having problems getting the value, ensure your XPath is correct by debugging the Javascript and viewing the configXML variable.

 function TPARetrieveConfig() {
 setRTEValue('<%=propertyProperty.ClientID%>', configXml.selectSingleNode("/NWActionConfig/Parameters/Parameter[@Name='Property']/PrimitiveValue/@Value").text);

 document.getElementById('<%=resultOutput.ClientID%>').value = configXml.selectSingleNode("/NWActionConfig/Parameters/Parameter[@Name='ResultOutput']/Variable/@Name").text;

 }

From the TPAWriteConfig code it is basically doing the opposite of TPARetrieveConfig, just it checks the dropdown control (resultOutput) that a value has been selected before saving.

 function TPAWriteConfig() {
 configXml.selectSingleNode("/NWActionConfig/Parameters/Parameter[@Name='Property']/PrimitiveValue/@Value").text = getRTEValue('<%=propertyProperty.ClientID%>');

 var resultOuputCtrl = document.getElementById('<%=resultOutput.ClientID%>');

 if (resultOuputCtrl.value.length > 0) {
 configXml.selectSingleNode("/NWActionConfig/Parameters/Parameter[@Name='ResultOutput']/Variable/@Name").text = resultOuputCtrl.value;
 }

 return true;
 }

ReadFromPropertyBagAction.nwa

The NWA file as stated previously is just an XML file. This file is used by the Feature Receiver to register the custom action within SharePoint WebApplication.

Add an Elements file to the project and name it NWAFile. Add an XML file and name it ReadFromPropertyBagAction.nwa. From the properties window (press F4) we need to change the build action from None to Content, change the deployment type to ElementFile and remove the Path “\NWAFile”.

From within the XML file, remove all text and then place the following in.

(Update for SharePoint 2013 don’t forget Layouts is now /_layouts/15/)

<NintexWorkflowActivity>
 <Name>Retrieve from Property Bag</Name>
 <Category>CannonFodder Category</Category>
 <Description>A custom action to retrieve a property from the SharePoint Web Property Bag.</Description>
 <ActivityType>CFSP.CustomActions.ReadFromPropertyBag.ReadFromPropertyBagActivity</ActivityType>
 <ActivityAssembly>$SharePoint.Project.AssemblyFullName$</ActivityAssembly>
 <AdapterType>CFSP.CustomActions.ReadFromPropertyBag.ReadFromPropertyBagAdapter</AdapterType>
 <AdapterAssembly>$SharePoint.Project.AssemblyFullName$</AdapterAssembly>
 <HandlerUrl>ActivityServer.ashx</HandlerUrl>
 <Icon>/_layouts/NintexWorkflow/CustomActions/ReadFromPropertyBag/Images/ReadFromPropertyBagIcon49x49.png</Icon>
 <ToolboxIcon>/_layouts/NintexWorkflow/CustomActions/ReadFromPropertyBag/Images/ReadFromPropertyBagIconSmall30x30.png</ToolboxIcon>
 <ConfigurationDialogUrl>CustomActions/ReadFromPropertyBag/ReadFromPropertyBagDialog.aspx</ConfigurationDialogUrl>
 <ShowInCommonActions>yes</ShowInCommonActions>
 <DocumentLibrariesOnly>no</DocumentLibrariesOnly>
</NintexWorkflowActivity>

Let me explain each line to you.

  • Name – The display name of the custom action
  • Category – The category in the toolbox area that the custom action will be displayed under.
  • Description – A description of the category.
  • ActivityType – The Namespace of the Activity.cs file.
  • ActivityAssembly – The Full assembly name. (I’m using a token, which I’ll show how to set up afterwards)
  • AdapterType – The Namespace of the Adapter.cs file.
  • AdapterAssembly – The full assembly name. (I’m using a token, which I’ll show how to set up afterwards)
  • HandlerUrl – The Nintex handler, this will always be ActivityServer.ashx
  • Icon – The URL to the larger Icon.
  • ToolboxIcon – The URL to the smaller icon.
  • WarningIcon – The URL to the Warning Icon <-Not used in the above XML.
  • ConfigurationDialogUrl – The URL to the Action Dialog file. Note that we don’t put /_layouts/NintexWorkflow at the front.
  • ShowInCommonActions – If this custom action shows up in CommonActions on the toolbox.
  • DocumentLibrariesOnly – If this custom action should only be used in DocumentLibraries or not.

Lastly add the NWA file to the Web Application Feature.

Getting the Token $SharePoint.Project.AssemblyFullName$ to replace on a build.

At this point, save and close your solution. Now open up your .csproj file in Notepad or Notepad++. At the bottom of your first <PropertyGroup> section add the following XML. Save the file and re-open your solution in Visual Studio.

<TokenReplacementFileExtensions>nwa</TokenReplacementFileExtensions>

When you build your solution, Visual Studio will replace your token with the actual Full Assembly Name. More information about TokenReplacementFileExtensions.

WebApplication – Custom Action EventReceiver.cs

The Feature Event receiver is the final piece to our custom action. This will deploy or remove our custom action and make it available to the Web Application by modifying the Web.Config and registering the Action with Nintex within the farm. To add the custom action we use the nwa file. To remove it we need to know the namespace of the adapter, and assembly name. As you build more custom actions you can reuse this feature and just de-activate and re-activate each time you deploy a new custom action.

using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Runtime.InteropServices;
using System.Xml;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using Nintex.Workflow;
using Nintex.Workflow.Administration;
using Nintex.Workflow.Common;
using System.Reflection;
namespace CFSP.CustomActions.Features.WebApplication___Custom_Actions
{
    [Guid(&amp;quot;07607091-449b-422b-94e4-84e6d863eb9e&amp;quot;)]
    public class WebApplication___Custom_ActionsEventReceiver : SPFeatureReceiver
    {
        public override void FeatureActivated(SPFeatureReceiverProperties properties)
        {
            SPWebApplication parent = (SPWebApplication) properties.Feature.Parent;
            AddCustomAction(parent, properties, "ReadFromPropertyBagAction.nwa");
           //Add additional Custom Actions nwa files here.
        }
        public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
        {
            SPWebApplication parent = (SPWebApplication) properties.Feature.Parent;
            RemoveCustomAction(parent, properties,
                "CFSP.CustomActions.ReadFromPropertyBag.ReadFromPropertyBagAdapter",
                Assembly.GetExecutingAssembly().FullName);
           //Remove additional Custom Actions here.
        }

        protected void AddCustomAction(SPWebApplication parent, SPFeatureReceiverProperties properties,
            string pathToNWAFile)
        {
            // First step is register the action to the Nintex Workflow database
            XmlDocument nwaXml = GetNWADefinition(properties, pathToNWAFile);

            ActivityReference newActivityReference = ActivityReference.ReadFromNWA(nwaXml);

            ActivityReference action = ActivityReferenceCollection.FindByAdapter(newActivityReference.AdapterType,
                newActivityReference.AdapterAssembly);

            if (action != null)
            {
                // update the details if the adapter already exists
                ActivityReferenceCollection.UpdateActivity(action.ActivityId, newActivityReference.Name,
                    newActivityReference.Description, newActivityReference.Category,
                    newActivityReference.ActivityAssembly, newActivityReference.ActivityType,
                    newActivityReference.AdapterAssembly, newActivityReference.AdapterType,
                    newActivityReference.HandlerUrl, newActivityReference.ConfigPage,
                    newActivityReference.RenderBehaviour, newActivityReference.Icon, newActivityReference.ToolboxIcon,
                    newActivityReference.WarningIcon, newActivityReference.QuickAccess,
                    newActivityReference.ListTypeFilter);
            }
            else
            {
                ActivityReferenceCollection.AddActivity(newActivityReference.Name, newActivityReference.Description,
                    newActivityReference.Category, newActivityReference.ActivityAssembly,
                    newActivityReference.ActivityType, newActivityReference.AdapterAssembly,
                    newActivityReference.AdapterType, newActivityReference.HandlerUrl, newActivityReference.ConfigPage,
                    newActivityReference.RenderBehaviour, newActivityReference.Icon, newActivityReference.ToolboxIcon,
                    newActivityReference.WarningIcon, newActivityReference.QuickAccess,
                    newActivityReference.ListTypeFilter);
                action = ActivityReferenceCollection.FindByAdapter(newActivityReference.AdapterType,
                    newActivityReference.AdapterAssembly);
            }

            // Second step is to modify the web.config file to allow use of the activity in declarative workflows
            string activityTypeName = string.Empty;
            string activityNamespace = string.Empty;

            Utility.ExtractNamespaceAndClassName(action.ActivityType, out activityTypeName, out activityNamespace);
            AuthorisedTypes.InstallAuthorizedWorkflowTypes(parent, action.ActivityAssembly, activityNamespace,
                activityTypeName);
            // Third step is to activate the action for the farm
            ActivityActivationReference reference = new ActivityActivationReference(action.ActivityId, Guid.Empty,
                Guid.Empty);

            reference.AddOrUpdateActivationReference();
        }
        protected void RemoveCustomAction(SPWebApplication parent, SPFeatureReceiverProperties properties,
            string adapterType, string adapterAssembly)
        {
            ActivityReference action = ActivityReferenceCollection.FindByAdapter(adapterType, adapterAssembly);
            if (action != null)
            {
                // Remove the action definition from the workflow configuration database if the Feature is not activated elsewhere
                if (!IsFeatureActivatedInAnyWebApp(parent, properties.Definition.Id))
                    ActivityReferenceCollection.RemoveAction(action.ActivityId);
                string activityTypeName = string.Empty;
                string activityNamespace = string.Empty;
                Utility.ExtractNamespaceAndClassName(action.ActivityType, out activityTypeName, out activityNamespace);

                // Remove the web.config entry
                Collection<SPWebConfigModification> modifications = parent.WebConfigModifications;

                foreach (SPWebConfigModification modification in modifications)
                {
                    if (modification.Owner == AuthorisedTypes.OWNER_TOKEN)
                        // OWNER_TOKEN is the owner for any web config modification added by Nintex Workflow
                    {
                        if (IsAuthorizedTypeMatch(modification.Value, action.ActivityAssembly, activityTypeName,
                            activityNamespace))
                        {
                            modifications.Remove(modification);
                            parent.Farm.Services.GetValue<SPWebService>().ApplyWebConfigModifications();
                           break;
                        }
                    }
                }
            }
        }
        private bool IsAuthorizedTypeMatch(string modification, string activityAssembly, string activityType,
            string activityNamespace)
        {
            XmlDocument doc = new XmlDocument();
            doc.LoadXml(modification);

            if (doc.FirstChild.Name == "authorizedType")
            {
                return (doc.SelectSingleNode("//@TypeName").Value == activityType
                        && doc.SelectSingleNode("//@Namespace").Value == activityNamespace
                        && doc.SelectSingleNode("//@Assembly").Value == activityAssembly);
            }
            return false;
        }

        private bool IsFeatureActivatedInAnyWebApp(SPWebApplication thisWebApplication, Guid thisFeatureId)
        {
            SPWebService webService = SPWebService.ContentService;
            if (webService == null)
                throw new ApplicationException("Cannot access ContentService");
            SPWebApplicationCollection webApps = webService.WebApplications;
            foreach (SPWebApplication webApp in webApps)
            {
                if (webApp != thisWebApplication)
                    if (webApp.Features[thisFeatureId] != null)
                        return true;
            }

            return false;
        }

        private XmlDocument GetNWADefinition(SPFeatureReceiverProperties properties, string pathToNWAFile)
        {
            using (Stream stream = properties.Definition.GetFile(pathToNWAFile))
            {
                XmlDocument nwaXml = new XmlDocument();
                nwaXml.Load(stream);
                return nwaXml;
            }
        }
   }
}

Deploying and checking everything has worked.

If you have done everything correctly, at the point go ahead and deploy your solution. Ensure your feature has been activated for a given web application. Then open Central Admin. Under the Nintex Workflow Management section select Manage allowed actions.

In Manage Allowed action you should see your Action, and that it is ticked. Meaning it is allowed to be used.

Let us go to our site now, and create a new Nintex Workflow for our custom list. My list has a Single line of text called Title and another one called PropertyValue. In the toolbar panel of my Nintex Workflow, I can now see my CannonFodder Category, and my custom action.

Drag this onto your page. If you find it doesn’t stick to your workflow page, go back and check your nwa file that all your Types and Assemblies match up correctly. Once it is on your page, configure this custom action. Your dialog will be presented to you.

Assign the Property Bag Property to the Item Property Title.

Create a new Workflow Variable and name this PBResult. Then assign Result Output to PBResult. Click Save on the dialog.


Under Libraries and List find the action Set field value and drag this onto the form underneath our custom action. Then configure it so that it sets our column PropertyValue to Workflow Data called PBResult that we created in the last step. Click Save on the dialog.

Lastly before we test this out, on the ribbon bar of the workflow page, under the Nintex Workflow 2010 tab, click Workflow Settings. Configure it so that it Starts when items are created.

Save and Publish the workflow.

Testing

I already have a value in my property bag called cann0nf0dderpb. So I’m going to create a new item in my list, and set the title to cann0nf0dderpb and save the form.

After a moment or two the workflow has kicked in. Once I refresh my list I can see that in PropertyValue, the value of my PropertyBag item is displayed. I purposely made the property bag value say ‘Nintex Workflow Worked’.

As I stated right at the start, there is a lot of files that need to be in place to plumb everything together, and it’s only the small Execute function that does the actual work. For further reading, please download the Nintex SDK

You can download this project directly from my OneDrive. – 2010

You can download this project directly from my OneDrive – 2013

WebConfigModifications adding and removing


I have had loads of problems in the past and recently when it comes to updating the Web.Config of a SharePoint Web Application. Today with the help of the blog http://blogs.devhorizon.com/reza/2008/01/05/spwebconfigmodifications-top-6-issues/ by Reza Alirezaei, I think I have worked out why it never works. It’s all to do with the Name.

Using PowerShell I’m trying to add a connection string to a SQL Database to my Web.Config. First I want to create the section in my web.config of <connectionStrings>. This might already exist, but I have found that if it does already exist it will not be duplicated.


#MyWebApplication URL

$myHandlerWebApplication = "http://intranet.cann0nf0dder.com"

Add-PSSnapin Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue

#Get and Load SharePoint dll.

$sharePointDLL = $ENV:CommonProgramFiles+("\Microsoft Shared","Web Server Extensions","14","ISAPI" -join "\") + "\Microsoft.SharePoint.dll"

[System.Reflection.Assembly]::LoadFile($sharePointDLL) | Out-Null

#Get Web Application.

$webapp = [Microsoft.SharePoint.Administration.SPWebApplication]::Lookup($myHandlerWebApplication)

#Create the ConnectionString Section.

#new SPWebConfigModification

$connectionPath = New-Object "Microsoft.SharePoint.Administration.SPWebConfigModification"

#XPath to the section.

$connectionPath.Path = "configuration"

#Name of the the SPWebConfigModification

$connectionPath.Name = "connectionStrings"

#Order – 0 means first, lower number will place further down.

$connectionPath.Sequence = 0

#Owner of the SPWebConfigModification

$connectionPath.Owner = "cann0nF0dderMyDemoConnectionSettings"

#Type can be 0 - EnsureChildNode, 1 - EnsureAttribute, 2 - EnsureSection

$connectionPath.Type = 2

#Add Modification to collection.

$webApp.WebConfigModifications.Add($connectionPath)

The above code will create a section similar to below

<connectionStrings></connectionStrings>
</configuration>

Underneath the above code we will write a ChildNode which will contain our actual connection string. (Please don’t run the following in your environment, you won’t be able to remove by code)


$connectionString = New-Object "Microsoft.SharePoint.Administration.SPWebConfigModification"

$connectionString.Path = "configuration/connectionStrings"

$connectionString.Name = "MyConnectionStringName"

$connectionString.Sequence = 0

$connectionString.Type = 0

$connectionString.Owner = " cann0nF0dderMyDemoConnectionSettings "

$connectionString.Value = "<add name='MyConnectionStringName' connectionString='server=SQLServer1;Database=Database1;Trusted_Connection=True' />"

$webapp.WebConfigModifications.Add($connectionString)

#Update the Web Application and apply all existing web.config modifications

$webapp.Update()

$webapp.Parent.ApplyWebConfigModifications()

 

The above code will add an entry to your web.config, however if you ever want to remove it by code it won’t remove. The reason, I discovered, is to do with the line $connectionString.Name = “MyConnectionStringName”. The Name parameter used is not really the names. They are relative xpaths to the modification. If these are not correctly defined, when you run the code to remove, it might actually remove the value from the $webapp.WebConfigModifications collection, it doesn’t remove it from the actual Web.Config. Therefore in my case the name should be


$connectionString.Name = "add[@name='MyConnectionStringName']"

To remove the code, the best way is to grab the webConfigModifications collection where owner is x. Then loop through and delete.


$myHandlerWebApplication = "http://intranet.cann0nf0dder.com "

Add-PSSnapin Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue

$sharePointDLL = $ENV:CommonProgramFiles+("\Microsoft Shared","Web Server Extensions","14","ISAPI" -join "\") + "\Microsoft.SharePoint.dll"

[System.Reflection.Assembly]::LoadFile($sharePointDLL) | Out-Null

$webapp = [Microsoft.SharePoint.Administration.SPWebApplication]::Lookup($myHandlerWebApplication)

$Modification = $webapp.WebConfigModifications | ? { $_.Owner -eq " cann0nF0dderMyDemoConnectionSettings "}

 if($Modification -ne $null){

 write-Host "Deleting existing entries"

 #Delete them otherwise they will duplicate.

    foreach($mod in $Modification){

            $webapp.WebConfigModifications.Remove($mod)

    }

$webapp.Update()

$webapp.Parent.ApplyWebConfigModifications()

Write-Host "Complete"

Make SPLongOperation postback to same page. (Well a workaround at least)


The title here “Make SPLongOperation postback to the same page. (Well a workaround at least)” is because there is no actual way of making the SPLongOperation postback to the same page. However I have been able to achieve a similar affect. First let’s explain what the SPLongOperation is.

The SPLongOperration class in SharePoint is a brilliant way to inform your users that a long operation is running in the background, without freezing or locking up the UI while the process is running. You may have seen the screen many times when creating a site.

The code to create this is very simple.


 using (SPLongOperation longOperation = new SPLongOperation(this.Page))

{

 //Show Text for the Top Line on the screen.

  longOperation.LeadingHTML = "Provisioning Sites";

 //Show text for the Bottom line on the screen.

  longOperation.TrailingHTML = "Please wait while the sites are being provisioned.";

  longOperation.Begin();

  try

 {

  //The code that will take it's time.

  Thread.Sleep(5000);

 //On complete navigate to your success/complete page.

  longOperation.End("MySuccessPage.aspx");

 }

 catch(ThreadAbortException)

 {

  //Don't do anything, this error can occur because the SPLongOperation.End

 // performs a Response.Redirect internally and doesn't take into account that other code might still be executed.

 }

 catch(Exception ex)

 {

  //When an exception happens, the page is redirected to the error page.

  //Here you can redirect to another custom page.

  SPUtitility.TransferToErroPage(ex.ToString());

 }

}

As you can see from above it is pretty simple. However the longOperation.End(“MySuccessPage.aspx”) redirects the user to another page. You can point it to the same page you are currently on such as longOperation.End(Request.Url.ToString()); but this doesn’t cause a post back.

SPLongOperation.End has 3 overloads.

  • SPLongOperation.End(string strRedirectPage)
  • SPLongOperation.End(string strProposedRedirect, SPRedirectFlags rgfRedirect, HttpContext context, string queryString)
  • SPLongOperation.End(string strProposedRedirect, SPRedirectFlags rgfRedirect, HttpContext context, string queryString, string strScript)

It is the third overload here that can help us achieve a postback like state.

The strScript is javascript that is run before the page gets redirected. If we reflect on the 3rd method we can see how this works.

public void End(string strProposedRedirect, SPRedirectFlags rgfRedirect, HttpContext context, string queryString, string strScript)

{

    string str;

    if (!SPUtility.DetermineRedirectUrl(strProposedRedirect, rgfRedirect, context, queryString, out str))

    {

        str = strProposedRedirect;

    }

    string str2 = SPHttpUtility.EcmaScriptStringLiteralEncode(str);

    string str3 = string.Format(CultureInfo.InvariantCulture, "window.location.replace(\"{0}\");", new object[] { str2 });

    if (strScript != null)

    {

        str3 = strScript + str3;

    }

    this.EndScript(str3);

}

In this method call, the javascript is added to the front of the windows.location.replace, so that your html/javascript is run first, then it redirects you to your strProposedRedirect URL. From this knowledge, if I put return false; as my strScript, it prevents any JavaScript running after and prevents the redirection of the page. However I’m still looking at the loading page.

So how does any of this help me? What am I trying to achieve?

My end goal is to perform a long process that gathers some results and then displays them on the same page once the long process has finished. Example screen shots below.

The above can be achieved by using Javascript to call a function that will post the user to the page with the required results within the post data. In my original example of how to use SPLongOperation you would replace the line Thread.Sleep(5000); with your method that would process the word file and return in a string the results. Then you need to create the javascript post form data using the results.


…

 try

 {

  //The code that will take it's time.

  WordProcessing wordprocess = new WordProcessing(FileUpload1.FileContent, FileUpload1.FileName);

//results is a global string variable.

 results = wordprocess.Process();

 //On complete navigate to your success/complete page.

  longOperation.End(Request.Url.ToString(), SPRedirectFlages.UseSource, HttpContext.Current, "", postBackToPage();

 }

 …

 

Now we need to create the JavaScript function post_Back_To_Page() which will be called.


private string postBackToPage()

        {

            StringBuilder str = new StringBuilder();

            str.Append("function post_Back_To_Page(){");

            str.Append("var method = 'post';");

            str.Append("var form = document.createElement('form');");

            str.Append("form.setAttribute('method', method);");

            str.Append("form.setAttribute('action', '" + Request.Url.ToString() + "');");

            str.Append("var data = document.createElement('input');");

            str.Append("data.setAttribute('type', 'hidden');");

            str.Append("data.setAttribute('name', 'results');");

            str.Append("data.setAttribute('value', '" + SPEncode.UrlEncode(results) + "');");

            str.Append("form.appendChild(data);");

            str.Append("document.body.appendChild(form);");

            str.Append("form.submit();");

            str.Append("};post_Back_To_Page();return false;");

            return str.ToString();

        }

Now on loading the page if there is a form data called results, we will want to grab and display that. So on your page load, add the following.


protected void Page_Load(object sender, EventArgs e)

{

  if (Request.Form["results"] != null)

 {

   txt.Results.Text = SPEncode.UrlDecodeAsUrl(Request.Form["results"]);

 }

}

At this stage, I was excited that a workaround was possible. However first time I ran this code I encountered the error “The security validation for this page is invalid.”

This is happening because I’m trying to insert data into a page. SharePoint thinks I’m a hacker, and therefore rejects the post. After extensive searching on the web, peoples suggestion was to disable the SPWebapplication.FormDigestSettings. Do not do this! It is a huge security risk to your farm. What SharePoint is after is the form digest control value.

According to MSDN http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.webcontrols.formdigest.aspx

“The FormDigest value is a generated security validation, or message digest, to help prevent the type of attack whereby a user is tricked into posting data to the server without knowing it. The security validation is specific to a user, site, and time period and expires after a configurable amount of time. When the user requests a page, the server returns the page with security validation inserted. When the user then submits the form, the server verifies that the security validation has not changed.”

We need to create a form digest value and add it to the form being posted in the Javascript. Replace the code in the method postBackToPage()


private string postBackToPage()

        {

          //Create the Form Digest String.

            StringBuilder formDigestString = new StringBuilder();

            StringWriter tw = new StringWriter(formDigestString);

            HtmlTextWriter textWriter = new HtmlTextWriter(tw);

            FormDigest formDigest = new FormDigest();

            formDigest.RenderControl(textWriter);

            string formDigestHtml = formDigestString.ToString();

            StringBuilder str = new StringBuilder();

            str.Append("function post_Back_To_Page(){");

            str.Append("var method = 'post';");

            str.Append("var form = document.createElement('form');");

            str.Append("form.setAttribute('method', method);");

            str.Append("form.setAttribute('action', '" + Request.Url.ToString() + "');");

           //Add formdigestHtml to the Form.

            str.Append("var digest = document.createElement('div');");
            str.Append("digest.innerHTML = '" + formDigestHtml + "';");

            str.Append("form.appendChild(digest);");

            str.Append("var data = document.createElement('input');");

            str.Append("data.setAttribute('type', 'hidden');");

            str.Append("data.setAttribute('name', 'results');");

            str.Append("data.setAttribute('value', '" + SPEncode.UrlEncode(results) + "');");

            str.Append("form.appendChild(data);");

            str.Append("document.body.appendChild(form);");

            str.Append("form.submit();");

            str.Append("};post_Back_To_Page();return false;");

            return str.ToString();

        }

As the title of this blog says “Well a workaround at least.” You aren’t actually posting back to the same page, but you can redirect back to the same page, passing back data that you might want to display. The form created in JavaScript can be extended to add extra elements for other data you wish to post back as well.