Implementing double click in Silverlight

January 8, 2012

Recently I faced with the problem, that double click is not supported by Silverlight 4. On the web I found several solutions how to implement it for individual controls (regarding Silverlight 5, read this), however I wished a more general solution.

Inspired by this default button example (another missing feature in SL 4), I’ve implemented the following proof of concept (PoC) solution. I’ve created an attached property (called DoubleClick) where you can configure a RoutedEventHandler for your controls. We will use  this property to return our RoutedEventHandler instance that should handle double click events.

  1. using System;
  2. using System.Windows;
  3. using System.Windows.Controls;
  4. using System.Collections.Generic;
  5. using System.Linq;
  6.  
  7. namespace DblClickTest
  8. {
  9.  
  10.     public static class DoubleClickService
  11.     {
  12.         // time limit between mouse up / click events to be handled as a double click
  13.         private static readonly int TimeLimit = 1000;
  14.  
  15.         // list to track earlier mouse up / click events and related controls + handlers
  16.         private static List<Tuple<UIElement, DateTime, RoutedEventHandler>> dict = new List<Tuple<UIElement, DateTime, RoutedEventHandler>>();
  17.  
  18.         public static DependencyProperty DoubleClickProperty = DependencyProperty.RegisterAttached("DoubleClick", typeof(RoutedEventHandler), typeof(DoubleClickService), new PropertyMetadata(null, DoubleClickChanged));
  19.  
  20.         private static void DoubleClickChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  21.         {
  22.             UIElement uiElement = d as UIElement;
  23.             RoutedEventHandler clickFunc = e.NewValue as RoutedEventHandler;
  24.             if (uiElement != null && clickFunc != null)
  25.             {
  26.                 Button button = uiElement as Button;
  27.                 if (button != null)
  28.                 {
  29.                     button.Click += (sender, arg) => HandleClick(sender, arg, clickFunc);                    
  30.                 }
  31.                 else
  32.                 {
  33.                     uiElement.MouseLeftButtonUp += (sender, arg) => HandleClick(sender, arg, clickFunc);
  34.                 }
  35.             }
  36.         }
  37.  
  38.         private static void HandleClick(object sender, RoutedEventArgs e, RoutedEventHandler handler)
  39.         {
  40.             UIElement uie = sender as UIElement;
  41.             // just do the cleanup: remove old entries            
  42.             dict = dict.Where(p => (p.Item2 > DateTime.Now.AddMilliseconds(-1 * TimeLimit))).ToList();
  43.             Tuple<UIElement, DateTime, RoutedEventHandler> item = dict.FirstOrDefault(p => (p.Item1 == sender));
  44.             if (item == null)
  45.             {
  46.                 dict.Add(new Tuple<UIElement, DateTime, RoutedEventHandler>(uie, DateTime.Now, handler));
  47.             }
  48.             else
  49.             {
  50.                 item.Item3.Invoke(sender, e);
  51.  
  52.             }
  53.         }
  54.  
  55.         public static RoutedEventHandler GetDoubleClick(UIElement obj)
  56.         {
  57.             return (RoutedEventHandler)obj.GetValue(DoubleClickProperty);
  58.         }
  59.  
  60.         public static void SetDoubleClick(DependencyObject obj, RoutedEventHandler clickFunc)
  61.         {
  62.             obj.SetValue(DoubleClickProperty, clickFunc);
  63.         }
  64.     }
  65. }

My static class is built around a generic List of Tuple objects, that is used to track UIElement instances and related click events and handlers.

In the DoubleClickChanged method I register my handler method. If the control is a Button, then I attach to the Click event, otherwise the MouseLeftButtonUp event is used. That is needed because the Button control handles the MouseLeftButtonDown /MouseLeftButtonUp  events internally and set these methods as handled, that means it does not forward these events forward.

The HandleClick handler method first remove outdated entries from the list (you can set the value of TimeLimit  to match your needs), then if the source control is not found in the list, it is registered with the current time and the specified event handler, otherwise the event is considered a double click, and the specified RoutedEventHandler is invoked, passing the original sender and RoutedEventArgs parameters.

Note: The referenced post above uses the MouseLeftButtonDown event. Based on my experience this event is not triggered for example on ListBox control, so I switched to the MouseLeftButtonUp event, that is a bit different from the standard Windows double click behavior.

The MainPage.xaml for the test application looks like this:

  1. <UserControl x:Class="DblClickTest.MainPage"
  2.     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.     xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  5.     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  6.     xmlns:ap="clr-namespace:DblClickTest"
  7.     mc:Ignorable="d"
  8.     d:DesignHeight="300" d:DesignWidth="400"
  9.     x:Name="Top">
  10.  
  11.     <StackPanel>
  12.         <TextBlock Text="Double click me!"
  13.                    ap:DoubleClickService.DoubleClick="{Binding ElementName=Top, Path=DblClickHandler}"/>
  14.         <TextBox Name="Output" />
  15.                    <Button x:Name="MyButton"
  16.                 Content="Or me!"
  17.                 ap:DoubleClickService.DoubleClick="{Binding ElementName=Top, Path=DblClickHandler}"/>
  18.         <ListBox ap:DoubleClickService.DoubleClick="{Binding ElementName=Top, Path=DblClickHandler}">
  19.             <ListBoxItem Content="Item 1"></ListBoxItem>
  20.             <ListBoxItem Content="Item 2"></ListBoxItem>
  21.         </ListBox>
  22.  
  23.     </StackPanel>
  24. </UserControl>

The related code-behind:

  1. using System;
  2. using System.Linq;
  3. using System.Net;
  4. using System.Windows;
  5. using System.Windows.Controls;
  6.  
  7. using System.ComponentModel;
  8.  
  9. namespace DblClickTest
  10. {
  11.     public partial class MainPage : UserControl
  12.     {
  13.         public MainPage()
  14.         {
  15.             InitializeComponent();
  16.         }
  17.        
  18.         public RoutedEventHandler DblClickHandler
  19.         {
  20.             get
  21.             {
  22.                 return new RoutedEventHandler(OnDblClick);
  23.             }
  24.         }
  25.  
  26.         private void OnDblClick(object sender, RoutedEventArgs e)
  27.         {
  28.             Output.Text = String.Format("Double click on {0}", sender);
  29.         }
  30.  
  31.     }
  32. }

The sample application in action, after double-clicking on the second list box item:

image

Note: In a real application you might have to add synchronization code for the static list object access and / or exception handling.

Synchronizing SharePoint tasks with Outlook including categories

December 30, 2011

Recently one of our customers requested a SharePoint task list that is synchronized with Outlook. That is usually quite easy, but in this case we should have provided a way to tag the tasks and group them based on this multi-value property on the Outlook side, similarly to the default Categories field feature.

As you may know, only a limited set of task properties are synchronized between SharePoint and Outlook, but details –  like what exactly these properties are and how the synchronization process works – are not very well documented.

To investigate the behavior I’ve created a test list called OLTasks based on the Tasks list template in SharePoint.

image

Then connected the list to Outlook.

image

I’ve started Fiddler and found that the Lists web service is used for synchronization. First, the GetList method is called.

image

Next, a GetListItemChangesSinceToken request is sent. This method is suggested by Microsoft for item synchronization. As one can read on MSDN:

“The most efficient way to synchronize third-party clients with Microsoft SharePoint Foundation 2010 is to download only those items that have changed since the last synchronization occurred. You can do this in SharePoint Foundation 2010 by calling the GetListItemChangesSinceToken Web method.”

The first request contains no token, and the response includes the entire list schema (not shown below). Since our task list contains no item, the ItemCount is zero.

image

Note, that the request above contains the Categories property, however, our Task content type has no such field.

image

I’ve created a new task item in the list to see how it is downloaded to Outlook.

image

image

Again, the GetListItemChangesSinceToken method was called, in this case there was a token in the request (changeToken is not shown below due to lack of space) and the value of ItemCount was 1 in response.

image 

After synchronization, the item appeared in Outlook:

image

image

I’ve modified the task description, and found that on the next synchronization the UpdateListItems method was called to upload changes to SharePoint.

image

Before and after calling the UpdateListItems method the GetListItemChangesSinceToken method was called to detect possible conflicts and synchronize back changes from server.

To provide the Categories field for our tasks, I’ve added the existing site column with the same name to the list.

image

image 

Set a test value for the field at the existing task,

image

and created a new one with other test values. Note, that I’ve specified two values in this case, separated by a comma.

image

As expected the values are synchronized down to Outlook, as shown in this view, grouped by the Categories field.

image

When opening Task 2, we found that our category values are not in the Master Category List.

image

We can resolve it – and add some color codes as well – by clicking New… on the dialog box.

image

After this configuration, Outlook handles our categories as known ones. Next, I’ve set a new category for the task in Outlook.

image

The Outlook view reflects the changes.

image

The UpdateListItems method uploads the changes to SharePoint.

image

And the updated values are displayed in our SharePoint list as well.

image

I hope this quick guide helps you to better understand the default synchronization process, and to utilize similar techniques in your applications.

Creating custom validation rules in our list field iterators

