Tuesday, December 23, 2008

Creating Timer Job Definitions

At my current workplace, developers do not have the full access we require at times to run console applications on production servers. To get around this problem I have started creating my batch processes as SharePoint Timer Jobs.

Firstly, the code that does the batch processing is contained in its own class. This class has one static method Execute with a parameter of type SPWeb. I contain it in its own class so I can call if from anywhere (timer job, feature, console app, web part etc).

eg.

    public class SynchronizeGroupsManager

    {

        public static void Execute(SPWeb web)

        {

            //Do something here...

        }

    }



From there I create a SharePoint job definition. This class is a subclass of SPJobDefinition. This class implements an override of the Execute method. In this method, I create an SPWeb instance and pass this onto the static method I created early. This is the method that gets executed when the timer job fires.

eg.

    public class SynchronizeGroupsJob : SPJobDefinition

    {

        private const string siteIdPropertyName = "SiteCollectionId";

 

        public SynchronizeGroupsJob() : base()

        {

        }

 

        public SynchronizeGroupsJob(string jobName, SPService service, SPServer server, SPJobLockType targetType) : base(jobName, service, server, targetType)

        {

        }

 

        public SynchronizeGroupsJob(string jobName, SPWebApplication webApp, Guid siteId) : base(jobName, webApp, null, SPJobLockType.ContentDatabase)

        {

            this.Title = JobName.SynchronizeGroups;

            this.Properties.Add(siteIdPropertyName, siteId);

        }

 

        public override void Execute(Guid targetInstanceId)

        {

            HandleEventFiring handleEventFiring = new HandleEventFiring();

 

            handleEventFiring.CustomDisableEventFiring();

            try

            {

                Guid siteId = (Guid)this.Properties[siteIdPropertyName];

 

                using (SPSite site = new SPSite(siteId))

                {

                    using (SPWeb web = site.OpenWeb())

                    {

                        try

                        {

                            SynchronizeGroupsManager.Execute(web);

                        }

                        catch (Exception ex)

                        {

                            ErrorLogger.WriteErrorToTraceLog(web, ex);

                        }

                    }

                }

            }

            finally

            {

                handleEventFiring.CustomEnableEventFiring();

            }

        }

    }



The final step is to create a feature to install the timer job (and to do an initial execution if needed). I like to do the initial execution because I can deactivate and reactivate the feature at any time and perform the batch processing immediately.

You have plenty of options to create different schedules for your timer job. The following example is a daily schedule. As I have not specified any times, the following timer job will fire at 12:00am by default.

eg.

    public class InstallSynchronizeGroupsJob : SPFeatureReceiver

    {

        public override void FeatureActivated(SPFeatureReceiverProperties properties)

        {

            SPSite site = properties.Feature.Parent as SPSite;

 

            RemoveExistingTimerJob(site);

 

            SPDailySchedule schedule = new SPDailySchedule();

 

            SynchronizeGroupsJob synchronizeGroupsJob = new SynchronizeGroupsJob(JobName.SynchronizeGroups, site.WebApplication, site.ID);

            synchronizeGroupsJob.Schedule = schedule;

            synchronizeGroupsJob.Update();

 

            InitialSynchronization(site);

        }

 

        public override void FeatureDeactivating(SPFeatureReceiverProperties properties)

        {

            SPSite site = properties.Feature.Parent as SPSite;

            RemoveExistingTimerJob(site);

        }

 

        public override void FeatureInstalled(SPFeatureReceiverProperties properties)

        {

        }

 

        public override void FeatureUninstalling(SPFeatureReceiverProperties properties)

        {

        }

 

        private static void RemoveExistingTimerJob(SPSite site)

        {

            foreach (SPJobDefinition job in site.WebApplication.JobDefinitions)

            {

                if (job.Name == JobName.SynchronizeGroups)

                {

                    job.Delete();

                    break;

                }

            }

        }

 

        private static void InitialSynchronization(SPSite site)

        {

            using (SPWeb web = site.OpenWeb())

            {

                SynchronizeGroupsManager.Execute(web);

            }

        }

    }

