Second Life of a Hungarian SharePoint Geek

May 29, 2016

Project Publishing Failed due to Deleted SharePoint User

Filed under: Bugs, Event receivers, PowerShell, PS 2013 — Tags: , , , — Peter Holpar @ 05:53

In my recent post I wrote about a project publishing issue that was a result of a scheduling conflict.

The other day we had a similar problem with project publishing, but in this special case failed an other sub-process of the publishing process, the task synchronization. Another important difference from the former one is that at the scheduling conflict it was an end-user issue (a business user caused the conflict in the project plan scheduling), and in the case I’m writing about now, it was a mistake of an administrator plus a suboptimal code block in Project Server, that we can consider as a bug as well. But more on that a bit later…

First the symptoms we experienced. On the Manage Queue Jobs page in our PWA (http://YourProjectServer/PWA/_layouts/15/pwa/Admin/queue.aspx) we saw an entry of Job TypeSharePoint Task List Project” and Job State Failed And Blocking Correlation”.

Clicking on the entry displayed this information:

Queue: GeneralQueueJobFailed (26000) – ManagedModeTaskSynchronization.SynchronizeTaskListInManagedModeMessage. Details: id=’26000′ name=’GeneralQueueJobFailed’ uid=’46918ff3-3719-e611-80f4-005056b44e32′ JobUID=’adcad466-44bd-444b-a803-073fd12a2426′ ComputerName=’4fc61930-ef50-461b-b9ef-084a666c61ca’ GroupType=’ManagedModeTaskSynchronization’ MessageType=’SynchronizeTaskListInManagedModeMessage’ MessageId=’1′ Stage=” CorrelationUID=’cd56b408-a303-0002-d428-98cd03a3d101′.

The corresponding entries in the ULS logs:

PWA:http://YourProjectServer/PWA, ServiceApp:ProjectServerApplication, User:i:0#.w|YourDomain\FarmAccount, PSI: [QUEUE] SynchronizeTaskListInManagedModeMessage failed on project 5c21bf1b-c910-e511-80e5-005056b44e34. Exception: System.NullReferenceException: Object reference not set to an instance of an object.     at Microsoft.Office.Project.Server.BusinessLayer.ProjectModeManaged.UpdateAssignedToField(SPWeb workspaceWeb, DataSet taskDS, Guid taskUID, SPListItem listItem)     at Microsoft.Office.Project.Server.BusinessLayer.ProjectModeManaged.SynchronizeTask(SPList list, DataSet taskDS, Dictionary`2 taskMapping, DataRow row, DataView secondaryView, Dictionary`2 redoEntries)     at Microsoft.Office.Project.Server.BusinessLayer.ProjectModeManaged.<>c__DisplayClass1.<SynchronizeTaskListI…
…nManagedMode>b__0(SPWeb workspaceWeb)     at Microsoft.Office.Project.Server.BusinessLayer.Project.<>c__DisplayClass3d.<TryRunActionWithProjectWorkspaceWebInternal>b__3c()     at Microsoft.SharePoint.SPSecurity.<>c__DisplayClass5.<RunWithElevatedPrivileges>b__3()     at Microsoft.SharePoint.Utilities.SecurityContext.RunAsProcess(CodeToRunElevated secureCode)     at Microsoft.SharePoint.SPSecurity.RunWithElevatedPrivileges(WaitCallback secureCode, Object param)     at Microsoft.SharePoint.SPSecurity.RunWithElevatedPrivileges(CodeToRunElevated secureCode)     at Microsoft.Office.Project.Server.BusinessLayer.Project.TryRunActionWithProjectWorkspaceWebInternal(IPlatformContext context, Guid projectUid, Action`1 method, Boolean noThrow, DataRow row)     at Microsoft.Office.Project.Server.Busine…
…ssLayer.ProjectModeManaged.SynchronizeTaskListInManagedMode(Guid projectUid)     at Microsoft.Office.Project.Server.BusinessLayer.Queue.ProcessPublishMessage.ProcessSynchronizeTaskListInManagedModeMessage(Message msg, Group messageGroup, JobTicket jobTicket, MessageContext mContext), LogLevelManager Warning-ulsID:0x000CE687 has no entities explicitly specified.

So we have a NullReferenceException in the UpdateAssignedToField method of the Microsoft.Office.Project.Server.BusinessLayer.ProjectModeManaged class (Microsoft.Office.Project.Server assembly).

From the job message type “ManagedModeTaskSynchronization.SynchronizeTaskListInManagedModeMessage” it was obvious, that we have an issue with the synchronization between the project tasks and the Tasks list of the Project Web Site (PWS) of the project having the ID 5c21bf1b-c910-e511-80e5-005056b44e34”,  and from the method name “UpdateAssignedToField” we could assume, that the problem is caused either by an existing value of the “Assigned To” field, or by constructing a new value we want to update the field with.

We can use the following script to find out, which PWS belongs to the project ID above:

$pwa = Get-SPWeb http://YourProjectServer/PWA
$pwa.Webs | ? { $_.AllProperties[‘MSPWAPROJUID’] -eq ‘5c21bf1b-c910-e511-80e5-005056b44e34’ }

If we have a look at the code of the UpdateAssignedToField method, we see it begins with these lines. These lines are responsible for removing users from the “Assigned To” field (of type SPFieldUserValueCollection) that are no longer responsible for the task. The second part of method (not included below) is responsible for inserting new user entries. I highlighted the line that may cause (and in our case in fact has caused) an error if the value of the assignedTo[i].User expression is null.

bool isModified = false;
SPFieldUserValueCollection assignedTo = listItem["AssignedTo"] as SPFieldUserValueCollection;
DataRowView[] source = taskDS.Tables[1].DefaultView.FindRows(taskUID);
if (assignedTo != null)
{
    for (int i = assignedTo.Count – 1; i >= 0; i–)
    {
        string userName = ClaimsHelper.ConvertAccountFormat(assignedTo[i].User.LoginName);
        if (!source.Any<DataRowView>(resourceRow => (string.Compare(userName, resourceRow.Row.Field<string>("WRES_CLAIMS_ACCOUNT"), StringComparison.OrdinalIgnoreCase) == 0)))
        {
            assignedTo.RemoveAt(i);
            isModified = true;
        }
    }
}

The expression may be null if the user it refers to was deleted from the site. Note, that the expression assignedTo[i].LookupId even in this case returns the ID of the deleted user, and the expression assignedTo[i].LookupValue return its name.

How to detect which projects and which users are affected by the issue? I wrote the script below to display the possible errors:

  1. $rootWeb = Get-SPWeb http://YourProjectServer/PWA
  2.  
  3. $rootWeb.Webs | % {
  4.  
  5.     $web = $_
  6.  
  7.  
  8.     Write-Host ——————————-
  9.     Write-Host $web.Title
  10.  
  11.  
  12.     $foundMissingUsers = New-Object 'Collections.Generic.Dictionary[int,string]'
  13.  
  14.     $list = $web.Lists["Tasks"]
  15.  
  16.     if ($list -ne $null)
  17.     {
  18.         $list.Items | % {
  19.             $_["AssignedTo"] | ? {
  20.                  ($_.User -eq $null) -and (-not $foundMissingUsers.ContainsKey($_.LookupId)) } | % {
  21.                      if ($_ -ne $null ) { $foundMissingUsers.Add($_.LookupId, $_.LookupValue) }
  22.                  }
  23.         }
  24.  
  25.         $foundMissingUsers | % { $_ }
  26.     }
  27. }

Assuming

$allUserIds = $rootWeb.SiteUsers | % { $_.ID }

we could use

$allUserIds -NotContains $_.LookupId

instead of the condition

$_.User -eq $null

in the script above.

Indeed, we could identify two users on two separate projects, that were deleted by mistake, although they have assignments in the project Tasks lists.

We have recreated the users (and assigned the new users to the corresponding enterprise resources), but they have now another IDs. What can we do to fix the problem? The synchronization does not work anymore on these projects (making the project publishing impossible as well) so it does not provide a solution. We could replace the users in the “Assigned To” field, or simply remove the wrong one (it would be re-inserted by the second part of the UpdateAssignedToField method during the next synchronization), but there is an event receiver (Microsoft.Office.Project.PWA.ManagedModeListItemEventHandler) registered on this list, that cancels any changes in the list items when you want to persist the changes via the Update method. To avoid that, we could temporary disable the event firing, as described here.

We used the following script to fix the errors.

  1. $rootWeb = Get-SPWeb http://YourProjectServer/PWA
  2. $siteUsers = $rootWeb.SiteUsers
  3.  
  4.  
  5. # disable event firing to prevent cancelling updates by PreventEdits method (Microsoft.Office.Project.PWA.ManagedModeListItemEventHandler)
  6. # http://sharepoint.stackexchange.com/questions/37614/disableeventfiring-using-powershell
  7. $receiver = New-Object "Microsoft.SharePoint.SPEventReceiverBase"
  8. $type = $receiver.GetType()
  9. [System.Reflection.BindingFlags]$flags = [System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::NonPublic
  10. $method = $type.GetMethod("DisableEventFiring", $flags)
  11. $method.Invoke($receiver, $null)
  12.  
  13.  
  14. $rootWeb.Webs | ? { $_.Title -eq 'YourProjectName' } | % {
  15.  
  16. $web = $_
  17.  
  18. Write-Host ——————————-
  19. Write-Host $web.Title
  20.  
  21. $userPairs = ((122, 3421), (145, 2701))
  22.  
  23. $userPairsResolved = $userPairs | Select-Object -Property `
  24.   @{ Name="OldUserId"; Expression = { $_[0] }},
  25.   @{ Name="NewUser"; Expression = { $up = $_; $siteUsers | ? { $_.ID -eq $up[1] } }}
  26.  
  27. $list = $web.Lists["Tasks"]
  28.  
  29. if ($list -ne $null)
  30. {
  31.     $list.Items | % { $list.Items | % {
  32.         $item = $_
  33.         [Microsoft.SharePoint.SPFieldUserValueCollection]$assignedTo = $item["AssignedTo"]
  34.         if ($assignedTo -ne $null)
  35.         {
  36.             $isModified = $false
  37.  
  38.             # iterate through the assignments
  39.             for($i = 0; $i -lt $assignedTo.Count; $i++)
  40.             {
  41.                 if ($assignedTo[$i].User -eq $null)
  42.                 {
  43.                     $userName = $assignedTo[$i].LookupValue
  44.                     $userid = $assignedTo[$i].LookupId
  45.                     $taskTitle = $item.Title.Trim()
  46.                     Write-Host Task """$taskTitle""" assigned user """$userName""" "($userId)" missing
  47.                     $newUser = $userPairsResolved | ? { $_.OldUserId -eq $userid } | % { $_.NewUser }
  48.                     if ($newUser -ne $null)
  49.                     {
  50.                         $newUserId = $newUser.Id
  51.                         $newUserName = $newUser.Name
  52.                         do { $replaceAssignedTo = Read-Host Would you like to replace the assignment of the missing user with """$newUserName""" "($newUserId)"? "(y/n)" }
  53.                         until ("y","n" -contains $replaceAssignedTo )
  54.  
  55.                         if ($replaceAssignedTo -eq "y")
  56.                         {
  57.                             # step 1: removing the orphaned entry
  58.                             $assignedTo.RemoveAt($i)
  59.  
  60.                             # step 2: create the replacement
  61.                             [Microsoft.SharePoint.SPFieldUserValue]$newUserFieldValue = New-Object Microsoft.SharePoint.SPFieldUserValue($web, $newUser.Id, $newUser.Name)     
  62.                             $assignedTo.Add($newUserFieldValue)
  63.  
  64.                             # set the 'modified' flag
  65.                             $isModified = $true
  66.                         }
  67.                     }
  68.                     else
  69.                     {
  70.                         Write-Host WARNING No user found to replace the missing user with -ForegroundColor Yellow
  71.                     }
  72.                       }
  73.             }
  74.  
  75.             # update only if it has been changed
  76.             if ($isModified)
  77.             {
  78.             $item["AssignedTo"] = $assignedTo
  79.             $item.Update()
  80.             Write-Host Task updated
  81.             }
  82.         }
  83.     }}
  84. }
  85.  
  86. }
  87.  
  88. # re-enabling event fireing
  89. $method = $type.GetMethod("EnableEventFiring", $flags)
  90. $method.Invoke($receiver, $null)

The variable $userPairs contains the array of old user IDnew user ID mappings. In step 1 we remove the orphaned user entry (the one referring the deleted user), in step 2 we add the entry for the recreated user. If you plan to run the synchronization (for example, by publishing the project) after the script, step 2 is not necessary, as the synchronization process inserts the references for the users missing from the value collection.

Note 1: The script runs only on the selected project (in this case “YourProjectName”), to minimize the chance to change another project unintentionally.

Note 2: The script informs a user about the changes it would perform, like to replace a reference to a missing user to another one, and waits a confirmation (pressing the ‘y’ key) for the action on behalf on the user executes the script. If you have a lot of entries to change, and you are sure to replace the right entries, you can remove this confirmation and make the script to finish faster.

November 12, 2014

Managing Project Server Event Handlers via the Managed Client Object Model

Project Server events provide a great way to extend the functionality of the product by custom logic. For example, you can write code that will be executed if the web site of the project was created and customize the site with features that can not be integrated into the project site template itself, or respond to the deletion of the project and perform some kind of housecleaning, that is not part of the default deletion, like removing custom created project-specific SharePoint groups.

There is a page in the Central Administration that supports the event handler maintenance (see Central Administration / General Application Settings / PWA Settings (Manage) / Server Side Event Handlers, illustrated below).

image

There is unfortunately a major Application Lifecycle Management (ALM) related issue with this approach: It is a manual process that cannot be easily automated as part of the solution deployment.

Alternatively, one can deploy event handlers as part of a SharePoint package, as stated in the Project Server entity provisioning schema section of the Project Server programmability article. This method is described and demonstrated in the presentation Developer to developer – learn the Microsoft Project Online and Server extensibility (authors: Bill Avery and Chris Givens), see further details in section “Project Server App Packages” beginning at slide 75 of the presentation.

A sample of the Project Server entity provisioning XML including an event handler registration:

  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <ProjectServerEntities xmlns="http://schemas.microsoft.com/sharepoint/">
  3.   <EventHandlers>
  4.     <EventHandler Id="6B92EF26-25CA-4716-9352-67FC2EF57BE3" Name="PWSCreated">
  5.       <EventName>WssInteropWssWorkspaceCreated</EventName>
  6.       <Description>Called when a new PWS site was created</Description>
  7.       <EndpointUrl>http://dummysite.org</EndpointUrl>
  8.       <AssemblyName>$SharePoint.Project.AssemblyFullName$</AssemblyName>
  9.       <ClassName>Customer.EPM.PSEventHandler</ClassName>
  10.       <CancelOnError>false</CancelOnError>
  11.     </EventHandler>
  12.   </EventHandlers>
  13. </ProjectServerEntities>

Note the EndpointUrl element, that contains only a dummy value in this case. In this case we use a local event handler implemented in a custom assembly, however we could use remote event handlers as well. This element is optional by the XSD of the Project Server entity provisioning schema, however, if we did not include this in the XML, we would receive a deployment error on the feature activation:

Value cannot be null. Parameter name: uriString

An empty value causes a similar error in the feature receiver:

Invalid URI: The URI is empty.

If the URL is not well formatted:

Invalid URI: The format of the URI could not be determined.

Formerly I applied this approach to deploy my event handlers, lookup tables and enterprise custom fields, but it turned out quickly, that whenever I deploy a new version of the package, the former entities are retracted and deployed again, resulting in a loss of information, for example, on tasks, projects and resources that already had a re-deployed enterprise custom fields assigned. In a developer environment it was only inconvenient, but in test and production system I found that simply unacceptable.

So I decided to write my own tools to list, register and remove Project Server event handlers using the managed client object model.

First, let’s see how to enumerate the event handlers and dump the most important properties:

  1. using (var projectContext = new ProjectContext(pwaUrl))
  2. {
  3.     projectContext.Load(projectContext.EventHandlers, ehs => ehs.Include(
  4.         eh => eh.Id,
  5.         eh => eh.Event,
  6.         eh => eh.AssemblyName,
  7.         eh => eh.ClassName,
  8.         eh => eh.Order));
  9.     projectContext.ExecuteQuery();
  10.  
  11.     projectContext.EventHandlers.ToList().ForEach(eh =>
  12.         Console.WriteLine("Event \r\nEvent Handler [Id: {3} AssemblyName: {4} ClassName: {5} Order: {6}]\r\n",
  13.         eh.Event.SourceName, eh.Event.EventName, eh.Event.Id, eh.Id, eh.AssemblyName, eh.ClassName, eh.Order));
  14. }

The output in our case:

image

Using the code below, we can register our custom event handler. To be able to use the predefined values in the PSEventID enumeration, you should include a reference to the Microsoft.Office.Project.Server.Library.dll assembly.

  1. using (var projectContext = new ProjectContext(pwaUrl))
  2. {
  3.     var ehci = new EventHandlerCreationInformation
  4.     {
  5.         Id = projDeletedEventHandlerId,
  6.         Name = "ProjDeleting",
  7.         AssemblyName = "Customer.EPM, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b28db3dcb95229e1",
  8.         ClassName = "Customer.EPM.ProjectEvents",
  9.         Description = "Called when a project is being deleted",
  10.         EventId = (int)PSEventID.ProjectDeleting, // value is 49, handling ProjectEventReceiver.OnDeleting
  11.         Order = 100,
  12.         CancelOnError = false
  13.     };
  14.  
  15.     projectContext.EventHandlers.Add(ehci);
  16.     projectContext.EventHandlers.Update();
  17.     projectContext.ExecuteQuery();
  18. }

Note: this code only submits the requests for the event handler registration- The registration itself is performed asynchronously in a background process, so there is a delay after running the code until the event handler appears in the list.

Finally, if you don’t need the event handler any more, you can remove it using the following code (assuming you already know the ID of the event handler, for example, from the output of the event handler enumeration example):

  1. var projDeletedEventHandlerId = new Guid("0FB1389D-E620-4062-A99B-9E5716CC958E");
  2.  
  3. using (var projectContext = new ProjectContext(pwaUrl))
  4. {
  5.     var eh = projectContext.EventHandlers.GetByGuid(projDeletedEventHandlerId);
  6.     projectContext.EventHandlers.Remove(eh);
  7.     projectContext.EventHandlers.Update();
  8.     projectContext.ExecuteQuery();
  9. }

A similar code for unregistering the event can be found here, but the sample above is a bit simpler, using only a single ExecuteQuery call. You might need the double call if you don’t know the ID of the event handler yet. In this case you should look up the event handler by name, or by the type of the event it handles first, and it requires a former ExecuteQuery call to get the complete list of the event handlers. Then in the next step, you can already remove the event handler as illustrated above.

August 30, 2013

Deleting items from a list despite event receivers

Filed under: Event receivers, SP 2010 — Tags: , — Peter Holpar @ 02:18

Assume you have a SharePoint list with an ItemDeleting event receiver that cancels deletions, prohibiting users to remove any items even if they have the required permissions. If for some reason you would like to delete an item, you should “inactivate” the event receiver first. Inactivating the event receiver just for the current operation may be not trivial in a production system, as you cannot simply deregister (and then re-register) the event receiver, since your users might start deleting other items in the meantime.

The following code snippet provides a sample console application that performs the deletion even if an event receiver would otherwise cancel the operation. It requires the following command line arguments: URL of the list and the ID of the item that should be deleted.

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using Microsoft.SharePoint;
  6.  
  7. namespace DeleteItem
  8. {
  9.     class Program
  10.     {
  11.         static void Main(string[] args)
  12.         {
  13.             try
  14.             {
  15.                 if (args.Length != 2)
  16.                 {
  17.                     throw new ArgumentException("Incorrect number of parameters. Specify List URL and List item ID.");
  18.                 }
  19.                 else
  20.                 {
  21.                     var url = args[0];
  22.                     var itemId = int.Parse(args[1]);
  23.                     using (SPSite site = new SPSite(url))
  24.                     {
  25.                         using (SPWeb web = site.OpenWeb())
  26.                         {
  27.                             var eventFiringHandler = new EventFiringHandler();
  28.                             try
  29.                             {
  30.                                 var list = web.GetList(url.Substring(web.Url.Length));
  31.                                 if (list != null)
  32.                                 {
  33.                                     Console.WriteLine("List '{0}' found.", list.Title);
  34.                                     var item = list.GetItemById(itemId);
  35.                                     Console.WriteLine("Are you sure to delete item '{0}' (y/n)?", item.Title);
  36.                                     var response = Console.ReadLine();
  37.                                     if (response.ToLower() == "y")
  38.                                     {
  39.                                         eventFiringHandler.EventFiringEnabled = false;
  40.                                         item.Delete();
  41.                                         Console.WriteLine("Item '{0}' deleted.", itemId);
  42.                                     }
  43.                                     else
  44.                                     {
  45.                                         Console.WriteLine("Deletion cancelled.");
  46.                                     }
  47.                                 }
  48.                                 else
  49.                                 {
  50.                                     Console.WriteLine("List '{0}' not found.", url);
  51.                                 }
  52.                             }
  53.                             finally
  54.                             {
  55.                                 if (eventFiringHandler != null)
  56.                                 {
  57.                                     eventFiringHandler.EventFiringEnabled = true;
  58.                                 }
  59.                             }
  60.                         }
  61.                     }
  62.                 }
  63.             }
  64.             catch (Exception ex)
  65.             {
  66.                 Console.WriteLine("Exception: {0}\r\n{1}", ex.Message, ex.StackTrace);
  67.             }
  68.         }
  69.  
  70.         internal class EventFiringHandler : SPItemEventReceiver
  71.         {
  72.             public bool EventFiringEnabled
  73.             {
  74.                 get
  75.                 {
  76.                     return base.EventFiringEnabled;
  77.                 }
  78.                 set
  79.                 {
  80.                     base.EventFiringEnabled = value;
  81.                 }
  82.             }
  83.         }
  84.     }
  85. }

July 8, 2013

Finding and removing orphaned event receivers using PowerShell

Filed under: Event receivers, PowerShell, SP 2010 — Tags: , , — Peter Holpar @ 23:56

Recently I found a lot of errors in the Windows / SharePoint logs on one of our servers, stating, that SharePoint event manager cannot load the registered classes to handle specific events.

SharePoint log:

06/28/2013 10:35:06.94                OWSTIMER.EXE (0x2AA0)                           0x2DFC SharePoint Foundation                General                                       6644      Critical  Event manager error: Could not load type ‘My.Namespace.EventReceiverClass’ from assembly ‘My.Assembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f277a09de9ab6579’.          530f203f-2f99-4b24-9ba8-c59adf5039b6

Windows log:

Event manager error: Could not load type ‘My.Namespace.EventReceiverClass’ from assembly ‘My.Assembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f277a09de9ab6579’.

image

I wrote a few PowerShell snippets to help to find the orphaned event receiver registrations referring to the type mentioned in the error message.

First, searching event receivers on a specific list and all lists of the site:

# List event receivers of a specific list
$list = $web.Lists["yourlist"]
$list.EventReceivers | % { $_.Assembly, $_.Class, $_.Type }

# List event receivers of all lists of the current site, where the class name matches the type reported in the event log
$web.Lists | % {$_.EventReceivers} | ? { $_.Class -eq ‘My.Namespace.EventReceiverClass’ } | % { $_.Assembly, $_.Class, $_.Type }

Next, searching event receivers registered for a specific content type and all content types of the site:

# List event receivers of a specific content type
$ct = $site.RootWeb.ContentTypes["yourcontenttype"]
$ct.EventReceivers | % { $_.Assembly, $_.Class, $_.Type }

# List event receivers of all content types of the current site, where the class name matches the type reported in the event log
$site.RootWeb.ContentTypes | % {$_.EventReceivers} | ? { $_.Class -eq ‘My.Namespace.EventReceiverClass’ } | % { $_.Assembly, $_.Class, $_.Type }

Finally, search site event receivers:

# List site event receivers, where the class name matches (using wildcards) the type reported in the event log
$site.EventReceivers | ? { $_.Class -like ‘*EventReceiverClass*’ } | % { $_.Assembly, $_.Class, $_.Type }

The problematic event receivers were found among the site event receivers, so I wrote a script to remove them. To avoid the infamous “Collection was modified; enumeration operation may not execute.” error, we should remove the event receivers in two steps. First, the IDs of the event receivers are determined, then in the second step the event receivers are deleted by ID:

# Remove site event receivers, where the class name matches (using wildcards) the type reported in the event log
$evenReceiverIds = $site.EventReceivers | ? { $_.Class -like ‘*EventReceiverClass*’ } | % { $_.ID }
$evenReceiverIds | % {
  $er = $site.EventReceivers[$_]
  Write-Host Deleting  $er.Assembly, $er.Class, $er.Type
  $er.Delete()
  Write-Host Deleted
}

October 26, 2012

How to populate the Attendees field of a SharePoint event based on the addressees of a meeting request? (Version 2)

In my previous post I already demonstrated a method to resolve the meeting attendees based on the mail addresses in the incoming mail, though – as I wrote there – that method has issues with event updates.

Note: In this post I show you an alternative, that – at least, based on my experience – performs better. However, the code below uses non-public API calls and accesses SharePoint database directly, so it is not a supported approach. Use this sample at you own risk and preferably only in test environments.

In this version of implementation we alter the standard pipeline of incoming mail processing for our calendar to inject our code into the process. To achieve that, we create an a SPEmailHandler that first invokes the ProcessMessage method of the SPCalendarEmailHandler class to achieve the standard functionality, then resolves the attendees using the To mail header property based on this technique, and updates the related item / all related items (in the case of a recurring event, it may be not a single item) in the list.

Note: In the case of the To mail header property we don’t need to unescape the value, so you should comment out this line of code in the GetUsersByMailTo method introduced in the first part of this post:

emailTo = emailTo.Replace("&lt;", "<").Replace("&gt;", ">");

We can get the corresponding item(s) based the unique identifier (UID, you can read details on Wikipedia) vCalendar property of the event using the GetCalendarProp method described in my former post. We use the GetExistingItems method below to get these related items:

  1. private SPListItemCollection GetExistingItems(SPList list, string uid)
  2. {            
  3.     SPQuery query = new SPQuery();
  4.     query.Query = "<Where><Eq><FieldRef Name=\"" + list.Fields[SPBuiltInFieldId.EmailCalendarUid].InternalName + "\"/><Value Type=\"Text\">" + SPEncode.HtmlEncode(uid) + "</Value></Eq></Where>";
  5.     SPListItemCollection existingItems = list.GetItems(query);
  6.     return existingItems;
  7. }

The following method allows us to invoke the ProcessMessage method of the SPCalendarEmailHandler class:

  1. private void ProcessMessage(SPList list, SPEmailMessage emailMessage)
  2. {
  3.     Trace.TraceInformation("Starting SPCalendarEmailHandler processing");
  4.  
  5.     string spCalendarEmailHandlerTypeName = "Microsoft.SharePoint.SPCalendarEmailHandler";
  6.  
  7.     // hack to get the Microsoft.SharPoint assembly
  8.     Assembly sharePointAssembly = typeof(SPWeb).Assembly;
  9.     // and a reference to the type of the SPCalendarEmailHandler internal class
  10.     Type spCalendarEmailHandlerType = sharePointAssembly.GetType(spCalendarEmailHandlerTypeName);
  11.  
  12.     // spCalendarEmailHandler will be of type internal class
  13.     // Microsoft.SharePoint.SPCalendarEmailHandler
  14.     // defined in Microsoft.SharePoint assembly
  15.     object spCalendarEmailHandler = sharePointAssembly.CreateInstance(spCalendarEmailHandlerTypeName, false,
  16.         BindingFlags.Public | BindingFlags.Instance, null, new object[] { list }, CultureInfo.InvariantCulture, null);
  17.  
  18.     if (spCalendarEmailHandler != null)
  19.     {
  20.         MethodInfo mi_ProcessMessage = spCalendarEmailHandlerType.GetMethod("ProcessMessage",
  21.                     BindingFlags.Public | BindingFlags.Instance, null,
  22.                     new Type[] { typeof(SPEmailMessage) }, null
  23.                     );
  24.         if (mi_ProcessMessage != null)
  25.         {
  26.             // result of type SPEmailHandlerResult is ignored
  27.             mi_ProcessMessage.Invoke(spCalendarEmailHandler, new Object[] { emailMessage });
  28.         }
  29.     }
  30.  
  31.     Trace.TraceInformation("SPCalendarEmailHandler processing finished");
  32. }

Using these helper methods our EmailReceived method looks like these:

  1. public override void EmailReceived(SPList list, SPEmailMessage emailMessage, string receiverData)
  2. {
  3.     try
  4.     {
  5.         Trace.TraceInformation("EmailReceived started");
  6.  
  7.         string uid = GetCalendarProp(emailMessage, "UID");
  8.  
  9.         ProcessMessage(list, emailMessage);
  10.  
  11.         string emailTo = emailMessage.Headers["To"];
  12.         SPFieldUserValueCollection users = GetUsersByMailTo(list.ParentWeb, emailTo);
  13.  
  14.         if (!string.IsNullOrEmpty(uid))
  15.         {
  16.             SPListItemCollection existingItems = GetExistingItems(list, uid);
  17.             foreach (SPListItem listItem in existingItems)
  18.             {
  19.                 Trace.TraceInformation("Updating item ID: {0}, To: {1}", listItem.ID, emailTo);
  20.                 listItem[SPBuiltInFieldId.ParticipantsPicker] = users;
  21.                 listItem.Update();
  22.             }
  23.         }
  24.  
  25.         Trace.TraceInformation("EmailReceived calling base handler(s)…");
  26.     }
  27.     catch (Exception ex)
  28.     {
  29.         Trace.TraceInformation("EmailReceived exception: {0}", ex.Message);
  30.         Trace.TraceInformation(ex.StackTrace);
  31.     }
  32.     base.EmailReceived(list, emailMessage, receiverData);
  33. }

Finally, this method seems to fulfill our goals and resolves attendees both on new meeting requests and event updates.

How to populate the Attendees field of a SharePoint event based on the addressees of a meeting request? (Version 1)

Filed under: Calendar, Event receivers, Incoming email, SP 2010 — Tags: , , , — Peter Holpar @ 09:39

As I formerly wrote, if you enable the incoming mails on a SharePoint calendar, and send a meeting request to the list, the participants’ mail addresses won’t be resolved to meeting attendees.

To workaround this limitation, my first idea was a SPEmailHandler that extracts this info from the mail and stores it into the adequate SharePoint field. However I found, that after I registered my event receiver on the list, all of the default functionality of the incoming mail on calendars (like resolving time and location of the meetings, updating former items in the list on event updates, etc.) were lost, even if I allow in my override of EmailReceived method other registered receivers to be called, like:

base.EmailReceived(list, emailMessage, receiverData);

The reason for this phenomena I have found reflecting the related classes and in this post. That means, specific list types, like calendars, announcements, discussions, etc. have their standard ProcessMessage methods implemented in subclasses of the internal abstract SPEmailHandler class. For example, the incoming mails for a calendar is handled by the SPCalendarEmailHandler class. If you register a custom event receiver (that means the HasExternalEmailHandler property of the list will be true), the standard method will be totally ignored, and the custom event receiver will be called through a SPExternalEMailHandler instance instead. Really bad news!

How could we inject our requirement of resolving attendees into this processing chain without disturbing the standard steps?

Spending a few minutes with Reflector I found, that the SetStandardHeaderFields method of the SPEmailHandler class (called from the ProcessVEvent method of the SPCalendarEmailHandler class) sets (among others) the EmailTo field, and the SetEmailHeadersField method of the same class (called from the SetStandardHeaderFields method) sets the EmailHeaders field.

Both of these fields seem to be undocumented hidden fields. The EmailHeaders field contains the whole (unescaped!) SMTP header block of the original mail that triggered the item creation, while the EmailTo field contains the addresses of the mail in these escaped format:

John Smith &lt;john.smith@contoso.com&gt;;  Peter Black &lt;peter.black@contoso.com&gt;

The escaping seems to be important. When I tried setting the values without it, the e-mail addresses were removed from the field, leaving only the display names there.

To resolve users, I used a slightly modified version of the method demonstrated in my former post:

  1. private SPFieldUserValueCollection GetUsersByMailTo(SPWeb web, string emailTo)
  2. {
  3.     SPFieldUserValueCollection result = new SPFieldUserValueCollection();
  4.     if (!string.IsNullOrEmpty(emailTo))
  5.     {
  6.         emailTo = emailTo.Replace("&lt;", "<").Replace("&gt;", ">");
  7.         string[] addressees = emailTo.Split(';');
  8.  
  9.         Array.ForEach(addressees,
  10.             addressee =>
  11.             {
  12.                 MailAddress ma = new MailAddress(addressee);
  13.                 SPPrincipalInfo pi = SPUtility.ResolveWindowsPrincipal(web.Site.WebApplication, ma.Address, SPPrincipalType.User, true);
  14.                 if ((pi != null) && (!string.IsNullOrEmpty(pi.LoginName)))
  15.                 {
  16.                     SPUser user = web.EnsureUser(pi.LoginName);
  17.                     result.Add(new SPFieldUserValue(web, user.ID, null));
  18.                     Console.WriteLine("User: {0}", user.LoginName);
  19.                 }
  20.             });
  21.     }
  22.  
  23.     return result;
  24. }

My next step was to create an ItemUpdated event handler that should resolve the users based on their e-mail addresses. An issue I had to handle there is, that the item can be updated not only due to the incoming mail, but also due to the changes the users make through the UI. It would be rather frustrating for our users, if we set the original value back, after the they altered it through the UI. So I triggered the user resolving process only if the value of the EmailTo was not empty, and clear the value of the field after processing to prohibit further calls of the method (for example, triggered from the UI).

Note: To update the attendees of the event, we should set the value of the ParticipantsPicker field, and not the Participants field.

  1. public override void ItemUpdated(SPItemEventProperties properties)
  2. {
  3.     try
  4.     {
  5.         Trace.TraceInformation("ItemUpdated started");
  6.         SPListItem listItem = properties.ListItem;
  7.         string emailTo = listItem[SPBuiltInFieldId.EmailTo] as string;
  8.         Trace.TraceInformation("emailTo: {0}", emailTo);
  9.         SPWeb web = properties.Web;
  10.  
  11.         if (!string.IsNullOrEmpty(emailTo))
  12.         {
  13.             Trace.TraceInformation("Updating attendees");
  14.             listItem[SPBuiltInFieldId.ParticipantsPicker] = GetUsersByMailTo(web, emailTo);
  15.             // hack to prohibit triggering on updates from UI
  16.             listItem[Microsoft.SharePoint.SPBuiltInFieldId.EmailTo] = null;
  17.             listItem.Update();
  18.         }
  19.         Trace.TraceInformation("ItemUpdated finished");
  20.     }
  21.     catch (Exception ex)
  22.     {
  23.         Trace.TraceInformation("ItemUpdated exception: {0}", ex.Message);
  24.         Trace.TraceInformation(ex.StackTrace);
  25.     }
  26.     base.ItemUpdated(properties);
  27. }

After deploying my event receiver I’ve checked it through sending meeting requests to attendees including the address of the calendar list, and first the solution seemed to be perfect. However, after a while I found, that when I send an update for an event, the list of the attendees was not updated.

The reason behind this issue is – as it turned out after another round of reflectioning – that the EmailTo field is set only once, when the item is created, but not populated when updates are received. See the output parameter isUpdate of the FindItemToUpdate method of the SPCalendarEmailHandler class for details. If the actual processing is an update, the SetStandardHeaderFields method, and through this method the SetEmailHeadersField method won’t be invoked in the ProcessVEvent method.

In the next post I try to publish the alternative solution for the original request – resolving meeting attendees based on the mail addresses in the incoming mail.

February 14, 2012

Accessing external data from SharePoint timer jobs or from event receivers triggered by incoming mail

Recently we had an issue with accessing external list data. One of my colleagues wrote an ItemAdded event receiver for a document library, that updates the document metadata based on a CAML query run against an external list. The source of the external list is a simple database table. For this post assume it is the Meetings table in the Northwind database.

Everything was OK while we uploaded the documents “manually” – that means from the web UI. However, enabling incoming mail for the document library and configuring it to save mail attachments to the library caused us headache.

Documents sent as e-mail attachments were not handled as expected, instead, generated an exception on the external list query.

We received the following error message on a system configured for Kerberos:

Access Denied for User ”, which may be an impersonation by ‘domain\owsTimerAccount’.

In another configuration without Kerberos, using “simple” Windows authentication:

Access denied by Business Data Connectivity.

The stack trace was the same for the both cases:

Microsoft.SharePoint.SPListDataSource.GetEntityInstanceEnumerator(XmlNode xnMethodAndFilters)
at Microsoft.SharePoint.SPListDataSource.GetFilteredEntityInstancesInternal(XmlDocument xdQueryView, Boolean fFormatDates, Boolean fUTCToLocal, String firstRowId, Boolean fBackwardsPaging, String& bdcidFirstRow, String& bdcidNextPageRow, List`1& lstColumnNames, Dictionary`2& dictColumnsUsed, List`1& mapRowOrdering, List`1& lstEntityData)
at Microsoft.SharePoint.SPListDataSource.GetFilteredEntityInstances(XmlDocument xdQueryView, Boolean fFormatDates, Boolean fUTCToLocal, String firstRowId, Boolean fBackwardsPaging, String& bdcidFirstRow, String& bdcidNextPageRow, List`1& lstColumnNames, Dictionary`2& dictColumnsUsed, List`1& mapRowOrdering, List`1& lstEntityData)
at Microsoft.SharePoint.SPListItemCollection.EnsureEntityDataViewAndOrdering(String& bdcidFirstRow, String& bdcidNextPageFirstRow)
at Microsoft.SharePoint.SPListItemCollection.EnsureListItemsData()
at Microsoft.SharePoint.SPListItemCollection.get_Count()

Of course, the owsTimerAccount user has permissions for BCS, the external list and for the external database.

I’ve tried the same code with elevated permissions, SharePoint and Windows impersonation, but that has no effect, except elevated permissions, where I’ve received just another exception:

Attempted to perform an unauthorized operation.

and the stack trace is a bit different as well:

at Microsoft.SharePoint.SPListDataSource.CheckUserIsAuthorized(SPBasePermissions perms)
at Microsoft.SharePoint.SPListDataSource.GetFilteredEntityInstances(XmlDocument xdQueryView, Boolean fFormatDates, Boolean fUTCToLocal, String firstRowId, Boolean fBackwardsPaging, String& bdcidFirstRow, String& bdcidNextPageRow, List`1& lstColumnNames, Dictionary`2& dictColumnsUsed, List`1& mapRowOrdering, List`1& lstEntityData)
at Microsoft.SharePoint.SPListItemCollection.EnsureEntityDataViewAndOrdering(String& bdcidFirstRow, String& bdcidNextPageFirstRow)
at Microsoft.SharePoint.SPListItemCollection.EnsureListItemsData()
at Microsoft.SharePoint.SPListItemCollection.get_Count()

Note: If you would like to apply Windows impersonation, the original identity your process running with (in this case the account configured for the SharePoint 2010 Timer service) should have the Act as part of the operating system user right.

At this point we started to suspect that the source of the issue is somehow related with the SPTimerV4 context. I’ve created a timer job to access our external list just to validate this theory and found the very same results.

Fortunately, this forum thread led us to the solution: instead of running a CAML query against an external list, we should access and filter the external data directly using the BCS API.

The code examples below show the implementation steps for a timer job. If you need the solution for an event receiver, simply replace the name and signature of the Execute method to the one of your event receiver method.

Trace methods in code are included simply to verify the flow of the process.

First, we should add a reference to the Microsoft.BusinessData.dll assembly located at GAC.

Next, add the following namespaces to the code of your SPJobDefinition class:

  1. using System.Diagnostics;
  2. using Microsoft.SharePoint;
  3. using Microsoft.SharePoint.Administration;
  4. using Microsoft.SharePoint.BusinessData.SharedService;
  5. using Microsoft.BusinessData.MetadataModel;
  6. using Microsoft.BusinessData.MetadataModel.Collections;
  7. using Microsoft.BusinessData.Runtime;

I’ve defined the following “constants” in my code. These values should be altered to reflect the properties of your business data.

  1. private static readonly String _nameSpace = "http://sp2010&quot;;
  2. private static readonly String _externalList = "ExtCalendar";
  3. private static readonly String _externalCT = "Nortwind Calendar";
  4. private static readonly String _finderView = "Read List";
  5. private static readonly String _textFieldName = "cLocation";
  6. private static readonly String _dateFieldName = "dStart";

In the Execute method the test method is started using elevated permissions (site URL should be replaced):

  1. public override void Execute(Guid targetInstanceId)
  2. {
  3.     Trace.TraceInformation("Starting BcsTestJob execution");
  4.  
  5.     try
  6.     {
  7.         using (SPSite site = new SPSite("http://sp2010&quot;))
  8.         {
  9.             using (SPWeb web = site.OpenWeb())
  10.             {
  11.                 Guid siteId = site.ID;
  12.                 Guid webId = web.ID;
  13.  
  14.                 SPSecurity.RunWithElevatedPrivileges(delegate()
  15.                 {
  16.                     using (SPSite siteImp = new SPSite(siteId))
  17.                     {
  18.                         TestExtList(site, "New York", new DateTime(2010, 9, 21));
  19.                     }
  20.                 });
  21.             }
  22.         }
  23.     }
  24.     catch (Exception ex)
  25.     {
  26.         Trace.TraceInformation("ERROR in BcsTestJob.Execute: '{0}', '{1}'", ex.Message, ex.StackTrace);
  27.     }
  28.     Trace.TraceInformation("BcsTestJob finished");
  29. }

In the TestExtList method we access the business data and filter the results. Of course, in this case we can’t use CAML.

  1. private void TestExtList(SPSite site, string textValue, DateTime dateValue)
  2. {
  3.     try
  4.     {
  5.  
  6.         using (SPServiceContextScope scope = new Microsoft.SharePoint.SPServiceContextScope(SPServiceContext.GetContext(site)))
  7.         {
  8.             BdcService service = SPFarm.Local.Services.GetValue<BdcService>(String.Empty);
  9.             IMetadataCatalog catalog = service.GetDatabaseBackedMetadataCatalog(SPServiceContext.Current);
  10.             IEntity entity = catalog.GetEntity(_nameSpace, _externalCT);
  11.             ILobSystemInstance LobSysteminstance = entity.GetLobSystem().GetLobSystemInstances()[0].Value;
  12.  
  13.             IFieldCollection fieldCollection = entity.GetFinderView(_finderView).Fields;
  14.  
  15.             IMethodInstance methodInstance = entity.GetMethodInstance(_finderView, MethodInstanceType.Finder);
  16.             IEntityInstanceEnumerator ientityInstanceEnumerator = entity.FindFiltered(methodInstance.GetFilters(), LobSysteminstance);
  17.  
  18.             int counter = 0;
  19.             while (ientityInstanceEnumerator.MoveNext())
  20.             {
  21.                 IEntityInstance entityInstance = ientityInstanceEnumerator.Current;
  22.                 if (((String)entityInstance[_textFieldName] == textValue) && ((DateTime)entityInstance[_dateFieldName] == dateValue))
  23.                 {
  24.                     counter++;
  25.                 }
  26.             }
  27.  
  28.             Trace.TraceInformation("BcsTestJob.TestExtList item count: {0}", counter);
  29.         }
  30.  
  31.     }
  32.     catch (Exception ex)
  33.     {
  34.         Trace.TraceInformation("ERROR in BcsTestJob.TestExtList: '{0}', '{1}'", ex.Message, ex.StackTrace);
  35.     }
  36. }

Applying this approach no exception is thrown. The external data can be accessed from the event receiver as well from the timer job, both of them running in the OWS Timer process context.

June 26, 2011

Injecting HttpContext and SPContext into the event receiver context

Filed under: Event receivers, SP 2010 — Tags: , — Peter Holpar @ 14:59

Last week I met an interesting challenge and would like to show you a way of solution. Before going into details, I have to say that the method I show you probably far from what is called a best practice, but given the situation I think it might be a quick and dirty solution for the issue.

Assume we have the following static class:

  1. public static class Lists
  2. {
  3.     public static readonly String MyList = Resource.GetResFieldByKey("Lists_MyList");
  4.     public static readonly String AnotherList = Resource.GetResFieldByKey("Lists_AnotherList");
  5. }

We use this class to store “configurable” constants. The GetResFieldByKey is a static method to read resource strings. The resource file is stored in the App_GlobalResources folder of the web application. In the original implementation the path of the resource file was computed like this:

String rsPath = HttpContext.Current.Server.MapPath("~/App_GlobalResources") + "\\" + resourceFile + ".resx";

As the reference to the  HttpContext.Current in the above line suggests this code was planned to be used only from a web context.

The requirements were changed and we had to extend the solution with an after (-ed) list item event receiver. How could we use the same set of constants in the new code?

If we would have a reference to the right SPSite instance, we could get the resource file location like this:

  1. SPIisSettings iisSet = (SPIisSettings)site.WebApplication.IisSettings[SPUrlZone.Default];
  2. String relativePath = String.Format("\\App_GlobalResources\\{0}.resx", resourceFile);
  3. String rsPath = String.Format("{0}{1}", iisSet.Path.FullName, relativePath);

But passing the site as a parameter to the static properties makes no sense, so I tried to find instead a workaround.

First idea was to play with my favorite Reflection and walk up on the call stack in the GetResFieldByKey method to get the SPSite reference from the SPItemEventProperties parameter of the ItemUpdated method (like properties.Web.Site) but it would not be a great idea due to the performance costs of the solution. Then I started experimenting with HttpContext and SPContext.

See the following block of code that I’ve included in ItemUpdated. First part is only to validate there is really neither HttpContext nor SPContext at this point. It will get importance after we create our dummy contexts to check that out injected context do not remain attached to the process on subsequent events. The second part of the code is about creating and injecting the contexts:

  1. try
  2. {
  3.     HttpContext currentContext = HttpContext.Current;
  4.     Trace.TraceInformation("Try to get HTTP context");
  5.  
  6.     if (currentContext != null)
  7.     {
  8.         // this block is only to validate there is
  9.         // neither HttpContext nor SPContext when event receiver method
  10.         Trace.TraceInformation("Try to get SP context");
  11.  
  12.         SPContext context = SPContext.Current;
  13.         if ((context != null) && (context.Web != null))
  14.         {
  15.             Trace.TraceInformation("SPWeb URL: {0}", context.Web.Url);
  16.             Trace.TraceInformation("SPWeb user: {0}", context.Web.CurrentUser);
  17.         }
  18.         else
  19.         {
  20.             Trace.TraceInformation("HTTP context found but no usable SP context");
  21.         }
  22.     }
  23.     else
  24.     {
  25.         Trace.TraceInformation("Create dummy HTTP context");
  26.         HttpRequest request =
  27.              new HttpRequest(string.Empty, properties.WebUrl, string.Empty);
  28.  
  29.         HttpResponse response = new HttpResponse(
  30.              new System.IO.StreamWriter(new System.IO.MemoryStream()));
  31.  
  32.         HttpContext dummyContext = new HttpContext(request, response);
  33.         // these lines required to inject SPContext as well
  34.         // if you don't need that it can be deleted
  35.         if (properties.Web != null)
  36.         {
  37.             dummyContext.Items["HttpHandlerSPWeb"] = properties.Web;
  38.         }
  39.         HttpContext.Current = dummyContext;
  40.     }
  41.  
  42.     // here we try to get a static value bound to a resource string
  43.     String myList = Lists.MyList;
  44.     Trace.TraceInformation("MyList list name: {0}", myList);
  45.  
  46.     base.ItemUpdated(properties);
  47. }
  48. catch (Exception ex)
  49. {
  50.     Trace.TraceError("Exception: '{0}', '{1}', '{2}'", ex.Message, ex.InnerException, ex.StackTrace);
  51. }

The next code is included in the static ResXResourceSet method. Parts of the code are similar to the above one. Of course, you don’t have to include the lines used only to trace out the state of the contexts. I used these lines only to validate the contexts do not exists when entering into the event handler method but they are there in the place we need them (in this case on resource reading) after they got injected.

  1. String rsPath = null;
  2.  
  3. HttpContext currentContext = HttpContext.Current;
  4. Trace.TraceInformation("Try to get HTTP context");
  5.  
  6. if (currentContext != null)
  7. {
  8.     rsPath = HttpContext.Current.Server.MapPath("~/App_GlobalResources/PORequest") + "\\" + resourceFile + ".resx";
  9.     try
  10.     {
  11.         Trace.TraceInformation("Try to get SP context");
  12.  
  13.         SPContext context = SPContext.Current;
  14.         if ((context != null) && (context.Web != null))
  15.         {
  16.             Trace.TraceInformation("SPWeb URL: {0}", context.Web.Url);
  17.             Trace.TraceInformation("SPWeb user: {0}", context.Web.CurrentUser);
  18.         }
  19.         else
  20.         {
  21.             Trace.TraceInformation("HTTP context found but no usable SP context");
  22.         }
  23.     }
  24.     catch (Exception ex)
  25.     {
  26.         Trace.TraceError("Exception: '{0}', '{1}', '{2}'", ex.Message, ex.InnerException, ex.StackTrace);
  27.     }
  28. }
  29. else
  30. {
  31.     Trace.TraceInformation("No HTTP context");
  32. }
  33. Trace.TraceInformation("Path: " + rsPath);

Again, this method is only presented here as a technological curiosity, use it at your own risk.

August 6, 2010

Event receiver “inheritance” between parent and child content types

Filed under: Content types, Event receivers, SP 2010 — Tags: , , — Peter Holpar @ 23:02

On the week I got a task for creating a simple routing workflow. Forms including different pieces of information are submitted by users and this data must be approved by another users. The workflow itself is not important here, the main point is that the data submitted is different but the workflow should be common.

We already solved similar workflows utilizing item event receiver, but in that cases the receiver was bound to the list. Now I planned that the workflow would be associated with a parent content type that would contain only the fields required by the workflow, and child content types would be “inherited” from the parent to add form data.

It sounds good, but to tell the truth I’ve never tried that before and found no information on the web (except this MSDN forum thread after my tests) about whether the event receiver settings would be propagated to child content types or not.

So, I’ve created a simple event receiver that only trace out the content type information of the new item when it was added to the list:

  1. using System;
  2. using System.Security.Permissions;
  3. using Microsoft.SharePoint;
  4. using System.Diagnostics;
  5.  
  6. namespace SimpleEventReceiver
  7. {
  8.     /// <summary>
  9.     /// List Item Events
  10.     /// </summary>
  11.     public class SimpleEventReceiver : SPItemEventReceiver
  12.     {
  13.        /// <summary>
  14.        /// An item was added.
  15.        /// </summary>
  16.        public override void ItemAdded(SPItemEventProperties properties)
  17.        {
  18.            Trace.TraceInformation("Content type: {0}", properties.ListItem.ContentType.Name);           
  19.            base.ItemAdded(properties);
  20.        }
  21.  
  22.     }
  23. }

Next, I’ve created a new content type called EventReceiverParent on the UI derived from the Item parent content type, and registered the event receiver using a simple command line tool.

RegisterContentTypeReceiver(web, "EventReceiverParent");

Where the RegisterContentTypeReceiver method looks like this:

  1. private void RegisterContentTypeReceiver(SPWeb web, String contentTypeName)
  2.         {
  3.             Console.WriteLine("Registering: {0}", contentTypeName);
  4.             SPContentType contentType = web.ContentTypes[contentTypeName];
  5.  
  6.             SPEventReceiverDefinition eventReceiverDefinition = contentType.EventReceivers.Add();
  7.             eventReceiverDefinition.Type = SPEventReceiverType.ItemAdded;
  8.             eventReceiverDefinition.Assembly = "SimpleEventReceiver, Version=1.0.0.0, Culture=neutral, PublicKeyToken=624649cd872eaad1, processorArchitecture=MSIL";
  9.             eventReceiverDefinition.Class = "SimpleEventReceiver.SimpleEventReceiver";
  10.             eventReceiverDefinition.SequenceNumber = 1000;
  11.  
  12.             eventReceiverDefinition.Update();
  13.             // use the following if you would like only new child content types to inherit setting
  14.             // but not existing ones:
  15.             //contentType.Update();
  16.             contentType.Update(true);
  17.  
  18.             DisplayContentTypeReceivers(web, contentTypeName);
  19.         }

The DisplayContentTypeReceivers displays the event receiver registration for the specified content type:

  1. private void DisplayContentTypeReceivers(SPWeb web, String contentTypeName)
  2. {
  3.     Console.WriteLine("Display: {0}", contentTypeName);
  4.     SPContentType contentType = web.ContentTypes[contentTypeName];
  5.     foreach (SPEventReceiverDefinition eventReceiverDefinition in contentType.EventReceivers)
  6.     {
  7.         Console.WriteLine("Assembly: '{0}', Class: {1}, SequenceNumber: {2}",
  8.             eventReceiverDefinition.Assembly,
  9.             eventReceiverDefinition.Class,
  10.             eventReceiverDefinition.SequenceNumber);
  11.     }
  12. }

After this, I’ve created a child content type called EventReceiverChild on the UI, and displayed the event receivers for that:

DisplayContentTypeReceivers(web, "EventReceiverChild");

The result showed that the receiver information was inherited to the child content type.

I found the same, when created another child content type called called EventReceiverChild2 using code:

AddContentType(web, "EventReceiverParent", "EventReceiverChild2");

Where the AddContentType method looks like this:

  1. private void AddContentType(SPWeb web, String parentContentTypeName, String childContentTypeName)
  2. {
  3.     Console.WriteLine("Creating {0} based on {1}", childContentTypeName, parentContentTypeName);
  4.     SPContentType childContentType = new SPContentType(web.ContentTypes[parentContentTypeName], web.ContentTypes, childContentTypeName);
  5.     web.ContentTypes.Add(childContentType);
  6.  
  7.     DisplayContentTypeReceivers(web, childContentTypeName);
  8. }

When you register a new event receiver for the parent content type, and update the content type using the Update() or Update(false) method instead of Update(true), then I found that the changes are not propagated down to the children content types.

When I assigned the content types to a list, and created new items for each of the content types, I found that the event receiver is triggered for each and the name of the right content type is traced out.

The result of my tests shows that utilizing an event receiver bound to a parent content type is a practical way to create simple workflows for children content types.

Note, that I made the tests on SharePoint 2010. There might be differences when you work on WSS 3.0 / MOSS 2007.

April 27, 2010

A simple demo application of Office 2010 and SharePoint 2010 development using Visual Studio 2010

In the past few weeks I had not much time for blogging. Beyond my general daily work and a few strict deadlines I was preparing for my presentation about VS 2010 based SharePoint and Office 2010 development. To compensate the low number of blog posts in the last month I would like to share the demo application with you.

This demo was presented at the Hungarian Visual Studio 2010 Launch event on 12th April 2010. The post is rather long, but I hope it is worth to read. You find the sample code here, so you can play with it at your environment if you wish.

There are two VS 2010 solutions in the demo. One for the SharePoint part, and the other one for Office.

Before going to deep into the technical details, let’s see the overview of the solution from a higher point of view. We store the lists of F1 races and F1 drivers in SharePoint lists. Our Excel add-in will connect to the list of F1 drivers and “download” the items to the Excel sheet. After we set the time results for each of the pilots, we can compute the result of the race. Then we can go to the our custom backstage view and select the race we set the results for. Finally we can publish the results back to the server where first a Word document is generated based on the result and it is converted to a PDF document.

After this short introduction, let’s start with the SharePoint solution!

The solution assumes your SharePoint server is called SP2010 and there is a F1Site web for the demo. If you have other configuration, you should alter the Site URL property of the project before deployment.

In this solution we create  two list definitions, one for the F1 pilots (including name of the pilot as Title, nationality as Country Code, Constuctor and Engine fields) and another one for F1 races. This list includes name of the GP as Title, City for the city of the race, RaceDate for the exact date and time of the race, CircuitImage for the image of the circuit (not mandatory, used only in a few races for illustration purposes) and Result field to store the result of the race in XML format.

We create two list instances based on these list definitions to upload the initial data for our application, as shown on the images below:

image

image

Additionally, we create a document library (called DocConverter) to store generated Word documents and converted PDF documents.

I created an event receiver for the F1 Races list to generate a Word document into the DocConverter library when the results are published and another event receiver on the DocConverter to do the conversion when the Word files are generated. More on these event receivers later.

There is a mapped folder for the flag and circuit images we need in the demo.

I’ve included a feature receiver in the solution just to show it is possible so easily in VS 2010. It’s commented out by default in the sample code, feel free to use it if you would like to see it in action:

  1. public override void FeatureActivated(SPFeatureReceiverProperties properties)
  2. {
  3.     //SPWeb web = (SPWeb)properties.Feature.Parent;
  4.     //web.Webs.Add("VS2010Launch", "VS 2010 Launch",
  5.     //    "VS 2010 Launch Web Site", 1033, SPWebTemplate.WebTemplateSTS,
  6.     //    false, false);
  7. }
  8.  
  9.  
  10. // Uncomment the method below to handle the event raised before a feature is deactivated.
  11.  
  12. public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
  13. {
  14.     //SPWeb web = (SPWeb)properties.Feature.Parent;
  15.     //web.Webs["VS2010Launch"].Delete();
  16. }

Finally, there is a simple Visual Web Part item in the project that utilizes the data included in the application and illustrates working with LINQ on SharePoint data.

To generate the helper class I used SPMetal:

"C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\BIN\SPMetal" /web:http://SP2010/F1Site /code:F1SiteLinq.cs
/namespace:VS2010Launch.F1Results  /language:csharp /parameters:SPMetal.xml

And this is the configuration file for SPMetal:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <Web AccessModifier="Internal" xmlns="http://schemas.microsoft.com/SharePoint/2009/spmetal">
  3.     <List Name="F1 Races">
  4.         <ContentType Name="Item" Class="F1Race">
  5.             <Column Name="City" Member="City" />
  6.             <Column Name="RaceDate" Member="RaceDate" />
  7.             <Column Name="CircuitImage" Member="CircuitImage" />
  8.         </ContentType>
  9.     </List>
  10.     <ExcludeOtherLists />
  11. </Web>

Using the generated helper class accessing our data stored in SharePoint is as simple as this code shows you:

  1. public partial class F1ResultsUserControl : UserControl
  2. {
  3.     protected void Page_Load(object sender, EventArgs e)
  4.     {
  5.         F1SiteLinqDataContext dataContext = new F1SiteLinqDataContext(SPContext.Current.Web.Url);
  6.         EntityList<F1Race> f1Races = dataContext.GetList<F1Race>("F1 Races");
  7.         var allF1Races = from f1Race in f1Races
  8.                             orderby f1Race.Id
  9.                             select f1Race;
  10.  
  11.         bool initialized = false;
  12.         foreach (var race in allF1Races)
  13.         {
  14.             ListItem raceListItem = new ListItem(race.Title, race.Id.ToString());
  15.             if ((!initialized) && (!IsPostBack))
  16.             {
  17.                 initialized = true;
  18.                 SetValues(race);
  19.             }
  20.             Races.Items.Add(raceListItem);
  21.         }            
  22.     }
  23.  
  24.     private void SetValues(F1Race race)
  25.     {
  26.         NoMapLabel.Visible = String.IsNullOrEmpty(race.CircuitImage);
  27.         CircuitImage.Visible = !NoMapLabel.Visible;
  28.         CircuitImage.ImageUrl = String.Format("/F1Site/_layouts/images/VS2010Launch/Circuits/{0}.png", race.CircuitImage);
  29.         City.Text = race.City;
  30.         RaceDate.Text = race.RaceDate.Value.ToString("yyyy.MM.dd HH.mm");
  31.     }
  32.  
  33.     protected void Races_SelectedIndexChanged(object sender, EventArgs e)
  34.     {
  35.         DropDownList racesDropDown = (DropDownList)sender;
  36.         int f1RaceId = int.Parse(racesDropDown.SelectedValue);
  37.         F1SiteLinqDataContext dataContext = new F1SiteLinqDataContext(SPContext.Current.Web.Url);
  38.         EntityList<F1Race> f1Races = dataContext.GetList<F1Race>("F1 Races");
  39.         var selectedRace = (from f1Race in f1Races
  40.                             where f1Race.Id == f1RaceId
  41.                             orderby f1Race.Id
  42.                             select f1Race).First();
  43.         SetValues(selectedRace);
  44.     }
  45. }

After deploying our project to SharePoint we have to add the web part to a web part page first, as shown below:

image

The following figure shows the web part in action. Here you can select the race from the drop down list, and the city, date and image (if there is one) are updated based on the selection.

image

Let’s switch to our Office sample project that is based on the Excel 2010 Add-in project template, to see it in action and to check some of its most interesting parts in the implementation.

The goal of the project was to illustrate the UI extensibility (both the “classical” ribbon and the new backstage view) of the Office applications from code and to show how easy it is to get and publish data in Office applications from and to SharePoint 2010.

I’ve added a Ribbon (XML) item to the project, and customized both the code and the XML definition.

When starting the project, the new ribbon item looks like this:

image

In the ribbon the flag and helmet icons are stored as resource images in the project and provided through the following method in the code:

  1. public System.Drawing.Image GetImage(IRibbonControl control)
  2. {
  3.     if (control.Id == "getPilotsButton")
  4.     {
  5.         return Properties.Resources.Helmet;
  6.     }
  7.     else
  8.     {
  9.         return Properties.Resources.Flag;
  10.     }
  11. }

The method is bound to the buttons through the getImage attribute in the XML file:

  1. <button id="getPilotsButton" size="large" label="Get Pilots" screentip="Gets the list of pilots from the SharePoint site" getImage="GetImage" onAction="GetPilotsButton_OnAction" />
  2. <button id="computeResultsButton" getEnabled="IsConnected" size="large" label="Compute Results" screentip="Compute the order" getImage="GetImage" onAction="ComputeResultsButton_OnAction" />

To access SharePoint data we should first specify the location of data and credential we would like to use on connection. If you check the Ask for credential checkbox, you can set connection parameters in the following dialog box when you press the Get Pilots button to populate worksheets with F1 driver data.

image

Connection parameters are stored as user scoped settings. The Password is stored encoded using a helper method defined in our static Util class and decoded when its value is requested by the code.

  1. [global::System.Configuration.UserScopedSettingAttribute()]
  2. [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
  3. [global::System.Configuration.DefaultSettingValueAttribute("Password")]
  4. public string Password {
  5.     get {
  6.         return (Util.Decrypt((string)(this["Password"])));
  7.     }
  8.     set {
  9.         this["Password"] = Util.Encrypt(value);
  10.     }
  11. }

The GetCredentials method is called several times from the code to get the credentials we specified for the connection:

  1. private ICredentials GetCredentials()
  2. {
  3.     if (Properties.Settings.Default.CurrentCredentials)
  4.     {
  5.         return CredentialCache.DefaultCredentials;
  6.     }
  7.     else
  8.     {
  9.         String userName = Properties.Settings.Default.UserName;
  10.         String domainName = String.Empty;
  11.         int pos = userName.IndexOf(@"\");
  12.         if (pos > -1)
  13.         {
  14.             domainName = userName.Substring(0, pos);
  15.             userName = userName.Substring(pos + 1);
  16.         }
  17.         return new NetworkCredential(
  18.             userName,
  19.             Properties.Settings.Default.Password,
  20.             domainName
  21.             );
  22.     }
  23. }

I’ve accessed SharePoint data from the Excel add-in using the REST service of SharePoint as discussed in this post of Randy Williams. As described there, if you work with SharePoint beta 2, you should install ADO.NET Data Services v1.5 CTP2.

Using the F1 Pilots SharePoint list as data source the code that populates the Excel cells with data looks like this:

  1. public void GetPilotsButton_OnAction(IRibbonControl control)
  2. {
  3.     DialogResult result = DialogResult.OK;
  4.     // we ask for credentials only if the user would like to change the stored values
  5.     if (_askForCredentials)
  6.     {
  7.         ConnectForm connectForm = new ConnectForm();
  8.         result = connectForm.ShowDialog();
  9.     }
  10.     if (result == DialogResult.OK)
  11.     {
  12.         try
  13.         {
  14.             _isConnected = true;
  15.             F1SiteDataContext dataContext = new F1SiteDataContext(
  16.                 new Uri(String.Format(@"{0}/_vti_bin/listdata.svc", Properties.Settings.Default.SiteUrl)));
  17.             dataContext.Credentials = GetCredentials();
  18.             var f1Pilots = from f1Pilot in dataContext.F1Pilots
  19.                             //orderby f1Pilot.Title
  20.                             select new
  21.                             {
  22.                                 f1Pilot.Title,
  23.                                 f1Pilot.CountryCode,
  24.                                 f1Pilot.Constructor,
  25.                                 f1Pilot.Engine
  26.                             };
  27.  
  28.             _f1Races = (IEnumerable<F1RacesItem>)
  29.                                             from f1Race in dataContext.F1Races
  30.                                             //orderby f1Race.RaceDate
  31.                                             select new F1RacesItem
  32.                                             {
  33.                                                 ID = f1Race.ID,
  34.                                                 Title = f1Race.Title,
  35.                                                 City = f1Race.City,
  36.                                                 RaceDate = f1Race.RaceDate,
  37.                                                 CircuitImage = f1Race.CircuitImage
  38.                                             };
  39.             if ((_f1Races != null) && (_f1Races.Count() > 0))
  40.             {
  41.                 _currentRace = _f1Races.ElementAt(0);
  42.             }
  43.  
  44.             _ribbon.InvalidateControl("computeResultsButton");
  45.             _ribbon.InvalidateControl("backstage");
  46.  
  47.             Microsoft.Office.Interop.Excel.Application excelApp = _addIn.Application;
  48.             Workbook workbook = excelApp.ActiveWorkbook;
  49.  
  50.             if (workbook != null)
  51.             {
  52.                 Worksheet worksheet = (Worksheet)workbook.Sheets[1];
  53.                 worksheet.Activate();
  54.                 worksheet.Cells[1, 1] = "Position";
  55.                 worksheet.Cells[1, 2] = "Driver";
  56.                 worksheet.Range["B1:B1"].ColumnWidth = 20;
  57.                 worksheet.Cells[1, 3] = "Nationality";
  58.                 worksheet.Range["C1:C1"].ColumnWidth = 12;
  59.                 worksheet.Cells[1, 4] = "Constructor";
  60.                 worksheet.Range["D1:D1"].ColumnWidth = 20;
  61.                 worksheet.Cells[1, 5] = "Engine";
  62.                 worksheet.Range["E1:E1"].ColumnWidth = 10;
  63.                 worksheet.Cells[1, 6] = "Time";
  64.                 _pilotCount = f1Pilots.Count();
  65.                 int rowCount = 2;
  66.                 foreach (var f1Pilot in f1Pilots)
  67.                 {
  68.                     worksheet.Cells[rowCount, 2] = f1Pilot.Title;
  69.                     worksheet.Cells[rowCount, 3] = f1Pilot.CountryCode;
  70.                     worksheet.Cells[rowCount, 4] = f1Pilot.Constructor;
  71.                     worksheet.Cells[rowCount, 5] = f1Pilot.Engine;
  72.                     rowCount++;
  73.                 }
  74.                 worksheet.Range["A1:F1"].Select();
  75.                 ((Range)excelApp.Selection).Font.Bold = true;
  76.                 ((Range)excelApp.Selection).HorizontalAlignment = Microsoft.Office.Interop.Excel.XlHAlign.xlHAlignCenter;
  77.                 worksheet.Range[worksheet.Cells[2, 6], worksheet.Cells[rowCount – 1, 6]].Select();
  78.                 ((Range)excelApp.Selection).NumberFormat = "h:mm:ss";
  79.  
  80.             }
  81.             else
  82.             {
  83.                 MessageBox.Show("No active workbook!", "Warning",
  84.                 MessageBoxButtons.OK,
  85.                 MessageBoxIcon.Warning,
  86.                 MessageBoxDefaultButton.Button1);
  87.             }
  88.         }
  89.         catch (Exception ex)
  90.         {
  91.             _isConnected = false;
  92.             _ribbon.InvalidateControl("computeResultsButton");
  93.             MessageBox.Show(ex.Message, "Error",
  94.                 MessageBoxButtons.OK,
  95.                 MessageBoxIcon.Error,
  96.                 MessageBoxDefaultButton.Button1);
  97.         }
  98.     }
  99. }

The image below shows the populated worksheet. Note that the Compute Results button is now enabled.

image

After you type the time results or copy them from the results.txt file from the project folder to make your life a bit easier, you can click the Compute Results button that compute the order based on the time.

image

To tell the truth there is nothing extra in this action, since it is only populate the cells of the first column with an Excel formula. Of course we could do that in the same action we get the pilots in the first step, but it simply looks better this way in the demo.

  1. public void ComputeResultsButton_OnAction(IRibbonControl control)
  2. {
  3.     Microsoft.Office.Interop.Excel.Application excelApp = _addIn.Application;
  4.     Workbook workbook = excelApp.ActiveWorkbook;
  5.  
  6.     if (workbook != null)
  7.     {
  8.         Worksheet worksheet = (Worksheet)workbook.Sheets[1];
  9.         worksheet.Activate();
  10.         for (int rowCount = 2; rowCount <= _pilotCount + 1; rowCount++)
  11.         {
  12.             worksheet.Cells[rowCount, 1] = String.Format(@"= 1 + COUNTIF(F$2:F${0}, ""<"" & F{1})",
  13.                 _pilotCount, rowCount);
  14.         }
  15.     }
  16. }

After you have the results, you can switch to the backstage view where there is a new tab called Publish F1 Results. You can select the race you would like to publish results for. The data of the race, like location, date and image of the circuit if it is specified will be displayed. If the race is in the future, the Publish Results button is disabled.

When a race is selected from the dropdown list we set the selected race, then invalidate all of the controls that must be repainted to reflect the change:

  1. public void RaceSelected(IRibbonControl control, String itemId, int itemIndex)
  2. {
  3.     _currentRace = _f1Races.ElementAt(itemIndex);
  4.     _ribbon.InvalidateControl("raceLocation");
  5.     _ribbon.InvalidateControl("raceDate");
  6.     _ribbon.InvalidateControl("circuitImage");
  7.     _ribbon.InvalidateControl("noMapLabel");
  8.     _ribbon.InvalidateControl("publish");
  9. }

For example, to get the circuit image from the images folder of our SharePoint server using the WebClient class:

  1. public System.Drawing.Image GetCircuitImage(IRibbonControl control)
  2. {
  3.     if (GetImageVisible(control))
  4.     {
  5.         WebClient webClient = new WebClient();
  6.         webClient.Credentials = GetCredentials();
  7.         byte[] imageBytes = webClient.DownloadData(
  8.             String.Format(@"{0}/_layouts/images/VS2010Launch/circuits/{1}.png",
  9.             Properties.Settings.Default.SiteUrl,
  10.             _currentRace.CircuitImage));
  11.         MemoryStream imageStream = new MemoryStream(imageBytes);
  12.         System.Drawing.Image image = System.Drawing.Image.FromStream(imageStream);
  13.         System.Drawing.Image circuitImage = image.GetThumbnailImage((int)(image.Width * 0.7), (int)(image.Height * 0.7), null, System.IntPtr.Zero);
  14.         return circuitImage;
  15.     }
  16.     else
  17.     {
  18.         return null;
  19.     }
  20. }

The method is mapped again to the imageControl through the getImage attribute in the XML file:

  1. <imageControl id="circuitImage" getVisible="GetImageVisible" getImage="GetCircuitImage"/>

If you press the Publish Results button the results are published to the F1 Races list to the matching list item in the Result field as an XML text.

image

In the code we use again our data source created earlier to get the matching race item and update its Result field with the result XML:

  1. public void PublishResults_OnAction(IRibbonControl control)
  2. {
  3.     F1SiteDataContext dataContext = new F1SiteDataContext(
  4.                 new Uri(String.Format(@"{0}/_vti_bin/listdata.svc", Properties.Settings.Default.SiteUrl)));
  5.     dataContext.Credentials = GetCredentials();
  6.     var f1RaceCurrent = (from f1Race in dataContext.F1Races
  7.                             where f1Race.ID == _currentRace.ID
  8.                             select f1Race).First();
  9.     f1RaceCurrent.Result = GetResultXml();
  10.     dataContext.UpdateObject(f1RaceCurrent);
  11.     DataServiceResponse response = dataContext.SaveChanges();
  12.     MessageBox.Show("Result saved", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information);
  13.  
  14. }

And at this point we are back to SharePoint since updating the race item triggers our event receiver that generate the Word document. The F1RaceResultReceiver project item in the SharePoint solution contains the ResultDocCreator class that receives the result XML document and generates the Word document based on that. The code of this class is quite long and even more complicated, but I have to admit that I’ve not created that from scratch.

Note: To be able to run this code, you have to install the Open XML SDK 2.0 for Office on the SharePoint front-end.

First I’ve created a Word document that seems like the one I would like to generate. For this “prototype” I used the standard Word application UI.

Next I started the Open XML SDK 2.0 for Office Tool, opened my document and used the Reflect Code tab to get the code I need.

image

I used that code as a base version, but I had to work a bit more on that. Since it included separate code blocks with the same patterns for each items, I had to modify that to use iteration and loops for the same type of code. For example, original code included table row generation for all of the pilots. Instead of that I needed a single code block that generates a table row based on pilot data and call that code block for each of the pilots.

Probably one of the most interesting part of the code is the method that gets the binary content of the flag .png files from the images folder on SharePoint:

  1. private void GenerateImagePart1Content(ImagePart imagePart1, String nationality)
  2. {
  3.     String url = String.Format(@"{0}/_layouts/images/VS2010Launch/flags/{1}.png", _webUrl, nationality);
  4.     WebClient webClient = new WebClient();
  5.     webClient.UseDefaultCredentials = true;
  6.     byte[] imageBytes = webClient.DownloadData(url);
  7.     MemoryStream imageStream = new MemoryStream(imageBytes);
  8.     imagePart1.FeedData(imageStream);
  9.     imageStream.Close();
  10. }

When the Word document is generated its binary content is saved into the DocConverter library on SharePoint, as illustrated by the code of the event receiver:

  1. public override void ItemUpdated(SPItemEventProperties properties)
  2. {
  3.     SPListItem item = properties.ListItem;
  4.     SPWeb web = properties.Web;
  5.     String webUrl = properties.WebUrl;
  6.  
  7.     if (!(String.IsNullOrEmpty((String)item["Result"])))
  8.     {
  9.  
  10.         ResultDocCreator generator = new ResultDocCreator();
  11.  
  12.         String resultXmlString = (String)item["Result"];
  13.         MemoryStream stream = new MemoryStream();
  14.         generator.CreatePackage(stream, webUrl, resultXmlString);
  15.  
  16.         XmlDocument resultXml = new XmlDocument();
  17.         resultXml.LoadXml(resultXmlString);
  18.  
  19.         XmlNode raceNode = resultXml.SelectSingleNode("/Race");
  20.  
  21.         String docName = ResultDocCreator.GetStringAttribute(
  22.             raceNode, "title", null, true) + ".docx";
  23.  
  24.         SPList docConverter = web.Lists["DocConverter"];
  25.  
  26.         // just save the file, PDF conversion will be completed
  27.         // by the DocConverterReceiver event receiver
  28.         SPFile file = docConverter.RootFolder.Files.Add(
  29.             docName, stream, true);
  30.            
  31.     }
  32.     base.ItemUpdated(properties);
  33. }

Note: To be able to run the converter code, you have to create and configure a Word Automation Services service application on your SharePoint server. Since the conversation process runs asynchronously on the server I set the frequency to 1 minute to make the service more responsive on the demo.

image

I’ve created an event receiver bound to the DocConverter library. All this receiver does is to register a new PDF conversation job when a .docx document is saved into the library:

  1. public override void ItemAdded(SPItemEventProperties properties)
  2. {
  3.     SPListItem listItem = properties.ListItem;
  4.  
  5.     if ((listItem.Name.Length > 5) &&
  6.         (listItem.Name.Substring(listItem.Name.Length – 5).ToLower() == ".docx"))
  7.     {
  8.  
  9.         SPWeb web = properties.Web;
  10.         SPList list = properties.List;
  11.  
  12.         try
  13.         {
  14.  
  15.             EventFiringEnabled = false;
  16.  
  17.             // name of the configured service application for Word Automation Services
  18.             ConversionJob job = new ConversionJob("Word Automation Services");
  19.             job.UserToken = web.CurrentUser.UserToken;
  20.             job.Settings.UpdateFields = true;
  21.             job.Settings.OutputSaveBehavior = SaveBehavior.AlwaysOverwrite;
  22.             job.Settings.OutputFormat = SaveFormat.PDF;
  23.             String wordFile = String.Format("{0}/{1}", web.Url, listItem.Url);
  24.             String pdfFile = String.Format("{0}/{1}.pdf",
  25.                 web.Url, listItem.Url.Substring(0, listItem.Url.Length – 5));
  26.  
  27.             job.AddFile(wordFile, pdfFile);
  28.             job.Start();
  29.  
  30.  
  31.         }
  32.         finally
  33.         {
  34.             EventFiringEnabled = true;
  35.         }
  36.         base.ItemAdded(properties);
  37.     }
  38. }

You should wait a minute or two to let the conversation process complete.

At this point the DocConverter library should contain both the Word document and the PDF document for the race results.

image

Saving these files to your local machine and opening them you can see the results in Word:

image

Or with the very same content in Adobe Reader:

image

Thanks to Wikipedia authors for free circuit and flag images and to my F1-fun son for quality assurance all the data included in the demo applications.

Older Posts »

Blog at WordPress.com.