November 14, 2011

Last week we saw how to hide specific field controls or how to display them as read-only even on the edit form. In the current post I introduce a simple technique one can apply to create custom validation for SharePoint forms without InfoPath.

First, what are the main problems with the out of the box SharePoint form validation capability?

As written in the first post of this series about custom list field iterators, SharePoint 2010 made a large step to the right direction compared to its predecessor, WSS 3.0. One can define field level and form level validation rules using a syntax similar to the one used with calculated columns, based on values of other fields of the current item.

However, you can define only a single validation rule for your form having a single validation error message, and a single validation rule and error message for the fields. Furthermore, there is another serious limitation, namely one can validate only the basic field types (like text, date, numeric) and the same is true to the fields one can refer to in the validation rule.

It means, you are out of luck if you have to work with validation of lookup or user fields, managed metadata or even your custom field types.

In the validation rule you can refer to the fields of the current item, you cannot build validation rules based on field values of other, related items, custom user properties, not to mention accessing other systems, like CRM, 3rd party web services, etc.

Moreover, the validation error messages are static text, one can not built them up dynamically based on field values, for example.

I found that in real world applications customers need usually much more validation power.  All of these inflexibilities listed above lead me to create some kind of custom solution for validation. You can read another approach built on custom field types here, but in this post I illustrate the list field iterator based solution.

As you might know the BaseFieldControl class, the base class of all field controls implements the IValidator interface. In practice it means, you can use these field controls similar to standard ASP.NET validator controls.

But before working with them, we first have to get a reference to the field controls. So we extend our BaseListFieldIterator class – introduced in the previous part – with the GetFieldControlByName method. There might be multiple forms on the page, so just to be sure that the field control is within our list field iterator, we call the GetIteratorByFieldControl method and compare the ClientID values.

The SetValidationError method is to set the validation error for the specified field.

  1. protected BaseFieldControl GetFieldControlByName(String fieldNameToFind)
  2. {
  3.     foreach (Control control in _formContext.FieldControlCollection)
  4.     {
  5.         if (control is BaseFieldControl)
  6.         {
  7.             BaseFieldControl baseField = (BaseFieldControl)control;
  8.             String fieldName = baseField.FieldName;
  9.             if ((fieldName == fieldNameToFind) &&
  10.                 (GetIteratorByFieldControl(baseField).ClientID == ClientID))
  11.             {
  12.                 return baseField;
  13.             }
  14.         }
  15.     }
  16.     return null;
  17. }
  18.  
  19. protected Microsoft.SharePoint.WebControls.ListFieldIterator GetIteratorByFieldControl(BaseFieldControl fieldControl)
  20. {
  21.     return (Microsoft.SharePoint.WebControls.ListFieldIterator)fieldControl.Parent.Parent.Parent.Parent.Parent;
  22. }
  23.  
  24. protected void SetValidationError(String fieldName, String errorMessage)
  25. {
  26.     BaseFieldControl fieldControl = GetFieldControlByName(fieldName);
  27.     fieldControl.ErrorMessage = errorMessage;
  28.     fieldControl.IsValid = false;
  29. }

Next we have to add the following code to our custom ListFieldIterator class.

  1. protected override void OnLoad(EventArgs e)
  2. {
  3.     if (Page.IsPostBack)
  4.     {
  5.         Page.Validate();
  6.         this.Validate();
  7.     }
  8. }
  9.  
  10. public void Validate()
  11. {
  12.     if (base.ControlMode != SPControlMode.Display)
  13.     {
  14.         // here comes the validation logic
  15.     }
  16. }

We will extend the Validate method to fulfill our fictitious business needs.

The validation messages are stored as constants:

  1. public static class ValidationMessages
  2. {
  3.     public static readonly String TitleNotUnique = "Task title should be unique";
  4.     public static readonly String UserBusy = "This user already has another task for this time";
  5.     public static readonly String NoDateForAssignement = "A user can be assigned only if the start and due dates are specified";
  6.     public static readonly String PredecessorsNotCompleted = "At least one of the predecessors is not yet completed";
  7.     public static readonly String PredecessorSelfReference = "A task cannot be its own predecessor";
  8.     public static readonly String DueDateEarlierThanStartDate = "Due date should not be earlier than start date ";
  9.     public static readonly String CompletedTaskPercentage = "For a completed task the % complete should be 100";
  10. }

Despite of the constant values used in the sample, it is easy to create dynamic messages as well. For example, you can define a message like "Task title ‘{0}’ is not unique", then substitute the current value when setting the validation error.

The first rule is to allow only unique task titles.

  1. TextField titleField = GetFieldControlByName(SampleTaskListFields.Title) as TextField;
  2.  
  3. if (titleField != null)
  4. {
  5.     String title = titleField.Value as String;
  6.  
  7.     if (!String.IsNullOrEmpty(title))
  8.     {
  9.         if (ItemWithSameTitleExists(title))
  10.         {
  11.             SetValidationError(SampleTaskListFields.Title, ValidationMessages.TitleNotUnique);
  12.         }
  13.     }
  14. }

We check the uniqueness through a CAML query, and use the GetFieldRefs helper method to build the value of the ViewFields property of the SPQuery instance.

  1. protected bool ItemWithSameTitleExists(string title)
  2. {
  3.  
  4.     SPQuery query = new SPQuery();
  5.  
  6.     query.ViewFields = GetFieldRefs(SampleTaskListFields.Title);
  7.     // we should not check the ID in this case, as altering the title is not allowed
  8.     // title will be validated only on new item creation
  9.     // if title would be editable for existing items, then we should check
  10.     // whether the ID is not the one of the current (edited) item
  11.     // note, that CAML Eq for a Text field type is case insensitive
  12.     query.Query = String.Format("<Where><Eq><FieldRef Name='{0}'/><Value Type='Text'>{1}</Value></Eq></Where>",
  13.         SampleTaskListFields.Title, title);
  14.  
  15.     bool result = List.GetItems(query).Count > 0;
  16.  
  17.     return result;
  18. }
  19.  
  20. private String GetFieldRefs(params String[] fieldNames)
  21. {
  22.     String fieldRefs = String.Concat(fieldNames.ToList().
  23.         ConvertAll(fieldName => String.Format("<FieldRef Name='{0}'/>", fieldName)));
  24.  
  25.     return fieldRefs;
  26. }

The next image shows the validator in action.

image

The comparison is case insensitive.

image

Yes, you are right it is nothing more than a custom implementation for the built-in Enforce unique values feature, but this one does not require indexed columns, and you have the chance to handle issues like leading / trailing spaces. For example, if you enable Enforce unique values, it won’t prohibit users entering ‘ Sample task 1  ‘ without any difficulties. If you don’t like this, you can trim the title parameter value and/or use Contains instead of Eq in the CAML query.

After this basic validation, let’s see a more tricky one. This one can be solved using standard field validation, although in this case it is more complicated “thanks” to the read-only Start date field when the task is not in the “Not Started” status.

  1. DateTimeField startDateField = GetFieldControlByName(SampleTaskListFields.StartDate) as DateTimeField;
  2. DateTimeField dueDateField = GetFieldControlByName(SampleTaskListFields.DueDate) as DateTimeField;
  3.  
  4. // if the task is not in 'Not Started' status, then we display the start date
  5. // as read-only and value would be null
  6. // in this case we should get the real value, stored in the item
  7. DateTime? startDateValue = (startDateField.ControlMode == SPControlMode.Display) ? (DateTime?)Item[SampleTaskListFields.StartDate] : startDateField.Value as DateTime?;
  8. DateTime? dueDateValue = dueDateField.Value as DateTime?;
  9.  
  10. if ((startDateValue.HasValue) && (dueDateValue.HasValue))
  11. {
  12.     if (dueDateValue.Value < startDateValue.Value)
  13.     {
  14.         SetValidationError(SampleTaskListFields.DueDate, ValidationMessages.DueDateEarlierThanStartDate);
  15.     }
  16. }

The illustration of the validation error:

image

This validation is again a simple one. We require 100% for % Complete when Status is Completed.

  1. DropDownChoiceField statusField = GetFieldControlByName(SampleTaskListFields.Status) as DropDownChoiceField;
  2. NumberField percComplField = GetFieldControlByName(SampleTaskListFields.PercentComplete) as NumberField;
  3.  
  4. bool checkForPredecessorStatus = false;
  5.  
  6. if ((statusField != null) && (percComplField != null))
  7. {
  8.     String statusFieldValue = statusField.Value as String;
  9.     Double? percComplValue = percComplField.Value as Double?;
  10.  
  11.     if (statusFieldValue == TaskStates.Completed)
  12.     {
  13.         checkForPredecessorStatus = true;
  14.         if ((!percComplValue.HasValue) || (percComplValue.Value != 1))
  15.         {
  16.             SetValidationError(SampleTaskListFields.PercentComplete, ValidationMessages.CompletedTaskPercentage);
  17.         }
  18.     }
  19. }

The screenshot of the validation:

image

Before you leave the post unread thinking this technique provides no additional value for validation, let’s see some more advanced example.