Thursday, November 27, 2008

Creating a user friendly error message from web part

Instead of getting a web part exception from exceptions raised in web part code, you can use the following to show a user friendly error message using built-in SharePoint functionality:

SPUtility.TransferToErrorPage("This is a user friendly error message.");



Wednesday, November 26, 2008

Javascript to make fields readonly in edit view

The following javascript can be used to hide Input, Date and Select fields in the builtin SharePoint edit pages. This should cover most fields. Haven't touched the People fields yet as they look to be a lot more complicated.

I have almost completed incorporating these functions into a webpart that can customise the readonly permissions based on the current users SharePoint group membership.



<script language='javascript'>

 

function GetControlByTypeAndName(type, name)

{

    var controls = document.body.all.tags(type);

    for (var i = 0; i < controls.length; i++)

    {

        var control = controls[i];

        if (control.title.toLowerCase() == name.toLowerCase())

            return control;

    }

    return null

}

 

function SetInputReadOnly(name)

{

    var control = GetControlByTypeAndName('input', name);

    if (control)

    {

        control.style.display = 'none';

        var label = document.createElement('');

        label.innerHTML = control.value;

        control.parentElement.insertBefore(label, control);

    }

}

 

function SetSelectReadOnly(name)

{

    var control = GetControlByTypeAndName('select', name);

    if (control)

    {

        control.style.display = 'none';

        var label = document.createElement('');

        label.innerHTML = control.value;

        control.parentElement.insertBefore(label, control);

    }

}

 

function SetDateReadOnly(name)

{

    var control = GetControlByTypeAndName('input', name);

    if (control)

    {

        var parentControl = control.parentElement.parentElement.parentElement.parentElement;

        parentControl.style.display = 'none';

        var label = document.createElement('');

        label.innerHTML = control.value;

        parentControl.parentElement.appendChild(label);

    }

}

 

SetInputReadOnly('Author Surname');

SetInputReadOnly('Author First Name');

SetDateReadOnly('Document Date');

SetSelectReadOnly('Type of Document');

 

</script>

Tuesday, November 18, 2008

Filtering ListView with Query String

This is something I have needed for a while that I didn't think was possible. I have often needed to filter a list based on an attribute of the currently logged in user. In the past I have create multiple views for each of the possible combinations.

I thought there must be a better way. After filtering a few lists, I noticed the query string was changing and adding the filter values to the query string. I started creating my own query string filters and it worked a treat.

All you need to do is add the following to the query string:

?FilterField1=[FieldInternalName]&FilterValue1=[Filter Value]

You can repeat this for multiple columns by using FilterField2, FilterValue2 etc.

To filter on Person field, use their user name:

eg. FilterValue1=The%20Masked%20SharePointer

To filter on DateTime field, use US format:

eg. FilterValue1=11/20/2008

If you are having trouble working out how to filter a column, do the filter through the SharePoint interface and see how it does it!!

As an added bonus, the field you filter on doesn't even need to be on the current view!!

Friday, November 14, 2008

Storing SPListItem in SPFolder

We have had a need to store SPListItems into a folder structure in a SPList (preferably without the user having to navigate to the folder and select 'New Item'). The folder structure we required is a top level of year and then a month folder.

Much research on the web was about having a list event handler that moves the list item to the desired location. Move however for a list item (not document) is only possible by creating a new item in the desired location, copying all field values and then deleting the original item. I thought this was ridiculous and that there must be a better way.

I decided to attack the problem from the other end and change the folder location before the list item is saved.

To do this I created a simple web part and placed it in the "NewForm.aspx" of the list. The web part checks the current folder (stored as "RootFolder" in query string), compares it to the expected location and if needed redirects to the same page with the corrected RootFolder.

