El Blanco's Office 2007 Blog

Wednesday, July 29, 2009

Update: ItemCheckingIn Event and Detecting a Major Document Version

In my last post I detailed a problem where within the ItemCheckingIn event receiver I couldn't determine whether the document was being checked in as a major or a minor version and this was causing me big problems. I posted on the MSDN forums and Aaron Han from Microsoft very kindly investigated the issue in detail and came up with a solution.

The solution is only for those of us running Service Pack 2, but I am running it so it's not an issue for me. If you're hitting this same issue, then here's a reason to update !

The solution revolves around the "vti_level" property that is available in the BeforeProperties and AfterProperties collections. Here's the breakdown of what this contains when checking in a major version:

Without SP2

  • properties.BeforeProperties["vti_level"] (value is 255)
  • properties.AfterProperties["vti_level"] (value is 255)

With SP2

  • properties.BeforeProperties["vti_level"] (value is 255)
  • properties.AfterProperties["vti_level"] (value is 1)

Thanks to SP2 we can now write code such as the following within our ItemCheckingIn event receiver that can detect whether the item being checked in is a major version:

public override void ItemCheckingIn(SPItemEventProperties properties)
{
    base.ItemCheckingIn(properties);

    int beforeLevel = (int)properties.BeforeProperties["vti_level"];
    int afterLevel= (int)properties.AfterProperties ["vti_level"];

    if((beforeLevel==255) && (afterLevel==1))
    {
        // This is a major version that is being checked in . . .
        int majorVer= properties.ListItem.File.MajorVersion+1;
    }
}

Hopefully this is helpful as I can't believe I'm the only one who's ever needed this information. Thanks to Aaron Han from Microsoft !!

Monday, July 20, 2009

Interrogate the Document Version within the ItemCheckingIn Event Receiver?

I've added an event receiver to a content type, and I've overridden the ItemCheckingIn event. My scenario is that if the document is being checked in as a Major version (i.e. x dot zero) then unless it fulfils some other criteria I want to prevent the check in from occurring (i.e. set the properties.Cancel property to true, and set the properties.ErrorMessage property to some message indicate that the user must perform some action or other before a major version can be checked in).

So my scenario is that in the ItemCheckingIn event receiver method I want to interrogate whether the document is being checked in as a minor or a major version - sounds simple enough, right?

I first of all checked the following property within my event receiver:

properties.ListItem.File.UIVersionLabel

But I soon realised that since ItemCheckingIn is the synchronous event receiver method, then the item hasn't actually been checked in yet, so I won't have the new version number applied yet. So I changed this to check the following within my event receiver:

properties.AfterProperties["vti_sourcecontrolversion"]

However this still doesn't work – when I check in a major version, it just gives me the minor version number. If I check this property afterwards in the ItemCheckedIn event then I DO get the correct major version number, but obviously this is too late to cancel the check in operation since ItemCheckedIn is the asynchronous event receiver and it's not possible to prevent the operation in this event receiver method.

Has anyone come across this problem before – in the ItemCheckingIn event receiver method how can I tell what version of the document is being checked in i.e. whether it's a major or a minor version?!

Tuesday, May 19, 2009

ASP.NET Unit Testing

Unit Testing ASP.NET? ASP.NET unit testing has never been this easy.

Typemock is launching a new product for ASP.NET developers – the ASP.NET Bundle - and for the launch will be giving out FREE licenses to bloggers and their readers.

The ASP.NET Bundle is the ultimate ASP.NET unit testing solution, and offers both Typemock Isolator, a unit test tool and Ivonna, the Isolator add-on for ASP.NET unit testing, for a bargain price.