Using the standard Tasks list you can set an existing task as its own predecessor. That is not very nice, and in this case I found no simple OOB SharePoint tool to prohibit that.

Using this technique, you can achieve the result as shown below:

  1. MultipleLookupField predecessorsField = GetFieldControlByName(SampleTaskListFields.Predecessors) as MultipleLookupField;
  2.  
  3. if (predecessorsField != null)
  4. {
  5.     SPFieldLookupValueCollection predecessorsValue = predecessorsField.Value as SPFieldLookupValueCollection;
  6.  
  7.     if (predecessorsValue != null)
  8.     {
  9.         // a task can reference itself only in edit mode
  10.         if (ControlMode == SPControlMode.Edit)
  11.         {
  12.             if (!predecessorsValue.TrueForAll(predecessor => predecessor.LookupId != ItemId))
  13.             {
  14.                 SetValidationError(SampleTaskListFields.Predecessors, ValidationMessages.PredecessorSelfReference);
  15.             }
  16.         }
  17.         if (checkForPredecessorStatus)
  18.         {
  19.             if (!IsAllTasksCompleted(predecessorsValue.ConvertAll(predecessor => predecessor.LookupId)))
  20.             {
  21.                 SetValidationError(SampleTaskListFields.Status, ValidationMessages.PredecessorsNotCompleted);
  22.             }
  23.         }
  24.     }
  25. }

And in action:

image

The code above contains a check of predecessors when the task is to be set Completed. (The value of checkForPredecessorStatus computed earlier when validating % Complete.) We don’t want to allow that if there is at least on uncompleted predecessor. For the check we use the IsAllTasksCompleted method, passing the task ID of all of the predecessors as parameter.

  1. private bool IsAllTasksCompleted(List<int> taskIds)
  2. {
  3.     SPQuery query = new SPQuery();
  4.     query.ViewFields = GetFieldRefs(
  5.         SampleTaskListFields.Id,
  6.         SampleTaskListFields.Status);
  7.  
  8.     query.Query = String.Format("<Where><Neq><FieldRef Name='{0}'/><Value Type='Text'>{1}</Value></Neq></Where>",
  9.                     SampleTaskListFields.Status, TaskStates.Completed);
  10.  
  11.     bool result = true;
  12.     
  13.     SPListItemCollection tasksNotCompleted = List.GetItems(query);
  14.  
  15.     foreach (SPListItem task in tasksNotCompleted)
  16.     {
  17.         if (taskIds.Contains((int)task[SampleTaskListFields.Id]))
  18.         {
  19.             result = false;
  20.             break;
  21.         }
  22.     }
  23.  
  24.     return result;
  25. }

The result of the validation is shown here:

image

Next, we want to allow to assign a user to the task if both start and due dates are set.

  1. UserField userField = GetFieldControlByName(SampleTaskListFields.AssignedTo) as UserField;
  2.  
  3. if (userField != null)
  4. {
  5.     String userFieldValue = userField.Value as String;
  6.  
  7.     if (!String.IsNullOrEmpty(userFieldValue))
  8.     {
  9.  
  10.         if ((startDateValue.HasValue) && (dueDateValue.HasValue))
  11.         {
  12.             if (startDateValue.Value <= dueDateValue.Value)
  13.             {
  14.                 SPFieldUserValue userValue = new SPFieldUserValue(Web, userFieldValue);
  15.                 int? taskId = (base.ControlMode == SPControlMode.Edit) ? ItemId : (int?)null;
  16.                 if ((userValue.LookupId != -1) && (UserIsBusy(userValue.LookupId, taskId, startDateValue.Value, dueDateValue.Value)))
  17.                 {
  18.                     SetValidationError(SampleTaskListFields.AssignedTo, ValidationMessages.UserBusy);
  19.                 }
  20.             }
  21.         }
  22.         else
  23.         {
  24.             SetValidationError(SampleTaskListFields.AssignedTo, ValidationMessages.NoDateForAssignement);
  25.         }
  26.     }
  27. }

On the screenshot below, I “forgot” to set the start date, that is not allowed in this case.

image

Finally, using the method described in this post, we allow to assign the user for the task, if (s)he has no other assignment for that time interval. In this case we should ignore the task being edited when doing the validation.

  1. protected bool UserIsBusy(int userId, int? taskId, DateTime startDate, DateTime dueDate)
  2. {
  3.  
  4.     SPQuery query = new SPQuery();
  5.     query.ViewFields = GetFieldRefs(
  6.         SampleTaskListFields.Id,
  7.         SampleTaskListFields.AssignedTo,
  8.         SampleTaskListFields.StartDate,
  9.         SampleTaskListFields.DueDate);
  10.  
  11.     // NOTE: you can add filter for Status Neq 'Completed' as well if you wish
  12.     // I have not included that for the sake of simplicity
  13.     if (taskId.HasValue)
  14.     {
  15.         // it is editing an existing task, so we should exclude the task itself
  16.         query.Query = String.Format("<Where><And><And><Neq><FieldRef Name='{0}'/><Value Type='Integer'>{1}</Value></Neq><Eq><FieldRef Name='{2}' LookupId='TRUE' /><Value Type='Lookup'>{3}</Value></Eq></And>{4}</And></Where>",
  17.                         SampleTaskListFields.Id, taskId.Value,
  18.                         SampleTaskListFields.AssignedTo, userId,
  19.                         BuildDateRangeOverlapFilter(startDate, dueDate));
  20.     }
  21.     else
  22.     {
  23.         // it is a new task, we don't have to check te task ID
  24.         query.Query = String.Format("<Where><And><Eq><FieldRef Name='{0}' LookupId='TRUE' /><Value Type='Lookup'>{1}</Value></Eq>{2}</And></Where>",
  25.                         SampleTaskListFields.AssignedTo, userId,
  26.                         BuildDateRangeOverlapFilter(startDate, dueDate));
  27.     }
  28.  
  29.     bool result = List.GetItems(query).Count > 0;
  30.  
  31.     return result;
  32. }
  33.  
  34. protected String BuildDateRangeOverlapFilter(DateTime startDate, DateTime endDate)
  35. {
  36.  
  37.     StringBuilder sb = new StringBuilder();
  38.     sb.Append(String.Format("<And>{0}{1}</And>",
  39.         BuildSimpleDateFilter(SampleTaskListFields.StartDate, endDate, "Leq"),
  40.         BuildSimpleDateFilter(SampleTaskListFields.DueDate, startDate, "Geq")));
  41.  
  42.     return sb.ToString();
  43.  
  44. }
  45.  
  46. protected String BuildSimpleDateFilter(String dateFieldName, DateTime filterDate, String relation)
  47. {
  48.     String datePattern = "yyyy-MM-ddT00:00:00Z";
  49.  
  50.     return String.Format("<{0}><FieldRef Name='{1}'/><Value Type='DateTime'>{2}</Value></{0}>", relation, dateFieldName, filterDate.ToString(datePattern));
  51.  
  52. }

And that is the outcome of the validation in this case:

image

Note: Other validation possibilities for Person or Group field type are to check the number of users set (you can allow a single user or multiple ones using the standard tools, but you can not expect exactly three of them), or to check if the specified users are from a specific Active Directory group.

Although the technique illustrated in this post require coding and not so straightforward as specifying a simple formula on the UI, I hope you can use it  effectively if the customer requirements demand something more sophisticated.

You can download the sample application from here.

How to find overlapping items for a date range

November 11, 2011

It is a common requirement to find list items (like tasks) that have an overlapping duration with a specified data range.

Several years ago I wrote a post about the DateRangesOverlap CAML element and about its strange behavior returning items outside of the specified month range. As I suggested there, we can (and should) create the right query using basic CAML elements.

It is easy to create a complex CAML query to filter items for overlapping date ranges, however, there is a way to create a simple query as well. The simplest way and the best description in the topic is the one I found on stackoverflow. It says, the date ranges are overlapping when it is true for both of them, that their start date is no later than the end date of the other date range. That is very clear if you think about that just a bit.

Based on that concept I’ve created two helper methods:

  1. protected String BuildDateRangeOverlapFilter(DateTime startDate, DateTime endDate)
  2. {
  3.     StringBuilder sb = new StringBuilder();
  4.     sb.Append(String.Format("<Where><And>{0}{1}</And></Where>",
  5.         BuildSimpleDateFilter("StartDate", endDate, "Leq"),
  6.         BuildSimpleDateFilter("DueDate", startDate, "Geq")));
  7.  
  8.     return sb.ToString();
  9. }
  10.  
  11.  
  12. protected String BuildSimpleDateFilter(String dateFieldName, DateTime filterDate, String relation)
  13. {
  14.     String filter = String.Format("<{0}><FieldRef Name='{1}'/><Value Type='DateTime'>{2}</Value></{0}>",
  15.         relation, dateFieldName, SPUtility.CreateISO8601DateTimeFromSystemDateTime(filterDate));
  16.  
  17.     return filter;
  18. }