The code below demonstrates:

(code for DoesFolderExist/CreateFolder can be found in previous article)


protected override void Render(HtmlTextWriter writer)

{

    base.Render(writer);

 

    SPList list = SPContext.Current.Web.Lists["ListName"];

 

    DateTime today = DateTime.Today;

 

    string expectedFolderPath = string.Format("{0}/{1}", today.Year, today.Month);

 

    string currentRootFolder = Page.Request.QueryString["RootFolder"];

    string expectedRootFolder = string.Format("{0}/{1}" , list.RootFolder.ServerRelativeUrl, expectedFolderPath);

 

    if (currentRootFolder != expectedRootFolder)

    {

        if (!DoesFolderExist(list, expectedFolderPath))

            CreateFolder(list, expectedFolderPath);

 

        Page.Response.Redirect(HttpUtility.HtmlEncode(string.Format("{0}/{1}?RootFolder={2}", list.ParentWeb.Url, list.Forms[PAGETYPE.PAGE_NEWFORM].Url, expectedRootFolder)));

    }

}

Some useful routines when using SPFolders with SPListItems

Working with SPFolders and SPLists is not as straight forward as it should be. The following methods could help:

/// <summary>

/// Creates a folder (recursively  required). 

/// </summary>

/// <param name="list">The SharePoint list</param>

/// <param name="folderPath">Folder structure: eg: 2008/11/Sales</param>

public static void CreateFolder(SPList list, string folderPath)

{

    if (list == null)

        throw new ApplicationException("List is not specified.");

 

    if (string.IsNullOrEmpty(folderPath))

        throw new ApplicationException("Folder Path is not specified.");

 

    string[] hierarchy = folderPath.Split('/');

    string currentPath = string.Empty;

 

    foreach (string level in hierarchy)

    {

        if (!string.IsNullOrEmpty(currentPath))

            currentPath += "/";

 

        currentPath += level;

 

        if (!DoesFolderExist(list, currentPath))

        {

            SPListItem folder = list.Folders.Add(list.RootFolder.ServerRelativeUrl, SPFileSystemObjectType.Folder);

            folder["Title"] = currentPath;

            folder.Update();

        }

    }

}

 

/// <summary>

/// Returns whether a folder exists within a list.

/// </summary>

/// <param name="list">The SharePoint list</param>

/// <param name="folderPath">Folder structure: eg: 2008/11/Sales</param>

/// <returns></returns>

public static bool DoesFolderExist(SPList list, string folderPath)

{

    if (list == null)

        throw new ApplicationException("List is not specified.");

 

    if (string.IsNullOrEmpty(folderPath))

        throw new ApplicationException("Folder Path is not specified.");

 

    SPFolder folder = GetFolder(list, folderPath);

    return folder.Exists;

}

 

/// <summary>

/// Returns a folder from a list.  If folder does not exist, the Exist property will be FALSE. 

/// </summary>

/// <param name="list">The SharePoint list</param>

/// <param name="folderPath">Folder structure: eg: 2008/11/Sales</param>

/// <returns></returns>

public static SPFolder GetFolder(SPList list, string folderPath)

{

    if (list == null)

        throw new ApplicationException("List is not specified.");

 

    if (string.IsNullOrEmpty(folderPath))

        throw new ApplicationException("Folder Path is not specified.");

 

    return list.ParentWeb.GetFolder(list.RootFolder.Url + "/" + folderPath);

}



To follow on from these examples. Another option is to take advantage of .NET extensions and make these methods extensions of the SPList class (change the method signatures as follows):

public static void CreateFolder(this SPList list, string folderPath)

public static bool DoesFolderExist(this SPList list, string folderPath)

public static SPFolder GetFolder(this SPList list, string folderPath)

Start of Blog

Figured I should finally move into the naughties (2000's) and create a blog of my adventures in SharePoint. Stay tuned for some exciting posts and interesting readings...