Typemock Isolator is a leading .NET unit testing tool (C# and VB.NET) for many ‘hard to test’ technologies such as SharePoint, ASP.NET, MVC, WCF, WPF, Silverlight and more. Note that for unit testing Silverlight there is an open source Isolator add-on called SilverUnit.

The first 60 bloggers who will blog this text in their blog and tell us about it, will get a Free Isolator ASP.NET Bundle license (Typemock Isolator + Ivonna). If you post this in an ASP.NET dedicated blog, you'll get a license automatically (even if more than 60 submit) during the first week of this announcement.

Also 8 bloggers will get an additional 2 licenses (each) to give away to their readers / friends.

Go ahead, click the following link for more information on how to get your free license.

Labels: ,

Friday, October 31, 2008

The BlancoWorld Workflow Reporting Feature

It's been a while since my last blog post because I've been extremely busy in SharePoint land with my job and finding spare time to write blog posts or work on useful add-ons has been very hard indeed.

However, over the last few weeks in the spare few minutes I manage to grab here and there I've been working on another add-on that hopefully some people will find useful. A fair few clients I work with have asked the question "how can I report on the workflows that are running or have completed in my site collection ?". The simple answer, as far as I'm aware, is that out of the box, you can't. This has led me to develop my lastest add-on – the BlancoWorld Workflow Reporting Feature. This feature is packaged, as you would expect, as a SharePoint solution file (.wsp) which you can download from here.

Once you had deployed the solution you need to activate a site collection feature appropriately called "BlancoWorld Workflow Reporting". Once this feature has been activated then anyone with the "Manage Lists" right will see a new item in their Site Actions menu called "Workflow Reporting" as seen below:

Selecting this menu option will display the following page:

Using this page you can determine at what level you wish to run the report. There are three options:

  • This Site Only – selecting this option reports on workflows running only in the current site.

  • This Site and Child Sites – selecting this option reports on any workflows running in the current site and any workflows running in any site beneath the current site. This is a recursive option that will interrogate every site in the site collection that resides beneath the current site.
  • Entire Site Collection – as the name implies, selecting this option will report on any workflow within the whole site collection.

Once you have selected your desired option click on the "OK" button to proceed and the workflow report will be generated and displayed. In the example shown below I've chosen to report on workflows throughout the entire site collection:

The report that is generated contains the following columns:

  • Site – a hyperlink to the site where the workflow is located.
  • List – a hyperlink to the list or library where the workflow is located.
  • Workflow Association Name – the name of the workflow association.
  • List Item – a hyperlink to the list item on which the workflow is currently running or ran in the past.
  • Workflow State – the current state of the workflow.
  • Workflow Status – a hyperlink to the workflow status page for the workflow.

When you have finished interrogating your report click on the "Done" button.

Hopefully people will find this add-on useful. I've only tested this add-on in pretty basic scenarios and not in scenarios where there are hundreds / thousands of workflow instances in the site collection so I'd be interested to hear from peoples experiences in these scenarios. I imagine in these sort of scenarios the report may take a while to generate, so I'll consider wrapping the functionality in an SPLongOperation if this turns out to be the case.

If you have any issues with the add-on please drop me an email and I'll try and help as much as a I can. Also, if you'd like to see any other functionality included in the add-on then again please let me know and I'll see what I can do !!

Friday, July 04, 2008

MOSS Licensing / General Microsoft Licensing Questions ?

I don't know how many people know about this blog, I certainly didn't, but Lady Licensing's blog covers most things people ask about Microsoft Licensing.

She has a recent post on MOSS licensing which may answer a few questions for people.

Check it out to answer all of your MS licensing questions J


Technorati Tags:

Thursday, July 03, 2008

Adding a SharePoint Site Picker to a _layouts Page

I've come across this scenario of a few times . . . you're implementing an application page that sets up some configuration for a SharePoint application you're developing. As part of this configuration, you need your end-user to enter a site from the current site collection. You could just put a text box on your _layout page and let them enter the URL to the site, perhaps adding some server-side validation to ensure that the URL the user entered corresponds to an actual site. A more user friendly approach would be to repurpose the SharePoint site picker dialog box that no doubt you have seen in a few places throughout SharePoint configuration settings:

How can you go about doing this ? Well, it's not as hard as you might expect:
  1. Create your _layout page as you normally would, and add two server controls to the .aspx page: a standard ASP.NET text box called txtWebUrl and a standard ASP.NET button contol called btnBrowse.

  2. In the code behind of your _layout page override the OnPreRender method as shown below:

    protected override void OnPreRender(EventArgs e)
    {
        ScriptLink.Register(this.Page,
            "PickerTreeDialog.js", true);
        AddSitePickerControlClientScript();
        base.OnPreRender(e);
    }


  3. The OnPreRender method registers the "PickerTreeDialog" javascript file that ships with MOSS and contains the "LaunchPickerTreeDialog" function. OnPreRender also calls the "AddSitePickerControlClientScript" method that we need to define as shown below:

    private void AddSitePickerControlClientScript()
    {
    string script =
    "var lastSelectedSiteSmtPickerId = null; " +
    "function LaunchSitePicker() " +
    "{ " +
    "if (!document.getElementById) return; " +
    "var siteTextBox = document.getElementById('" +
    SPHttpUtility.EcmaScriptStringLiteralEncode(
    this.txtWebUrl.ClientID) +
    "'); " +
    "if (siteTextBox == null) return; " +
    "var serverUrl = '" +
    SPHttpUtility.EcmaScriptStringLiteralEncode(
    SPContext.Current.Web.ServerRelativeUrl) +
    "'; " +
    "var callback = function(results) " +
    "{ " +
    "if (results == null results[1] == null) return; " +
    "lastSelectedSiteSmtPickerId = results[0]; " +
    "siteTextBox.value = results[1]; " +
    "}; " +
    "LaunchPickerTreeDialog(" +
    "'CbqPickerSelectSiteTitle'," +
    "'CbqPickerSelectSiteText', " +
    "'websOnly','', " +
    "serverUrl, lastSelectedSiteSmtPickerId," +
    "'','','/_layouts/images/smt_icon.gif','', " +
    "callback); " +
    "}"
    ;

        this.Page.ClientScript.RegisterClientScriptBlock(
            typeof(TestPage),
            "LaunchSitePicker",
            script, true);
    }


  4. This method defines a JavaScript function called "LaunchSitePicker". The "LaunchSitePicker" function defines a JavaScript callback function which is used by the site picker dialog when the dialog is closed. In the callback function is code that sets the txtWebUrl text box to contain the URL of the selected site. The "LaunchSitePicker" function then calls the "LaunchPickerTreeDialog" function which is part of "PickerTreeDialog.js" i.e. the file that ships with MOSS that we linked to in the OnPreRender method above.

    The "AddSitePickerControlClientScript" then calls the RegisterClientScriptBlock page method to register the JavaScript function we defined.

  5. The final step is to add the following code in your Page_Load method:

    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            this.btnBrowse.Attributes.Add(
                "onClick", "LaunchSitePicker()");
        }
    }

  6. This simply adds on onClick attribute to the btnBrowse button so that the "LaunchSitePicker" JavaScript function we defined is called when the button is clicked.

  7. Following all of the above intructions should give you a _layout page that looks like the following:

  8. Clicking on the "Browse…" button will display the site picker allowing you to select the desired site in your site collection. When you close the site picker by clicking the "OK" button the URL of the selected site will be injected into the text box.