And here is an example about the usage:

  1. DateTime startDate = DateTime.Today.AddDays(11);
  2. DateTime endDate = DateTime.Today.AddDays(15);
  3.  
  4. SPList taskList = web.Lists["Tasks"];
  5.  
  6. SPQuery query = new SPQuery();
  7. query.ViewFields = "<FieldRef Name='Title'/><FieldRef Name='StartDate'/><FieldRef Name='DueDate'/>";
  8. query.Query = BuildDateRangeOverlapFilter(startDate, endDate);
  9.  
  10. SPListItemCollection matches = taskList.GetItems(query);
  11.  
  12. foreach (SPListItem match in matches)
  13. {
  14.     Console.WriteLine(match["Title"]);
  15. }

Note 1: The start date of the date ranges must be less than or equal to the end date, but I think that is an acceptable restriction.

Note 2: Before you ask it, I’ve not tested this solution with recurring events, but I assume it does not work in that case. Sorry!

Note 3: To convert my date stored as DateTime to String I used earlier the DateTime.ToString(String format) method with format pattern "yyyy-MM-ddT00:00:00Z", later I found that more-or-less matches the UniversalSortableDateTimePattern, so switched to parameter “u”. If you check the source code of the SPUtility.CreateISO8601DateTimeFromSystemDateTime method used in this sample (for example with Reflector), you can see that instead of simply calling DateTime.ToString with the right pattern, it takes the individual parts of the DateTime (like year, month, day, etc.), and creates the result by appending these component to each other using a StringBuilder. At first it was quite strange for me, but then thought, it must be the fastest way of conversion, as it does not have the overhead of pattern recognition and lot of conditions found in DateTime.ToString. However, if you should work with time zone information, then it might be not ideal for you. Although ISO 8601 supports time zones, AFAIS it is not implemented in CreateISO8601DateTimeFromSystemDateTime, for example, by providing an overload with a DateTimeOffset parameter. I don’t know if CAML supports at all this kind of time zone information provided in the date string. I admit it is not a crucial question most of the cases, however it might produce a surprise if not taken into account. You can read a bit more about that here.

Setting form field visibility and editability through custom list field iterators

November 9, 2011

In a recent post I showed you the basic of SharePoint list form customizations via custom list filed iterators.

In this post I provide a sample that can serve as a base for your form customization needs. From the sample you will learn how to alter the default display mode of the form fields, for example, hide fields on the edit or display form, or display fields as read-only on edit form. As a bonus, I will show you how to inject your code into the saving mechanism of list items and override the default behavior.

Before writing our own code, let’s see how the default field rendering is working. The following code snippet is the CreateChildControls method of the Microsoft.SharePoint.WebControls.ListFieldIterator class.

  1. protected override void CreateChildControls()
  2. {
  3.     this.Controls.Clear();
  4.     if (this.ControlTemplate == null)
  5.     {
  6.         throw new ArgumentException("Could not find ListFieldIterator control template.");
  7.     }
  8.     for (int i = 0; i < base.Fields.Count; i++)
  9.     {
  10.         SPField field = base.Fields[i];
  11.         if (!this.IsFieldExcluded(field))
  12.         {
  13.             TemplateContainer child = new TemplateContainer();
  14.             this.Controls.Add(child);
  15.             child.ControlMode = base.ControlMode;
  16.             child.FieldName = field.InternalName;
  17.             this.ControlTemplate.InstantiateIn(child);
  18.         }
  19.     }
  20. }

As you can see, hiding a field is quite straightforward, you should simply skip the field similarly to the IsFieldExcluded condition. In fact, the IsFieldExcluded property is a useful way to hide fields, but the method shown in the post hopefully provides a more flexible solution.

Displaying the field as read-only on the edit form would be possible through setting the ControlMode property of the TemplateContainer instance in the overriden version of CreateChildControls method.

Unfortunately, both ControlMode and FieldName properties are internal members of the TemplateContainer class, so there is no simple way to work with them in our custom CreateChildControls method.

That is where Reflection or dynamic methods come into the picture. In this case I chose the second one, and created the ILUtils helper class:

  1. public delegate void GenericSetter(object target, object value);
  2. public delegate object GenericGetter(object target);
  3.  
  4. class ILUtils
  5. {
  6.     ///
  7.     /// Creates a dynamic setter for the property
  8.     ///
  9.     public static GenericSetter CreateSetMethod(Type targetType, String propName)
  10.     {
  11.  
  12.         GenericSetter result = null;
  13.  
  14.         PropertyInfo propertyInfo = targetType.GetProperty(propName,
  15.             BindingFlags.NonPublic | BindingFlags.Instance);
  16.  
  17.         if (propertyInfo != null)
  18.         {
  19.             MethodInfo setMethod = propertyInfo.GetSetMethod(true);
  20.             if (setMethod != null)
  21.             {
  22.  
  23.                 Type[] arguments = new Type[2];
  24.                 arguments[0] = arguments[1] = typeof(object);
  25.  
  26.                 DynamicMethod setter = new DynamicMethod(
  27.                   String.Concat("_Set", propertyInfo.Name, "_"),
  28.                   typeof(void), arguments, propertyInfo.DeclaringType);
  29.                 ILGenerator generator = setter.GetILGenerator();
  30.                 generator.Emit(OpCodes.Ldarg_0);
  31.                 generator.Emit(OpCodes.Castclass, propertyInfo.DeclaringType);
  32.                 generator.Emit(OpCodes.Ldarg_1);
  33.  
  34.                 if (propertyInfo.PropertyType.IsClass)
  35.                     generator.Emit(OpCodes.Castclass, propertyInfo.PropertyType);
  36.                 else
  37.                     generator.Emit(OpCodes.Unbox_Any, propertyInfo.PropertyType);
  38.  
  39.                 generator.EmitCall(OpCodes.Callvirt, setMethod, null);
  40.                 generator.Emit(OpCodes.Ret);
  41.  
  42.                 result = (GenericSetter)setter.CreateDelegate(typeof(GenericSetter));
  43.             }
  44.         }
  45.         return result;
  46.     }
  47.  
  48.     ///
  49.     /// Creates a dynamic getter for the property
  50.     ///
  51.     public static GenericGetter CreateGetMethod(Type targetType, String propName)
  52.     {
  53.  
  54.         GenericGetter result = null;
  55.  
  56.         PropertyInfo propertyInfo = targetType.GetProperty(propName,
  57.             BindingFlags.NonPublic | BindingFlags.Instance);
  58.  
  59.         if (propertyInfo != null)
  60.         {
  61.             MethodInfo getMethod = propertyInfo.GetGetMethod(true);
  62.             if (getMethod != null)
  63.             {
  64.  
  65.                 Type[] arguments = new Type[1];
  66.                 arguments[0] = typeof(object);
  67.  
  68.                 DynamicMethod getter = new DynamicMethod(
  69.                   String.Concat("_Get", propertyInfo.Name, "_"),
  70.                   typeof(object), arguments, propertyInfo.DeclaringType);
  71.                 ILGenerator generator = getter.GetILGenerator();
  72.                 generator.DeclareLocal(typeof(object));
  73.                 generator.Emit(OpCodes.Ldarg_0);
  74.                 generator.Emit(OpCodes.Castclass, propertyInfo.DeclaringType);
  75.                 generator.EmitCall(OpCodes.Callvirt, getMethod, null);
  76.  
  77.                 if (!propertyInfo.PropertyType.IsClass)
  78.                     generator.Emit(OpCodes.Box, propertyInfo.PropertyType);
  79.  
  80.                 generator.Emit(OpCodes.Ret);
  81.  
  82.                 result = (GenericGetter)getter.CreateDelegate(typeof(GenericGetter));
  83.             }
  84.         }
  85.  
  86.         return result;
  87.  
  88.     }
  89.  
  90. }

Above idea and most of the code are borrowed from this great post.

We will store display rule exceptions using FieldDisplayRuleItem instances. FieldDisplayRule determines if we should hide the field or display it as read-only. FieldNames and ControlModes stores the field names and control modes the rule applies to. We will see this class in action soon.

  1. // helper class to store display exception rules
  2. // you can add your own properties to extend logic
  3. public class FieldDisplayRuleItem
  4. {
  5.     // list of field names the rule applies to
  6.     public List<String> FieldNames { get; set; }
  7.     // list of control modes the rule applies to
  8.     public List<SPControlMode> ControlModes { get; set; }
  9.     // the resulting display exeption rule
  10.     public FieldDisplayRule Rule { get; set; }
  11. }
  12.  
  13. public enum FieldDisplayRule
  14. {
  15.     Hidden,
  16.     Display
  17. }

Note: you can extend the FieldDisplayRuleItem class as your requirements dictate. For example if the items you work with have a Status field it is common to include this field in the rules. Of course, in this case you should alter the logic that selects the items to get the hidden / read-only fields as illustrated later.

