Tuesday, May 19, 2015

How to have AppSettings like config values available in SharePoint TimerJobs

In SharePoint timer jobs there is a simple way to implement app setting like key value pairs which can be used to store configuration values. Since Timer Jobs run under SharePoint central admin context so keeping configuration values in web.config is not feasible and that too if a job is catering to multiple websites then keeping web specific configuration is also a challenge.

As a simple yet effective solution is to keep such configuration in the form of a custom list at web or site collection level, whichever is more feasible and logical to your need. Here is how I have done the same

For my requirement I need to have couple of static values and some environment specific configurations to be stored for use in a Timer job. As a solution to this I decided to keep these configurations in a custom defined list. To do that

Create a custom list and name it Config or Configuration in the web site and add one field "Value" Set the type of this field to multiple lines of text( set rich text to false.


This list will store all the configurable values e.g. environment specific server paths, list names, notification email formats, email addresses or whatever you want to store as a configuration.

After defining this list now next step is to populate the configuration values and use them in the timer job.

CustomTimerJob.cs
using System;
using System.Data;
using System.Text;
using System.Collections.Generic;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using Microsoft.Office.Server.Diagnostics;

public class MyTimerJob : SPJobDefinition
{
   private static string SERVER_NAME = "xxx-xxx-xxxx";
   private static string LIST_NAME = "Test";
   private static string WEB_NAME = "Webname";

   public override void Execute(Guid targetInstanceId)
        {
            try
            {
                SPSecurity.RunWithElevatedPrivileges(delegate()
                {
          
          SPWebApplication webApp = this.Parent as SPWebApplication;

                    //root site collection
                    string siteURL = webApp.Sites[0].Url;

                    using (SPSite site = new SPSite(siteURL))
                    {
                        using (SPWeb web = site.AllWebs[WEB_NAME])
                        {
                            //Read timer job configuration 
                            //from config list from web
                            ReadConfiguration(web);

                            SPList list = web.Lists[LIST_NAME];
                            SPQuery q = new SPQuery();

                            //Get all items to process
                            q.Query = "<Where><And><Eq><FieldRef Name='WFStatus' /><Value Type='Text'>3</Value></Eq><Eq><FieldRef Name='IsProcessed' /><Value Type='Boolean'>0</Value></Eq></And></Where><OrderBy><FieldRef Name='ID' /></OrderBy>";

            //Record collection
            SPListItemCollection itemCollection = list.GetItems(q);
                            if (itemCollection.Count > 0)
                            {
                                                                          //Write your logic here for items
                            }
                        }
                    }
                });
            }
            catch (Exception ex)
            {
                PortalLog.LogString("Exception Occurred in: {0} || {1} || {2}", " Timer Job: Execute Method", ex.Message, ex.StackTrace);
            }

        }

        /// <summary>
        /// Read configuration from config list from web.
        /// </summary>
        /// <param name="web">Web object</param>
        private static void ReadConfiguration(SPWeb web)
        {
            //Overwrite default values from configuration
            if (web != null)
            {
                SPList configList = web.Lists.TryGetList("Config");
                if (configList != null)
                {
                    foreach (SPListItem item in configList.Items)
                    {
               SERVER_NAME = GetConfigValue(item, "SERVER_NAME")!= string.Empty ? GetConfigValue(item, "SERVER_NAME") : SERVER_NAME;
               LIST_NAME = GetConfigValue(item, "LIST_NAME")!= string.Empty ? GetConfigValue(item, "LIST_NAME") : LIST_NAME;

                    }
                }
            }

        }

 /// <summary>
 /// Get config list value based on key.
 /// </summary>
 /// <param name="key">key name</param>
 /// <returns></returns>
 private static string GetConfigValue(SPListItem item, string  key)
        {
            string value = string.Empty;
            if (string.Equals(item["Title"].ToString(), key) && item["Value"] != null)
            {
                value = item["Value"].ToString();
            }
            return value;

        }
}


The above code will read the configuration values from the config list from the web. To make this config list non editable be normal users stop the inheritance level for the list and add only site collection admin or other admin user contribute access to the list and also add the farm account user under which the timer job will run to the list contribute permissions as well. When the job tried to access this list the user under which the timer job is running should have access to the list.

Having this type of configuration has key benefits of


1) Managing the config values with ease and no physical file access required like the web.config file.
2) Change in config values does not impact the AppPool or resets web application.
3) Provides more control, for example if you need to write a job to send email notifications then the email format can be kept as a config value and changed anytime without impacting code.
4) Make the job as configurable as possible so that minimal code changes are required.

Happy Coding!

Wednesday, May 13, 2015

Nintex Workflow Task Form giving error Failed to retrieve form from workflow. Error: Value cannot be null.

Issue: A strange error started coming on one of the SharePoint 2010 environment when a Nintex 2010 workflow task form is opened by the assigne, it breaks and does not open the task form. The reason is unknown but the issue is grave because none of the assigned task form works.

Detailed error stack trace is like

Area     : SharePoint FoundationCategory : GeneralEventID  : 8ncaMessage  : Application error when access /XXXX/Lists/Workflow Tasks/EditForm.aspx, Error=Value cannot be null.  Paramete           r name: s   at System.IO.StringReader..ctor(String s)     at Nintex.Workflow.WorkflowConfig2010.LoadWorkflow           Config(String configXml)     at Nintex.Workflow.Forms.ControlTemplates.TaskForm.GetWorkflowConfig()     at N           intex.Workflow.Forms.ControlTemplates.TaskForm.GetForm(String formKey, FormData formData)     at Nintex.Work           flow.Forms.ControlTemplates.TaskForm.ConfigureFiller()     at Nintex.Workflow.Forms.ControlTemplates.TaskFor           m.InitialiseForm()     at Nintex.Workflow.Forms.ControlTemplates.TaskForm.OnInit(EventArgs e)     at System.           Web.UI.Control.InitRecursive(Control namingContainer)     at System.Web.UI.Control.InitRecursive(Control nam           ingContainer)     at System.Web.UI.Control.AddedControl(Control control, Int32 index)     at Microsoft.Share           Point.WebPartPages.ListFormWebPart.CreateChildControls()     at System.Web.UI.Control.EnsureChildControls()               at Microsoft.SharePoint.WebPartPages.WebPart.get_WebPartMenu()     at Microsoft.SharePoint.WebPartPages.          ListFormWebPart.CreateWebPartMenu()     at System.Web.UI.Control.OnLoad(EventArgs e)     at Microsoft.ShareP           oint.WebPartPages.ListFormWebPart.OnLoad(EventArgs e)     at System.Web.UI.Control.LoadRecursive()     at Sy           stem.Web.UI.Control.LoadRecursive()     at System.Web.UI.Control.LoadRecursive()     at System.Web.UI.Contro           l.LoadRecursive()     at System.Web.UI.Control.LoadRecursive()     at System.Web.UI.Control.LoadRecursive()               at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAft           erAsyncPoint)


Resolution: In order to resolve this issue I tried saving the task form by opening the task form in the Nintex workflow and saving it, republishing the workflow couple of times but in vain. Even the ULS logs provided only the above error with no clue as to why it was breaking. I took following steps to resolve this issue:

1. Export the existing error prone workflow as a backup copy.
2. Delete the workflow from the list in the current environment.
3. Export the same workflow from another environment i.e. production.
4. Imported the exported workflow to the problematic environment and publish it.

After publishing the workflow the task forms started opening correctly and the error was gone. There was no change in list permission level, site user groups, list definition or task form. However, it might save someones time in getting around such issue.

Happy Workflow-ing :)