Hopefully this will allow you to develop more user-friendly _layout application pages for your applications.

Thursday, June 26, 2008

My Fist Developer Feature: The BlancoWorld Notification Framework

A Call for Beta Testers . . .

Have you ever had a list with a date/time column (e.g. a column called "Review Date") and you wanted some custom notification functionality to execute 'x' days prior to the date, on the actual date, or 'y' days after the date for each list item in the list?

Are you sick of developing SPJobDefinition classes that simply trawl lists within site collections and implement some custom functionality when a date on that list has expired?

Welcome to the BlancoWorld Notification Framework . . . .

Installing / Deploying the BlancoWorld Notification Framework

The BlancoWorld Notification Framework is installed as a SharePoint Solution (BlancoWorld.NotificationFramework.wsp) and once installed and deployed it presents itself as an additional link in the Central Administration site accessed via the "Application Management" tab:

Configuring the BlancoWorld Notification Framework

Clicking on the "Manage BlancoWorld notification framework" link displays a management page allowing you to specify the web applications and site collections for which the BlancoWorld Notification Framework is enabled and how often the functionality executes:

Using this form, select the appropriate Web Application, and select whether to enable or disable the functionality for each Site Collection in the selected Web Application. Finally specify how often the functionality should run for the selected Web Application (e.g. every 10 minutes) – the BlancoWorld Notification Framework runs as a timer job and hence can be scheduled to run every 'x' minutes for each Web Application.

Once the functionality has been enabled for a particular site collection then the link shown below will be displayed on the list settings page for each list within that site collection:

Clicking on the "Item notification settings" link from the list settings page for a particular list allows you to specify the notification settings for items in that list:

Tick the "Item Notification Functionality Enabled" checkbox to enable the BlancoWorld Notification Framework functionality on the list. You must then specify the following information:

  • The column on which to calculate whether or not to raise notifications – the "Column to Interrogate" drop-down list details all of the Date/Time columns defined on the list. Select the desired column on which to calculate whether or not to raise notifications.
  • The full assembly name and class name of the type implementing the BlancoWorld Notification Framework Event Handler (see below for details).
  • The Desired Notification Settings – a list can be configured for Pre-Notifications (i.e. notifications raised 'x' days before the date/time specified in the selected column); Notifications (i.e. notifications raised when the actual date/time specified in the selected column is exceeded); and Post-Notifications (i.e. notifications raised 'y' days after the date/time specified in the selected column). Post notifications can also be specified to be recurring i.e. raised EVERY 'y' days after the date/time specified in the selected column.