Using the dynamic methods helper class it is easy to create a setter for the ControlMode and FieldName properties of the TemplateContainer class, that we can use in the overriden CreateChildControls method of our base class.

  1. public class BaseListFieldIterator : Microsoft.SharePoint.WebControls.ListFieldIterator
  2. {
  3.     // create dynamic setter methods that wrap the internal
  4.     // ControlMode and FieldName properties of the TemplateContainer class       
  5.     protected static GenericSetter set_TemplateContainer_ControlMode =
  6.             ILUtils.CreateSetMethod(typeof(TemplateContainer), "ControlMode");
  7.     protected static GenericSetter set_TemplateContainer_FieldName =
  8.         ILUtils.CreateSetMethod(typeof(TemplateContainer), "FieldName");
  9.  
  10.     protected List<FieldDisplayRuleItem> _dynamicRules = new List<FieldDisplayRuleItem>();
  11.  
  12.     // get references for the frequently used objects
  13.     protected SPSite _site = SPContext.Current.Site;
  14.     protected SPWeb _web = SPContext.Current.Web;
  15.     protected SPContext _context = SPContext.Current;
  16.     protected SPFormContext _formContext = SPContext.Current.FormContext;
  17.  
  18.     protected override void CreateChildControls()
  19.     {
  20.         this.Controls.Clear();
  21.         if (this.ControlTemplate == null)
  22.         {
  23.             throw new ArgumentException("Could not find ListFieldIterator control template.");
  24.         }
  25.  
  26.         for (int i = 0; i < base.Fields.Count; i++)
  27.         {
  28.             SPField field = base.Fields[i];
  29.             String fieldName = field.InternalName;
  30.  
  31.             // check if the current field is on the list of "hidden" fields in the current display mode
  32.             // or whether there is a "global" rule to hide fields
  33.             FieldDisplayRuleItem exception = _dynamicRules.FirstOrDefault(
  34.                 // empty (null) value means there is no restriction for the control mode
  35.                 e => ((e.ControlModes == null) || (e.ControlModes.Contains(ControlMode))) &&
  36.                 // empty (null) value means there is no restriction for the field name
  37.                 ((e.FieldNames == null) || (e.FieldNames.Contains(fieldName))) &&
  38.                 (e.Rule == FieldDisplayRule.Hidden));
  39.  
  40.             if ((!this.IsFieldExcluded(field)) && (exception == null))
  41.             {
  42.                 TemplateContainer child = new TemplateContainer();
  43.                 this.Controls.Add(child);
  44.                 SPControlMode controlMode = GetControlMode(fieldName);
  45.                 // use the dynamic setter to access internal properties
  46.                 set_TemplateContainer_ControlMode(child, controlMode);
  47.                 set_TemplateContainer_FieldName(child, fieldName);
  48.                 this.ControlTemplate.InstantiateIn(child);
  49.             }
  50.         }
  51.     }
  52.  
  53.     private SPControlMode GetControlMode(string fieldName)
  54.     {
  55.  
  56.         FieldDisplayRuleItem rule = _dynamicRules.FirstOrDefault(
  57.             e => ((e.ControlModes == null) || (e.ControlModes.Contains(ControlMode))) &&
  58.             ((e.FieldNames == null) || (e.FieldNames.Contains(fieldName))) &&
  59.             (e.Rule == FieldDisplayRule.Display));
  60.  
  61.         SPControlMode result = (rule == null) ? ControlMode : SPControlMode.Display;
  62.  
  63.         return result;
  64.     }
  65.  
  66. }

Instead of using string literals in my methods, I usually use static classes to store string constants like field or list names:

  1. public static class SampleTaskListFields
  2. {
  3.     public static readonly String Id = "ID";
  4.     public static readonly String Title = "Title";
  5.     public static readonly String PercentComplete = "PercentComplete";
  6.     public static readonly String AssignedTo = "AssignedTo";
  7.     public static readonly String Status = "Status";
  8.     public static readonly String Predecessors = "Predecessors";
  9.     public static readonly String StartDate = "StartDate";
  10.     public static readonly String DueDate = "DueDate";
  11. }
  12.  
  13. public static class Lists
  14. {
  15.     public static readonly String SampleList = "SampleTaskList";
  16. }
  17.  
  18.  
  19. public static class TaskStates
  20. {
  21.     // we only declare the state names used in the sample
  22.     public static readonly String NotStarted = "Not Started";
  23.     public static readonly String Completed = "Completed";
  24. }

If you prefer working with Guids then you may consider using the SPBuiltInFieldId class instead.

The exact implementation is derived from the BaseListFieldIterator class. We store static (global) rules in _staticRules and dynamic rules in _dynamicRules.

In the current example we are working with a task list and would like to achieve the following results:

Static rules:

  • The Title field can be edited only when the task is created.
  • Status and % Complete fields are hidden on task creation (we will set this values from our custom save handler method later).

Dynamic rules:

  • Predecessors field is only editable by site administrators.
  • Start Date is only editable if the item is in the Not Started status.

The following code creates the rules for these requirements.

  1. public class ListFieldIterator : BaseListFieldIterator
  2. {
  3.  
  4.     protected static List<FieldDisplayRuleItem> _staticRules = new List<FieldDisplayRuleItem>();
  5.  
  6.     static ListFieldIterator()
  7.     {
  8.  
  9.         // task title can be set on item creation
  10.         // but can't be altered later
  11.         _staticRules.Add(
  12.              new FieldDisplayRuleItem
  13.              {
  14.                  FieldNames = new List<String>() { SampleTaskListFields.Title },
  15.                  ControlModes = new List<SPControlMode>() { SPControlMode.Edit },
  16.                  Rule = FieldDisplayRule.Display
  17.              });
  18.  
  19.         // we don't allow to set status and % info on new item form
  20.         // items will be created with status "Not Started" and 0 %
  21.         // these values are set on item saving from code
  22.         _staticRules.Add(
  23.              new FieldDisplayRuleItem
  24.              {
  25.                  FieldNames = new List<String>() { SampleTaskListFields.Status, SampleTaskListFields.PercentComplete },
  26.                  ControlModes = new List<SPControlMode>() { SPControlMode.New },
  27.                  Rule = FieldDisplayRule.Hidden
  28.              });
  29.     }
  30.  
  31.  
  32.     protected override void OnInit(EventArgs e)
  33.     {
  34.         base.OnInit(e);
  35.  
  36.         // register save handler if not in display mode and form is posted back
  37.         if ((Page.IsPostBack) && (ControlMode != SPControlMode.Display))
  38.         {
  39.             _formContext.OnSaveHandler += new EventHandler(SaveHandler);
  40.         }
  41.  
  42.         CreateDynamicExceptions();
  43.  
  44.     }
  45.  
  46.     private void CreateDynamicExceptions()
  47.     {
  48.         _dynamicRules.AddRange(_staticRules);
  49.  
  50.         // only site admins are allowed to edit predecessor tasks
  51.         // others can see only the predecessors regardless of the control mode
  52.         if (!_web.CurrentUser.IsSiteAdmin)
  53.         {
  54.             _dynamicRules.Add(
  55.                 new FieldDisplayRuleItem
  56.                 {
  57.                     FieldNames = new List<String>() { SampleTaskListFields.Predecessors },
  58.                     Rule = FieldDisplayRule.Display
  59.                 });
  60.         }
  61.  
  62.         // start date can be set only if the task is not started
  63.         if ((String)Item[SampleTaskListFields.Status] != TaskStates.NotStarted)
  64.         {
  65.             _dynamicRules.Add(
  66.                 new FieldDisplayRuleItem
  67.                 {
  68.                     FieldNames = new List<String>() { SampleTaskListFields.StartDate },
  69.                     Rule = FieldDisplayRule.Display
  70.                 });
  71.         }
  72.  
  73.     }
  74.  
  75.     protected void SaveHandler(object sender, EventArgs e)
  76.     {
  77.         Page.Validate();
  78.  
  79.         if (Page.IsValid)
  80.         {
  81.             // do custom activities, send mail, create task,
  82.             // set permissions etc.
  83.  
  84.             // new tasks are created with status 'Not Started' and 0% complete
  85.             if (ControlMode == SPControlMode.New)
  86.             {
  87.                 Item[SampleTaskListFields.PercentComplete] = 0;
  88.                 Item[SampleTaskListFields.Status] = TaskStates.NotStarted;
  89.             }
  90.  
  91.             // we should save the item explicitly
  92.             Item.Update();
  93.         }
  94.     }
  95. }

In the OnInit method we register of custom save handler method. In the SaveHandler method we set the values for the Status and % Complete fields if the item is a new task. That is the method you can include your custom actions into, like sending mails or setting permissions on items.

To test the solution, we should first deploy the assembly as well as our user control. Then create a Tasks list called SampleTaskList, and register our custom ListFieldIterator to all of its forms as shown in my former post.

image

Logged on as a site admin, we create a task item.

image

Although the Status and % Complete fields are not visible on the form, the values of the fields are populated.

image

Next, we create another task item, and set the first one as a predecessor.

image

After saving the item, we open it again in edit mode. The Title field displayed as read-only.

image

We log in as a standard site member (non-admin), and open the item once again. The Predecessors field is also read-only in this case. We set the Status to In Progress.

image

After re-opening the item we see that the Start Date field became read-only.

image 

I hope this example helps you to create SharePoint forms that fit better to the common customer needs. In the following posts I’m planning to build on this sample with other interesting features.

You can download the sample application from here.

Using SharePoint 2010 Word Automation Services to convert document synchronously

October 27, 2011

A few months ago a fellow developer asked me how to use WAS in an application that requires synchronous document conversion. In the current post I show you a simple way for that.

As you might know (if not you can read a bit more about that at the end of this post), you can submit Word documents to WAS and let it convert the documents to PDF or other formats like XPS. WAS works as a timer job, so conversion is done based on the schedule of the job that you should set based on the number of documents to be converted and the free resources of the server. As in the case of any timer job, you can start the Word Automation Services Timer Job immediately using the web UI and from custom code as well.

For the sample method I pass in the document content as a byte array and the converted document is returned by the method as a byte array as well. First I’ve implemented a Stream-based solution but found it is easier to work with byte arrays in this case (see reason a bit later).

After preparing and starting the ConversionJob, we start the WAS timer job if immediate conversion is requested, then wait until the conversion is finished either successfully or unsuccessfully or until the timeout interval elapsed. In case of timeout, we cancel the conversion process. Next we display possible conversion errors and delete the documents from the working document library if requested.

  1. private byte[] ConvertDocument(SPWeb web, byte[] docToConvert, bool isImmediate,
  2.     String conversionLibName, int timeOutSecs, bool deleteDocs)
  3. {
  4.  
  5.     byte[] result = null;
  6.     SPList conversionLib = web.Lists[conversionLibName];
  7.  
  8.     SPFolder folder = conversionLib.RootFolder;
  9.  
  10.     // Get the default proxy for the current Word Automation Services instance
  11.     SPServiceContext serviceContext = SPServiceContext.GetContext(web.Site);
  12.     WordServiceApplicationProxy wordServiceApplicationProxy =
  13.         (WordServiceApplicationProxy)serviceContext.GetDefaultProxy(typeof(WordServiceApplicationProxy));
  14.  
  15.     ConversionJob job = new ConversionJob(wordServiceApplicationProxy);
  16.     job.UserToken = web.CurrentUser.UserToken;
  17.     job.Settings.UpdateFields = true;
  18.     job.Settings.OutputSaveBehavior = SaveBehavior.AlwaysOverwrite;
  19.     job.Settings.OutputFormat = SaveFormat.PDF;
  20.  
  21.     String docFileName = Guid.NewGuid().ToString("D");
  22.  
  23.     // we replace possible existing files on upload
  24.     // although there is a minimal chance for GUID duplicates :-)
  25.     SPFile docFile = folder.Files.Add(docFileName + ".docx", docToConvert, true);
  26.     conversionLib.AddItem(docFileName + ".docx", SPFileSystemObjectType.File);
  27.  
  28.     String docFileUrl = String.Format("{0}/{1}", web.Url, docFile.Url);
  29.     String pdfFileUrl = String.Format("{0}/{1}.pdf",
  30.         web.Url, docFile.Url.Substring(0, docFile.Url.Length – 5));
  31.  
  32.     job.AddFile(docFileUrl, pdfFileUrl);
  33.  
  34.     // let's do the job :-)
  35.     // Start-SPTimerJob "Word Automation Services"
  36.     job.Start();
  37.  
  38.     if (isImmediate)
  39.     {
  40.         StartServiceJob("Word Automation Services Timer Job");
  41.     }
  42.  
  43.     ConversionJobStatus cjStatus = new ConversionJobStatus(wordServiceApplicationProxy, job.JobId, null);
  44.     // set up timeout
  45.     TimeSpan timeSpan = new TimeSpan(0, 0, timeOutSecs);
  46.     DateTime conversionStarted = DateTime.Now;
  47.  
  48.     int finishedConversionCount = cjStatus.Succeeded + cjStatus.Failed;
  49.     while ((finishedConversionCount != 1) && ((DateTime.Now – conversionStarted) < timeSpan))
  50.     {
  51.         // wait a sec.
  52.         Thread.Sleep(1000);
  53.         cjStatus = new ConversionJobStatus(wordServiceApplicationProxy, job.JobId, null);
  54.         finishedConversionCount = cjStatus.Succeeded + cjStatus.Failed;
  55.     }
  56.  
  57.     // timeouted -> cancel conversion
  58.     if (finishedConversionCount != 1)
  59.     {
  60.         job.Cancel();
  61.     }
  62.  
  63.     // we can output the possible failed conversion error(s)
  64.     foreach (ConversionItemInfo cii in cjStatus.GetItems(ItemTypes.Failed))
  65.     {
  66.         Console.WriteLine("Failed conversion. Input file: '{0}'; Output file: '{1}'; Error code: '{2}'; Error message: '{3}';",
  67.             cii.InputFile, cii.OutputFile, cii.ErrorCode, cii.ErrorMessage);
  68.     }
  69.  
  70.     SPFile convertedFile = web.GetFile(pdfFileUrl);
  71.     // shouldn't be null (unless there is a conversion error)
  72.     // but we check for sure
  73.     if ((convertedFile != null) && (convertedFile.Exists))
  74.     {
  75.         Stream pdfStream = convertedFile.OpenBinaryStream();
  76.  
  77.         result = new byte[pdfStream.Length];
  78.         pdfStream.Read(result, 0, result.Length);
  79.  
  80.         // delete result doc if requested
  81.         if (deleteDocs)
  82.         {
  83.             convertedFile.Delete();
  84.         }
  85.     }
  86.  
  87.     // delete source doc if requested
  88.     if (deleteDocs)
  89.     {
  90.         docFile.Delete();
  91.     }
  92.  
  93.     return result;
  94.  
  95. }
  96.  
  97. private void StartServiceJob(string serviceTypeName, string jobTypeName)
  98. {
  99.     SPFarm.Local.Services.ToList().ForEach(
  100.         svc => svc.JobDefinitions.ToList().ForEach(
  101.             jd =>
  102.             {
  103.                 if ((jd.TypeName == jobTypeName) && ((serviceTypeName == null) || (serviceTypeName == svc.TypeName)))
  104.                 {
  105.                     jd.RunNow();
  106.                 }
  107.             }));
  108. }

To start immediate conversion in the ConvertDocument method I used a slightly modified version of the StartServiceJob method already introduced in my former post.

  1. private void StartServiceJob(string serviceTypeName, string jobTypeName)
  2. {
  3.     SPFarm.Local.Services.ToList().ForEach(
  4.         svc => svc.JobDefinitions.ToList().ForEach(
  5.             jd =>
  6.             {
  7.                 if ((jd.TypeName == jobTypeName) && ((serviceTypeName == null) || (serviceTypeName == svc.TypeName)))
  8.                 {
  9.                     jd.RunNow();
  10.                 }
  11.             }));
  12. }
  13.  
  14. private void StartServiceJob(string jobTypeName)
  15. {
  16.     StartServiceJob(null, jobTypeName);
  17. }

The following code snippet shows a sample for calling the ConvertDocument method. In this case we request an immediate conversion with 240 seconds timeout and use the standard Shared Documents document library as a working folder, deleting the temporary files.

  1. DateTime startTime = DateTime.Now;
  2. byte[] doc = File.ReadAllBytes(@"C:\Data\HelloWorld.docx");
  3. byte[] pdf = ConvertDocument(web, doc, true, "Shared Documents", 240, true);
  4. if (pdf != null)
  5. {
  6.     File.WriteAllBytes(@"C:\Data\HelloWorld.pdf", pdf);
  7. }
  8. Console.WriteLine("Duration of conversion: {0} ms", (DateTime.Now – startTime).TotalMilliseconds);

The sample above requires further work if you would like to use it in a real application. First, you should add some extra error handling, for example check if default WordServiceApplicationProxy is found at all, etc.

Next, instead of submitting documents one by one to WAS it is better to create a ConvertDocument version that supports multiple document conversion. In this case you should use arrays of byte arrays that I found easier than bothering (like disposing through using blocks) with multiple streams simultaneously.

You can extend the supported conversion options to other formats as well, like XPS.

In a real life application you probably wouldn’t like to start immediate conversions on each requests because it might produce a heavy load on your servers. Instead you can create a specific queue for documents with the option for high privilege users to submit dedicated document types for immediate conversions and leave the default conversion schedule for the others.

Although our original goal was to create a synchronous conversion method, sometimes it is more comfortable to do the conversion asynchronously, for example to avoid locking of the UI thread. To support that in your application, you can start ConvertDocument in a separate thread and raise your custom .NET events to reflect the output of the conversion job.

Getting the process ID of the IIS / SharePoint application pool using PowerShell

October 24, 2011

It is easy to attach your debugger to the right SharePoint application pool using Visual Studio, especially if specific extensions like CKS.Dev or my VS extension is installed.

Sometimes life is not so trivial, you don’t have Visual Studio installed on the server and / or you should use other tools to debug your application. It would be great to have the right process ID in this case as well.

For example, assume you have to attach WinDbg or other debugging tools like Deblector to the process of the IIS application pool of a selected SharePoint application.

Two years ago I wrote a post about how to attach the VS debugger from a VS macro. In that post I’ve illustrated how to get the right process based on the application pool name (see the GetProcessIdByAppPoolName and GetAppPoolNameFromCommandLine methods in that post). In the post about my VS extension mentioned above you can find a similar solution as well.