BlancoWorld Notification Framework Event Handler
What happens when a notification (a pre-notification, a notification, or a post-notification) is raised by the BlancoWorld Notification Framework? This totally depends on the assembly name and class name defined in the list settings above i.e. whatever you implement will happen! To define the functionality that is executed when a notification is raised, you need to develop a class that implements the BlancoWorld.NotificationFramework.INotificationFrameworkEventReceiver interface:
public interface INotificationFrameworkEventReceiver
{
    void ItemPreNotification(
        NotificationEventProperties properties);
    void ItemNotification(
        NotificationEventProperties properties);
    void ItemPostNotification(
        NotificationEventProperties properties);
}

This interface defines three methods: ItemPreNotification; ItemNotification; and ItemPostNotification. Define a class that inherits from this interface (by adding a reference to the "BlancoWorld.NotificationFramework.dll" in your project, and adding a using directive for the BlancoWorld.NotiticationFramework namespace). In each method, define your desired functionality – note that a useful parameter called properties is passed into each method containing information such as the web URL, site ID, SPListItem, List Item ID etc.). An example implementation can be seen below:

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
u
sing BlancoWorld.NotificationFramework;
using Microsoft.SharePoint;

namespace BlancoWorld.TestNotificationFramework
{
    public class TestEventHandler :
        INotificationFramworkEventReceiver
    {
        private const String TEST_OUTPUT_FILE =
            @"C:\Temp\TEST_NOTIFICATION_FRAMEWORK.txt";

        public void ItemPreNotification(
            NotificationEventProperties properties)
        {
            WriteToTestFile(String.Format(
            "ItemPreNotification - {0}",
            GeneratePropertyString(properties)));
        }

        public void ItemNotification(
            NotificationEventProperties properties)
        {
            WriteToTestFile(String.Format(
            "ItemNotification - {0}",
            GeneratePropertyString(properties)));
        }

        public void ItemPostNotification(
            NotificationEventProperties properties)
        {
            WriteToTestFile(String.Format(
            "ItemPostNotification - {0}",
            GeneratePropertyString(properties)));
        }

        private String GeneratePropertyString(
            NotificationEventProperties properties)
        {
            return String.Format(
            "WebUrl={0}, ListName={1}, ListItem={2}",
            properties.WebUrl,
            properties.ListTitle,
            properties.ListItem.Title);
        }

        private void WriteToTestFile(string message)
        {
            if (message.Length > 0)
            {
                StreamWriter sw = null;
                try
                {
                    sw = File.AppendText
                        (TEST_OUTPUT_FILE);
                    string traceLine =
                        String.Format("{0}{1}",
                        DateTime.Now.ToString(
                            "dd/MM/yyyy HH:mm:ss.ffff").PadRight(30),
                        message);
                        sw.WriteLine(traceLine);
                        sw.Flush();
                }
                catch (Exception)
                {
                }
                finally
                {
                    if (sw != null)
                        sw.Close();
                }
            }
        }
    }
}

The sample class defined above simply outputs information to a debugging file (C:\Temp\TEST_NOTIFICATION_FRAMEWORK.txt) whenever a notification is raised.

Build your class and then add your assembly to the GAC. When specifying the list settings (as detailed above) detail the full assembly name and class name e.g.

Class Name: BlancoWorld.TestNotificationFramework.TestEventHandler

Assembly Name: BlancoWorld.TestNotificationFramework, Version=1.0.0.0, Culture=neutral, PublicKeyToken=58a25bc3a52786ea

Conclusion
The BlancoWorld Notification Framework is a framework allowing you, the SharePoint developer, to worry about what happens regarding dates defined on list items in SharePoint lists without having to worry about implementing custom SPJobDefinitions. Simply follow the steps define below:

  1. Install the BlancoWorld Notification Framework.
  2. Configure the BlancoWorld Notification Framework via Central Administration for the desired Web Application and Site Collection.
  3. Define a class that implements the BlancoWorld.NotificationFramework.INotificationFrameworkEventReceiver interface and define the custom functionality YOU want to execute 'x' days before a date, when that date expires, or 'y' days after a date by implementing the ItemPreNotification; ItemNotification; and ItemPostNotification methods.
  4. Build your assembly, and add your assembly to the GAC.
  5. Configure the BlancoWorld Notification Framework for the desired list by configuring the list settings for that list and specifying the column, full assembly name, full class name, and the desired notification settings.
  6. Sit back and let the BlancoWorld Notification Framework do the rest.

I've tested this add-on to some extent, but would welcome the opportunity to open this up to the wider SharePoint audience. Eventually I'll add this to my CodePlex project (where the Event Receiver Manager lives) but for the time being I'd like a few beta-testers to try this out and let me have their feedback. If you're interested in beta testing, email me at chris at blancoworld dot co dot uk and I'll email you the SharePoint Solution for you to try out . . . I'll create another blog posting when this is released via CodePlex.