In this post I show you two PowerShell methods that can be used when there is no Visual Studio on the computer or you simply do not want to work with that.

You should specify the name of the application pool for the Get-AppPoolProcessIdByName method and it displays the process ID of the matching application pool:

function Get-AppPoolProcessIdByName($name) 

     Get-WmiObject Win32_Process |
       Where-Object { $_.CommandLine -like "*w3wp.exe*" } |
       ForEach-Object { [regex]::Matches($_.CommandLine, "-ap ""(.+)"" -v") |
       Add-Member NoteProperty -Name ProcessId -Value $_.ProcessId -PassThru } |
       Where-Object { $_.Success -and $_.Groups.Count -gt 1 -and $_.Groups[1].Value -eq $name } |
       ForEach-Object { Write-Host $_.ProcessId }
}

Usage:

Get-AppPoolProcessIdByName("SharePoint – 80")

The Get-SPAppPoolProcessIdByUrl method first determines the corresponding SharePoint web application, then calls the Get-AppPoolProcessIdByName method to get the process ID.

function Get-SPAppPoolProcessIdByUrl($url) 

     $app = Get-SPWebApplication $url
     Get-AppPoolProcessIdByName($app.DisplayName)
}

Usage:

Get-SPAppPoolProcessIdByUrl("http://sp2010")

You can use Get-AppPoolProcessIdByName for arbitrary IIS application pool, but as you can see from the code (and as I intended to sign with the Get-SP prefix) the Get-SPAppPoolProcessIdByUrl method is SharePoint specific.

Creating a ToggleButton on SharePoint ribbon with alternating images

October 24, 2011

Although ToggleButton is a useful control on the ribbon of SharePoint 2010 user interface, unfortunately I found no built-in way to assign separate images to the on / off states of the button.

After playing awhile with the Internet Explorer Developer Tools (F12, see also the Tools menu in IE 8), I find out how to achieve that through the HTML object model built up by the ribbon infrastructure.

The following JavaScript code is from the command handler method of the custom ribbon components and assumes jQuery is already loaded.

Replace the commandId of the ToggleButton and the image properties (src and style attributes) to match your needs.

  1. if (commandId === 'Ribbon.Custom.List.MyFeature.SwitchOnOff')
  2.     var x = jQuery("a[id|='" + commandId + "']").find("img");
  3.  
  4.     if (properties.On) {
  5.         x.attr("src", "/_layouts/images/MyImages/On.png");
  6.         x.attr("style", "top: 0px; left: 0px;");
  7.     }
  8.     else {
  9.         x.attr("src", "/_layouts/images/MyImages/Off.png");
  10.         x.attr("style", "top: 0px; left: 0px;");
  11.     }

You can use separate image files, or a single one with different vertical / horizontal offset value.

I’ve defined a global switchedOn variable to track the actual status of the ToggleButton easier, and query that value from other JavaScript methods:

var switchedOn = false;

I hope this trick helps you to make your SharePoint UI a bit more dynamic.

How to deploy a custom field with custom properties from a feature – the missing second part

October 21, 2011

Only readers following my blog from the beginning may remember my post I wrote about the deployment of custom fields having custom properties more than three years ago. Of course that post is about WSS 3.0.

At that time I described how to include the custom properties into the field definition even if these properties are not declared by the XSD validation schema. However, injecting the custom properties the way I described there did not have effect on the values of the custom properties the custom field actually deployed with. I promised then that in the next part I would show you how to resolve this issue. It’s better late than never, the second part comes now, updated for SharePoint 2010.

The trick to achieve our goal seemed originally to be really simple. The base idea behind  it was that an instance of the field must be created sooner or later before usage, and the values we would like to push into the custom properties were available on object creation in the field schema deployed.

I’ve modified the constructors of the custom field, adding a call to my custom InitializeProps method.

  1. public SPFieldRegEx(SPFieldCollection fields, string fieldName)
  2.     : base(fields, fieldName)
  3. {
  4.     InitializeProps();
  5. }
  6.  
  7. public SPFieldRegEx(SPFieldCollection fields, string typeName, string displayName)
  8.     : base(fields, typeName, displayName)
  9. {
  10.     InitializeProps();
  11. }

The InitializeProps method is responsible for reading up the deployed custom property values from the field schema and setting them to the standard custom location in the schema.

  1. // in the custom field feature definition
  2. private readonly string customNamespaceUri = "http://schemas.grepton.com/sharepoint/";
  3.  
  4. private void InitializeProps()
  5. {
  6.     String regEx = (String)GetCustomProperty("RegEx");
  7.  
  8.     // value of custom property is null if not yet set
  9.     if (regEx == null)
  10.     {
  11.  
  12.         // load the field schema into an XML document
  13.         XmlDocument schemaXml = new XmlDocument();
  14.         schemaXml.LoadXml(SchemaXml);
  15.  
  16.         XmlNode fieldNode = schemaXml.SelectSingleNode("Field");
  17.  
  18.         if (fieldNode != null)
  19.         {
  20.             InitCustomProperty(fieldNode, "RegEx");
  21.             InitCustomProperty(fieldNode, "ErrMsg");
  22.             InitCustomProperty(fieldNode, "MaxLen");
  23.         }
  24.  
  25.     }
  26.  
  27. }
  28.  
  29. private void InitCustomProperty(XmlNode fieldNode, String custPropName)
  30. {
  31.     XmlAttribute custPropOrigAttr = fieldNode.Attributes[custPropName, customNamespaceUri];
  32.     // should not be null, but we check it
  33.     if (custPropOrigAttr != null)
  34.     {
  35.         SetCustomProperty(custPropName, custPropOrigAttr.Value);
  36.     }
  37. }

After deploying my custom field, I’ve checked my field at the Change Site Column page and was happy to see my custom property values there:

image

However, it turned out quickly that the schema of my field was not updated as expected:

  1. <Field
  2.   ID="{54634385-A8AC-4898-BF24-E533EB23444F}"
  3.   Name="RegExField"
  4.   DisplayName="RegExField"
  5.   StaticName="RegExField"
  6.   Group="Grepton Fields"
  7.   Type="SPFieldRegEx"
  8.   Sealed="FALSE"
  9.   AllowDeletion="TRUE"
  10.   SourceID="http://schemas.microsoft.com/sharepoint/v3/fields"
  11.   Description="This is the RegEx field"
  12.   grp:RegEx="[0-9]"
  13.   grp:MaxLen="20"
  14.   grp:ErrMsg="Error!"
  15.   xmlns:grp="http://schemas.grepton.com/sharepoint/" />

Of course, pressing OK on the Change Site Column page updated the field schema with the custom properties:

  1. <Field
  2.   ID="{54634385-A8AC-4898-BF24-E533EB23444F}"
  3.   Name="RegExField"
  4.   DisplayName="RegExField"
  5.   StaticName="RegExField"
  6.   Group="Grepton Fields"
  7.   Type="SPFieldRegEx"
  8.   Sealed="FALSE"
  9.   AllowDeletion="TRUE"
  10.   SourceID="http://schemas.microsoft.com/sharepoint/v3/fields"
  11.   Description="This is the RegEx field"
  12.   grp:RegEx="[0-9]"
  13.   grp:MaxLen="20"
  14.   grp:ErrMsg="Error!"
  15.   xmlns:grp="http://schemas.grepton.com/sharepoint/"
  16.   Required="FALSE"
  17.   EnforceUniqueValues="FALSE"
  18.   Version="1">
  19.   <Customization>
  20.     <ArrayOfProperty>
  21.       <Property>
  22.         <Name>RegEx</Name>
  23.         <Value
  24.           xmlns:q1="http://www.w3.org/2001/XMLSchema"
  25.           p4:type="q1:string"
  26.           xmlns:p4="http://www.w3.org/2001/XMLSchema-instance">[0-9]</Value>
  27.       </Property>
  28.       <Property>
  29.         <Name>MaxLen</Name>
  30.         <Value
  31.           xmlns:q2="http://www.w3.org/2001/XMLSchema"
  32.           p4:type="q2:double"
  33.           xmlns:p4="http://www.w3.org/2001/XMLSchema-instance">20</Value>
  34.       </Property>
  35.       <Property>
  36.         <Name>ErrMsg</Name>
  37.         <Value
  38.           xmlns:q3="http://www.w3.org/2001/XMLSchema"
  39.           p4:type="q3:string"
  40.           xmlns:p4="http://www.w3.org/2001/XMLSchema-instance">Error!</Value>
  41.       </Property>
  42.     </ArrayOfProperty>
  43.   </Customization>
  44. </Field>

That gave me the idea that I should try to deploy my custom field using this schema. I removed my custom attributes from the Field element and used the following XML to deploy the field.

  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  3.   <Field
  4.     ID="{54634385-A8AC-4898-BF24-E533EB23444F}"
  5.     Name="RegExField"
  6.     DisplayName="RegExField"
  7.     StaticName="RegExField"
  8.     Group="Grepton Fields"
  9.     Type="SPFieldRegEx"
  10.     Sealed="FALSE"
  11.     AllowDeletion="TRUE"
  12.     SourceID="http://schemas.microsoft.com/sharepoint/v3/fields"
  13.     Description="This is the RegEx field"
  14.     Version="1">
  15.     <Customization>
  16.       <ArrayOfProperty>
  17.         <Property>
  18.           <Name>RegEx</Name>
  19.           <Value
  20.             xmlns:q1="http://www.w3.org/2001/XMLSchema"
  21.             p4:type="q1:string"
  22.             xmlns:p4="http://www.w3.org/2001/XMLSchema-instance">[0-9]</Value>
  23.         </Property>
  24.         <Property>
  25.           <Name>MaxLen</Name>
  26.           <Value
  27.             xmlns:q2="http://www.w3.org/2001/XMLSchema"
  28.             p4:type="q2:double"
  29.             xmlns:p4="http://www.w3.org/2001/XMLSchema-instance">20</Value>
  30.         </Property>
  31.         <Property>
  32.           <Name>ErrMsg</Name>
  33.           <Value
  34.             xmlns:q3="http://www.w3.org/2001/XMLSchema"
  35.             p4:type="q3:string"
  36.             xmlns:p4="http://www.w3.org/2001/XMLSchema-instance">Error!</Value>
  37.         </Property>
  38.       </ArrayOfProperty>
  39.     </Customization>
  40.   </Field>
  41. </Elements>

I’ve removed my custom codes described above either, and tried to deploy the field.

Voilà! The field is deployed successfully, including the custom properties. At that point I really don’t understand if it would work for WSS 3.0 and if so, how I missed to find that solution three years ago. The main point it is working now and requires no hacking at all.

A mystical observation is that after deploying the field I was not able to get a reference for the new field using PowerShell:

$avFields = $web.AvailableFields
$avFields["RegExField"]

At the same time, the field is visible on the web UI, and the following C# code finds it:

SPFieldCollection avFields = web.AvailableFields;
SPField field = avFields["RegExField"];

If I create the field from code, PowerShell finds that either:

SPFieldCollection fields = web.Fields;
SPField fieldCode = new SPField(fields, "SPFieldRegEx", "RegExCode");
fields.Add(fieldCode);

If you know the reason for that, let me know, please!

Creating more advanced conditions for your ECB menus through jQuery and synchronous Client Object Model / WCF Data Services calls

October 16, 2011

In my last post I showed you how to alter the Edit Control Block (ECB) menu of SharePoint 2010 based on simple conditions. As I described, if you need to use more advanced conditions, for example based on list item field values or other information not available on the client side, you should apply a few tricks or even hacks.

For these advanced solutions it is useful to provide the context to our allowDeletion method through the ctx parameter, so we add this one to the parameter list of the method.

It means that our original condition in method AddListMenuItems is changed from

if (allowDeletion(currentItemID))

to

if (allowDeletion(ctx, currentItemID))

In the simplest case, the information is already there at the page on client side, you simply have to find the way to get it. Checking the HTML source of the page or using the Developer Tools in the Tools menu of Internet Explorer (you can press F12 as a shortcut key) usually helps you to find the right track.

For example, if you need the title of the item, that is included in the LinkTitle field (as we have an ECB menu linked to the title) for each items. When looking up the right item, we should use the ID of the item (passed as the itemID parameter) and the ID of our list view (available in ctx.ctxId).

Note: Although it should be evident, your page must load the jQuery library before you can use jQuery methods. Similarly, the ECMAScript Client Object Model library must be loaded before you reference its objects in methods later in the posts.

So if we would like to allow deletion only for items having title beginning with ‘V’ then we need to apply a method like this:

  1. function allowDeletion(ctx, itemID) {
  2.  
  3.     var title = jQuery('div[Field="LinkTitle"][CTXName="ctx' + ctx.ctxId + '"][id="' + itemID + '"]').text();
  4.  
  5.     return (title.toUpperCase().startsWith("V"));
  6.  
  7. }

Next step is to see how to get the information if it is not available on the client side, but included in the list items on the server side. My first idea was to use the ECMAScript Client Object Model, however it turned out quickly that is not the best choice, as the AddListMenuItems method does not wait for the asynchronous reply supported by the client OM to be returned. Although I created a dirty workaround for that, I will show you that later.

Another alternative is to use REST protocol through the WCF Data Services.

As mentioned, our call must be synchronous, so we have to use jQuery.ajax specifying async: false. The next code snippet shows an example using the same condition applied above, that is title of the item must be started with ‘V’ to enable deletion.

  1. function allowDeletion(ctx, itemID) {
  2.  
  3.     itemUrl = ctx.HttpRoot + "/_vti_bin/listdata.svc/" + ctx.ListTitle +"(" + itemID + ")?$select=Title"    
  4.  
  5.     var title = "";
  6.  
  7.     jQuery.ajax(
  8.     {
  9.         type: 'GET',
  10.         url: itemUrl,
  11.         dataType: 'json',
  12.         success: function (result) {
  13.             if (result.isOk != false) {
  14.                 title = result.d.Title;
  15.             }
  16.         },
  17.         data: {},
  18.         async: false
  19.     });
  20.  
  21.     return (title.toUpperCase().startsWith("V"));
  22.  
  23. }

Note: When working with jQuery and WCF Data Services, it is useful to know about the parsererror issue with field values containing apostrophe (single quote) and how to fix it.

It is important to note that you are not restricted to the actual list only. With a bit of additional complexity you can query related lists as well using $expand and the lookup field / user field IDs in the current list. If you are unsure how to compose the URL for you request, I suggest you to try to create the filter first in C# using the LINQ syntax, then use Fiddler to capture the request or apply this simple trick from Sahil Malik.

If network bandwidth and speed are limited, then waiting for the synchronous result will cause issues via blocking the UI thread of the browser. If that is the case you should consider “pre-caching” the data required for checks on page load through a single request. If the item count of the list is limited, then you can cache all of the data (in our case, the IDs and the related titles of the items is needed), in the case of a larger list, you should get only the items displayed in the current page of the view. To get the IDs of the items on the page you should run a jQuery select similar to the one we used to get the title in the first example, and submit the REST request using a complex condition.

Last, I would like to show you my workaround for calling ECMAScript Client Object Model synchronously. As you might now, it is officially / theoretically not possible. I found that it can be done technologically, although I had to spend a few hours Fiddlering and digging into the internals of the ECMAScript Client OM (SP.Runtime.debug.js), comparing the JavaScript methods to the ones in the managed Client OM classes using Reflector. If you demand more information about it I can give you more details, but now I publish it “as is”.

To build up the request I use the “traditional” ECMAScript Client Object Model (that, of course, should be already loaded before our script), but before sending it I get the built-up request in XML format from the internal methods of the OM, and send the request through jQuery.ajax in synchronous mode, just as like the case of WCF Data Services. The JSON result is “loaded” into JavaScript objects.

Be aware that the method described here don’t use the public, documented interfaces, so it is probably not supported by MS, and suggested to be used only as an experiment. There is no guarantee that it will work for you, especially if our environments are at different SP / cumulative update level.

  1. function allowDeletion(ctx, itemID) {
  2.  
  3.     var title = "";
  4.  
  5.     try {
  6.         var context = SP.ClientContext.get_current();
  7.         var web = context.get_web();
  8.         var selectedListId = SP.ListOperation.Selection.getSelectedList();
  9.         var selectedListItem = web.get_lists().getById(selectedListId).getItemById(itemID);
  10.         context.load(selectedListItem, "Title");
  11.  
  12.         // start hacking
  13.         var pendingRequest = context.get_pendingRequest();
  14.         var webRequest = pendingRequest.get_webRequest();
  15.  
  16.         // get the request XML
  17.         var body = pendingRequest.$24_0().toString();
  18.  
  19.         // get the URL of client.svc
  20.         var url = webRequest.get_url();
  21.  
  22.         // "initialize" request
  23.         SP.ClientRequest.$1T(webRequest);
  24.  
  25.         // we should add digest later to the request as an HTTP header (see below)
  26.         var digest = webRequest.get_headers()['X-RequestDigest'];
  27.  
  28.         jQuery.ajax(
  29.         {
  30.             type: "POST",
  31.             data: body,
  32.             url: ctx.HttpRoot + url,
  33.             success: function (result) {
  34.                 if (result.isOk != false) {
  35.                     title = result[result.length - 1].Title;
  36.                 }
  37.             },
  38.             headers: {
  39.                 "x-requestdigest": digest
  40.             },
  41.             contentType: "application/x-www-form-urlencoded",
  42.             async: false
  43.         });
  44.  
  45.     }
  46.     catch (e) {
  47.         alert("Error: " + e);
  48.     }
  49.  
  50.     return (title.toUpperCase().startsWith("V"));
  51.  
  52. }

BTW, the example above achieves the same, it allows deletion only for items having title beginning with ‘V’. Similarly to the former WCF DS example, the synchronous request through the network blocks the UI thread, and might require pre-caching in case of a slow network.


Follow

Get every new post delivered to your Inbox.

Join 31 other followers