Second Life of a Hungarian SharePoint Geek

July 20, 2017

Bulk Deletion of Events from a SharePoint Calendar

Filed under: Calendar, SP 2013 — Tags: , — Peter Holpar @ 21:15

Assume you have a SharePoint Calendar with thousands of events, including recurring events, and recurring event exceptions. These ones are the result of aggregating events from several years, most of them are no more relevant.

Problems

If the number of events is over the List View Threshold limit, your users will not be able to access the All Events view (see sample screenshot below taken from another calendar including a standard and a recurring event, a deleted recurring event and a recurrence exception),

image

they receive instead that an error message:

The attempted operation is prohibited because it exceeds the list view threshold enforced by the administrator.

They won’t be able even to delete the calendar, either from the Site Content page (they get a warning like “We’re sorry, we had some trouble removing this. You can try again from the settings page.”):

image

image

or from the Settings page of the list (same error as above):

image

Of course, you as an administrator, can increase the List View Threshold limit, delete the list via PowerShell (see below), or even from the web UI if you log in using the farm account, but it takes more time for you and for your users.

$web = Get-SPWeb http://YourSharePoint/Web/SubWeb
$calendar= $web.lists["YourCalendar"]
$calendar.Delete()

Even if the number of events stays below the List View Threshold limit, the users might have experience performance problems.

Solution

Note, that we did not want to delete the list, the limitation with the deletion was only one of the examples. Instead of deletion, we wanted to drastically reduce the number of events by deleting all events that are no more relevant in the current year. That means, deleting all single events that were finished earlier than this year, and repeating events whose repetitions is finished earlier than the current year.

Based on my experience the deletion of such high number of items is not very performant, so I decided to delete the items in batches. There are several examples for that on the web, including solutions for C# (see here or here) or for PowerShell (see here) or for both of them (see this one). I planned to use PowerShell, but the examples I found were typically simple translations of the C# version, without using the structures and features available in PowerShell. Even worse, both the C# and PowerShell implementations out there have a serious limitation: they either output all of the results returned by the ProcessBatchData method, or they display no information at all. It might be OK as long as you successfully delete all of the items, but if you have any problem there, I wish you a good luck to find any usable information about it, if there are really of thousands of items in your list. So I created my own PowerShell implementation using the samples available. Note, that because I don’t want to wait for a feedback about the success of the deletion until all items are processed (we had over some 10k items there), I’m deleting the items in smaller batches, in the code below it is 1000 items / batch.

To get the items I should delete, I used the following CAML query:

<Where>
    <Lt>
    <FieldRef Name=’EndDate’ />
    <Value Type=’DateTime’>2017-01-01 00:00:00</Value>
    </Lt>
</Where>

Here is the full code of the first version of my script, see how I process the results by splitting the response XML to successfully deleted items an failures, and displaying only the latter ones:

  1. # modify the $url and $listTitle values to match your configuration
  2. $url = "http://YourSharePoint/Web/SubWeb&quot;
  3. $listTitle = "YourCalendar"
  4.  
  5. $web = Get-SPWeb $url
  6. $list = $web.Lists[$listTitle]
  7.  
  8. $query = New-Object Microsoft.SharePoint.SPQuery
  9.   $query.Query =
  10.           "<Where>
  11.              <Lt>
  12.                <FieldRef Name='EndDate' />
  13.                <Value Type='DateTime'>2017-01-01 00:00:00</Value>
  14.              </Lt>
  15.            </Where>"
  16. $query.ViewFields = "<FieldRef Name='ID' />"
  17. $query.ViewFieldsOnly = $true
  18. $query.RowLimit = 1000;
  19.  
  20. $itemCount = 0
  21. $listId = $list.ID
  22.  
  23. do
  24. {
  25.     $listItems = $list.GetItems($query)
  26.     $itemIds = $listItems | % { [String]$_.ID }
  27.     [System.Text.StringBuilder]$batchXml = New-Object "System.Text.StringBuilder"
  28.     [Void]$batchXml.Append("<?xml version=`"1.0`" encoding=`"UTF-8`"?><Batch>")
  29.     $itemIds | % {
  30.       $itemId = $_
  31.       [Void]$batchXml.Append("<Method ID=`"$itemId`"><SetList>$listId</SetList><SetVar Name=`"ID`">$itemId</SetVar><SetVar Name=`"Cmd`">Delete</SetVar></Method>")
  32.     }
  33.     [Void]$batchXml.Append("</Batch>")
  34.     Write-Host Deleting next $listItems.Count entries…
  35.  
  36.     $result = [Xml]$web.ProcessBatchData($batchXml.ToString())
  37.     $success = @(Select-Xml -Xml $result -XPath '//Results/Result[@Code="0"]')
  38.     $failure = @(Select-Xml -Xml $result -XPath '//Results/Result[@Code!="0"]')
  39.     $itemCount += $success.Count
  40.     # list errors
  41.     $failure | % {
  42.         $errorNode = $_.Node
  43.         Write-Host Error deleting entry with ID $errorNode.ID error code: $errorNode.Code error text: $errorNode.ErrorText
  44.     }
  45. }
  46. while ($listItems.ListItemCollectionPosition -ne $null)
  47.  
  48. Write-Host Summary: $itemCount entries deleted

Although a few events have been really deleted, I started to get the following errors very quickly, and no deletion was performed after that:

image

The error messages above correspond to the following XML response:

<Result ID="" Code="-2147023673">
<ErrorText>The operation failed because an unexpected error occurred. (Result Code: 0x800704c7)</ErrorText></Result>

In the ULS logs I found a lot of such entries:

Batchmgr Method error. Errorcode: 0x1c32cbb0. Error message: The operation failed because an unexpected error occurred. (Result Code: 0x800704c7)

and at the top of the a single entry like this:

Batchmgr Method error. Errorcode: 0x1c32cbb0. Error message: Item does not exist.  The page you selected contains an item that does not exist.  It may have been deleted by another user.

After that, I’ve tried to delete the items using the same CAML query, hoping that I get more information about the failure. I used this script:

  1. # modify the $url and $listTitle values to match your configuration
  2. $url = "http://YourSharePoint/Web/SubWeb&quot;
  3. $listTitle = "YourCalendar"
  4.  
  5. $web = Get-SPWeb $url
  6. $list = $web.Lists[$listTitle]
  7.  
  8. $query = New-Object Microsoft.SharePoint.SPQuery
  9.   $query.Query =
  10.           "<Where>
  11.              <Lt>
  12.                <FieldRef Name='EndDate' />
  13.                <Value Type='DateTime'>2017-01-01 00:00:00</Value>
  14.              </Lt>
  15.            </Where>"
  16. $query.ViewFields = "<FieldRef Name='ID' />"
  17. $query.ViewFieldsOnly = $true
  18. $listItems = $list.GetItems($query)
  19. $itemIDsToDelete = $listItems | % { $_["ID"] }
  20. $totalCount = $itemIDsToDelete.Count   
  21. Write-Host $totalCount item will be deleted
  22. $counter = 1
  23. $itemIDsToDelete | % {
  24.   $itemID = $_
  25.   Write-Host Deleting item with ID $itemID `($counter / $totalCount`)
  26.   $item = $list.GetItemById($itemID)
  27.   $item.Delete()
  28.   $counter++
  29. }

I’ve got similar error messages as earlier for specific items, but the deletion went further than:

Exception calling "Delete" with "0" argument(s): "Item does not exist.
The page you selected contains an item that does not exist.  It may have been
deleted by another user."
At line:19 char:3
+   $item.Delete()
+   ~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException 
    + FullyQualifiedErrorId : SPException

image

I stopped the script after a while, and checked the ULS logs:

Item does not exist.  The page you selected contains an item that does not exist.  It may have been deleted by another user.<nativehr>0x81020016</nativehr><nativestack></nativestack>
SPRequest.GetListItemDataWithCallback2: UserPrincipalName=i:0).w|s-1-5-21-3634847118-1559816030-2180994487-3194, AppPrincipalName= ,pSqlClient=<null> ,bstrUrl=http://YourSharePoint/Web/SubWeb ,bstrListName={2A67D5C3-7AC1-4F3E-AB47-2051CDB94237} ,bstrViewName=<null> ,bstrViewXml=<View Scope="RecursiveAll" ModerationType="Moderator"><Query><Where><Eq><FieldRef Name="ID"></FieldRef><Value Type="Integer">2328</Value></Eq></Where></Query><RowLimit Paged="TRUE">1</RowLimit></View> ,fSafeArrayFlags=SAFEARRAYFLAG_NONE
System.Runtime.InteropServices.COMException: Item does not exist.  The page you selected contains an item that does not exist.  It may have been deleted by another user.<nativehr>0x81020016</nativehr><nativestack></nativestack>, StackTrace:    at Microsoft.SharePoint.SPListItemCollection.EnsureListItemsData()     at Microsoft.SharePoint.SPListItemCollection.get_Count()     at Microsoft.SharePoint.SPList.GetItemById(String strId, Int32 id, String strRootFolder, Boolean cacheRowsetAndId, String strViewFields, Boolean bDatesInUtc, Boolean bExpandQuery)     at Microsoft.SharePoint.SPList.GetItemById(String strId, Int32 id, String strRootFolder, Boolean cacheRowsetAndId, String strViewFields, Boolean bDatesInUtc)     at Microsoft.SharePoint.SPList.GetItemById(String strId, Int32 id, String strR…
…ment.Automation.Runspaces.RunspaceBase.RunActionIfNoRunningPipelinesWithThreadCheck(Action action)     at System.Management.Automation.ScriptBlock.InvokeWithPipe(Boolean useLocalScope, ErrorHandlingBehavior errorHandlingBehavior, Object dollarUnder, Object input, Object scriptThis, Pipe outputPipe, InvocationInfo invocationInfo, Object[] args)     at System.Management.Automation.ScriptBlock.InvokeUsingCmdlet(Cmdlet contextCmdlet, Boolean useLocalScope, ErrorHandlingBehavior errorHandlingBehavior, Object dollarUnder, Object input, Object scriptThis, Object[] args)     at Microsoft.PowerShell.Commands.ForEachObjectCommand.ProcessRecord()     at System.Management.Automation.CommandProcessor.ProcessRecord()     at System.Management.Automation.CommandProcessorBase.DoExecute()     at System.Mana…
Item does not exist.  The page you selected contains an item that does not exist.  It may have been deleted by another user.<nativehr>0x81020016</nativehr><nativestack></nativestack>
SPRequest.DeleteItem: UserPrincipalName=i:0).w|s-1-5-21-3634847118-1559816030-2180994487-3194, AppPrincipalName= ,bstrUrl=http://YourSharePoint/Web/SubWeb ,bstrListName={2A67D5C3-7AC1-4F3E-AB47-2051CDB94237} ,lID=2327 ,dwDeleteOp=3 ,bUnRestrictedUpdateInProgress=False
System.Runtime.InteropServices.COMException: Item does not exist.  The page you selected contains an item that does not exist.  It may have been deleted by another user.<nativehr>0x81020016</nativehr><nativestack></nativestack>, StackTrace:    at Microsoft.SharePoint.SPListItem.DeleteCore(DeleteOp deleteOp)     at Microsoft.SharePoint.SPListItem.Delete()     at CallSite.Target(Closure , CallSite , Object )     at <ScriptBlock>(Closure , FunctionContext )     at System.Management.Automation.Interpreter.LightLambda.RunVoid1[T0](T0 arg0)     at System.Management.Automation.ScriptBlock.InvokeWithPipeImpl(Boolean createLocalScope, ErrorHandlingBehavior errorHandlingBehavior, Object dollarUnder, Object input, Object scriptThis, Pipe outputPipe, InvocationInfo invocationInfo, Object[] args)     a…

What should it mean, that specific items were deleted? I was sure, I work alone on the calendar. Fortunately, I already worked a lot previously with calendar entries, so it took not very long time to find a reason for the problem.

You should know, that if you delete the “master” item of the recurring events in SharePoint (and we had a lot of them in this case), all of the series exception are deleted the same time. As our original query returned this exception items as well, we had errors when we wanted to delete the already deleted items. The second script (without batch deletion) was able to survive it, but the batch deletion script got mad because of that.

What’s the solution for this issue? Let’s fix our CAML query and select only the items that are not recurring event exceptions:

<Where>
    <And>
    <Lt>
        <FieldRef Name=’EndDate’ />
        <Value Type=’DateTime’>2017-01-01 00:00:00</Value>
    </Lt>
    <Lt>
        <FieldRef Name=’EventType’/>
        <Value Type=’Integer’>2</Value>
    </Lt>
    </And>
</Where>

The query for EventType less than 2 originates from my practice, an unofficial documentation can be found here. The event types are defined (at least more or less) in the internal static class Microsoft.SharePoint.ApplicationPages.Calendar.EventType, for example Deleted has a value 3 and Updated has a value of 4.

The updated script:

  1. # modify the $url and $listTitle values to match your configuration
  2. $url = "http://YourSharePoint/Web/SubWeb&quot;
  3. $listTitle = "YourCalendar"
  4.  
  5. $web = Get-SPWeb $url
  6. $list = $web.Lists[$listTitle]
  7.  
  8. $query = New-Object Microsoft.SharePoint.SPQuery
  9.   $query.Query =
  10.           "<Where>
  11.              <And>
  12.                <Lt>
  13.                  <FieldRef Name='EndDate' />
  14.                  <Value Type='DateTime'>2017-01-01 00:00:00</Value>
  15.                </Lt>
  16.                <Lt>
  17.                  <FieldRef Name='EventType'/>
  18.                  <Value Type='Integer'>2</Value>
  19.                </Lt>
  20.              </And>
  21.            </Where>"
  22. $query.ViewFields = "<FieldRef Name='ID' />"
  23. $query.ViewFieldsOnly = $true
  24. $query.RowLimit = 1000;
  25.  
  26. $itemCount = 0
  27. $listId = $list.ID
  28.  
  29. do
  30. {
  31.     $listItems = $list.GetItems($query)
  32.     $itemIds = $listItems | % { [String]$_.ID }
  33.     [System.Text.StringBuilder]$batchXml = New-Object "System.Text.StringBuilder"
  34.     [Void]$batchXml.Append("<?xml version=`"1.0`" encoding=`"UTF-8`"?><Batch>")
  35.     $itemIds | % {
  36.       $itemId = $_
  37.       [Void]$batchXml.Append("<Method ID=`"$itemId`"><SetList>$listId</SetList><SetVar Name=`"ID`">$itemId</SetVar><SetVar Name=`"Cmd`">Delete</SetVar></Method>")
  38.     }
  39.     [Void]$batchXml.Append("</Batch>")
  40.     Write-Host Deleting next $listItems.Count entries…
  41.  
  42.     $result = [Xml]$web.ProcessBatchData($batchXml.ToString())
  43.     $success = @(Select-Xml -Xml $result -XPath '//Results/Result[@Code="0"]')
  44.     $failure = @(Select-Xml -Xml $result -XPath '//Results/Result[@Code!="0"]')
  45.     $itemCount += $success.Count
  46.     # list errors
  47.     $failure | % {
  48.         $errorNode = $_.Node
  49.         Write-Host Error deleting entry with ID $errorNode.ID error code: $errorNode.Code error text: $errorNode.ErrorText
  50.     }
  51. }
  52. while ($listItems.ListItemCollectionPosition -ne $null)
  53.  
  54. Write-Host Summary: $itemCount entries deleted

Using the modified CAML query I had no more errors, BUT according to the comment of Andrey Markeev in this thread, batch deleting only moves the items to the Recycle Bin, instead of really deleting them. That is not optimal for me.

To delete the items from the Recycle Bin as well, there are two main alternative solutions. Either you extend the batch deletion script with removing recycled items from the Recycle Bin, or you perform a cleanup in a second step.

The code samples in the forum thread referred to earlier as well are deleting either all items from the Recycle Bin, or trying to delete the items using the original list item ID, although the ID in the Recycle Bin differs from the original one. That is either not ideal or does not function. If you invoke the Recycle method of a SPListItem instance, the new ID, the transaction ID is returned to you (as a Guid), but this information is unfortunately not available by batch deletion. We had no folder structure in our calendar, so the LeafName property of the SPRecycleBinItem correspond to the server relative URL of the root folder of the source list (where the item was deleted) without the leading slash, and the DirName property corresponds to the list item ID (ending with ‘_.000’), so we can simply make a query for the IDs in the Recycle Bin, and remove them permanently:

$recBin = $web.Site.RecycleBin
$recBinIds = @($recBin | ? { $itemIds -contains $_.LeafName.Trim(‘_.000’) -and $_.DirName -eq $list.RootFolder.ServerRelativeUrl.Trim(‘/’) } | % { $_.ID })
$recBin.Delete($recBinIds)

Note, that this script is not universal. It might not function, if you have deleted an item from a list including folder structure or a document from a library. For example, in the case of a document library the LeafName property correspond to the file name without the extension and not to the ID.

The updated script:

  1. # modify the $url and $listTitle values to match your configuration
  2. $url = "http://YourSharePoint/Web/SubWeb&quot;
  3. $listTitle = "YourCalendar"
  4.  
  5. $web = Get-SPWeb $url
  6. $list = $web.Lists[$listTitle]
  7.  
  8. $query = New-Object Microsoft.SharePoint.SPQuery
  9.   $query.Query =
  10.           "<Where>
  11.              <And>
  12.                <Lt>
  13.                  <FieldRef Name='EndDate' />
  14.                  <Value Type='DateTime'>2017-01-01 00:00:00</Value>
  15.                </Lt>
  16.                <Lt>
  17.                  <FieldRef Name='EventType'/>
  18.                  <Value Type='Integer'>2</Value>
  19.                </Lt>
  20.              </And>
  21.            </Where>"
  22. $query.ViewFields = "<FieldRef Name='ID' />"
  23. $query.ViewFieldsOnly = $true
  24. $query.RowLimit = 1000;
  25.  
  26. $itemCount = 0
  27. $listId = $list.ID
  28.  
  29. do
  30. {
  31.     $listItems = $list.GetItems($query)
  32.     $itemIds = $listItems | % { [String]$_.ID }
  33.     [System.Text.StringBuilder]$batchXml = New-Object "System.Text.StringBuilder"
  34.     [Void]$batchXml.Append("<?xml version=`"1.0`" encoding=`"UTF-8`"?><Batch>")
  35.     $itemIds | % {
  36.       $itemId = $_
  37.       [Void]$batchXml.Append("<Method ID=`"$itemId`"><SetList>$listId</SetList><SetVar Name=`"ID`">$itemId</SetVar><SetVar Name=`"Cmd`">Delete</SetVar></Method>")
  38.     }
  39.     [Void]$batchXml.Append("</Batch>")
  40.     Write-Host Deleting next $listItems.Count entries…
  41.  
  42.     $result = [Xml]$web.ProcessBatchData($batchXml.ToString())
  43.     $success = @(Select-Xml -Xml $result -XPath '//Results/Result[@Code="0"]')
  44.     $failure = @(Select-Xml -Xml $result -XPath '//Results/Result[@Code!="0"]')
  45.     $itemCount += $success.Count
  46.     # list errors
  47.     $failure | % {
  48.         $errorNode = $_.Node
  49.         Write-Host Error deleting entry with ID $errorNode.ID error code: $errorNode.Code error text: $errorNode.ErrorText
  50.     }
  51.  
  52.     # delete items from Recycle Bin as well
  53.     $recBin = $web.Site.RecycleBin
  54.     $recBinIds = @($recBin | ? { $itemIds -contains $_.LeafName.Trim('_.000') -and $_.DirName -eq $list.RootFolder.ServerRelativeUrl.Trim('/') } | % { $_.ID })
  55.     Write-Host Deleting $recBinIds.Count entries from recycle bin…
  56.     $recBin.Delete($recBinIds)
  57. }
  58. while ($listItems.ListItemCollectionPosition -ne $null)
  59.  
  60. Write-Host Summary: $itemCount entries deleted

As the second option, you can delete all items you recycled from the calendar after you have finished the batch deletion. The script below removes items from the Recycle Bin that were deleted from the calendar in the past hour by the current user:

$recBin = $web.Site.RecycleBin
$ids = $recBin | ? { $_.DeletedDate -gt (Get-Date).ToUniversalTime().AddHours(-1) -and $_.DirName -eq ‘Web/SubWeb/Lists/YourCalendar’ -and $_.DeletedById -eq $web.CurrentUser.ID } | % { $_.ID }
Write-Host Deleting $ids.Count item from recycle bin…
$recBin.Delete($ids)

If the deletion was quicker than 60 minutes, you can reduce the time span used in the script, to reduce the possibility you delete an item inadvertently from the Recycle Bin.

February 17, 2015

Unhandled Exception in Gantt Chart View after Editing Calendar Item in Datasheet View

Filed under: Bugs, Calendar, SP 2010 — Tags: , , — Peter Holpar @ 22:48

The other day a user complained that since last week he receives an exception when navigating to a customized Calendar list in SharePoint.

The error details found in ULS logs and displayed on the web UI after turning custom errors off:

Unable to cast object of type ‘System.DBNull’ to type ‘System.String’.
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

Exception Details: System.InvalidCastException: Unable to cast object of type ‘System.DBNull’ to type ‘System.String’.

Source Error:

An unhandled exception was generated during the execution of the current web request. Information regarding the origin and location of the exception can be identified using the exception stack trace below. 

Stack Trace:
[InvalidCastException: Unable to cast object of type ‘System.DBNull’ to type ‘System.String’.]
   Microsoft.SharePoint.WebControls.GanttV4.<NormalizeDateFields>b__3a(Nullable`1 value, DataRow dr, String col) +114
   Microsoft.SharePoint.WebControls.GanttV4.TransformDataTableColumns(IEnumerable`1 cols, Func`4 transform) +454
   Microsoft.SharePoint.WebControls.GanttV4.GenerateGridSerializer() +62
   Microsoft.SharePoint.WebControls.GanttV4.OnPreRender(EventArgs e) +243
   System.Web.UI.Control.PreRenderRecursiveInternal() +108
   System.Web.UI.Control.PreRenderRecursiveInternal() +224
   System.Web.UI.Control.PreRenderRecursiveInternal() +224
   System.Web.UI.Control.PreRenderRecursiveInternal() +224
   System.Web.UI.Control.PreRenderRecursiveInternal() +224
   System.Web.UI.Control.PreRenderRecursiveInternal() +224
   System.Web.UI.Control.PreRenderRecursiveInternal() +224
   System.Web.UI.Control.PreRenderRecursiveInternal() +224
   System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)

image

The single reference we found for this error on the web did not helped at all.

The default view for the list was a Gantt chart view that included all events in the list, other views (like All Events) were displayed without any error.

My very first idea was that the Gantt view should have been altered recently that caused the error, however as we checked the last modified date for the view, it turned out that it has not been modified recently.

Next, as we reduced the item count in the view, the error was displayed not immediately, but only after navigating through several pages of events. It clearly indicated that the problem is caused by data corruption in one or more items. Checking the items created or modified last week, we found a single item. Opening the item for edition from the All Events view, and simply saving it without any modifications solved the issue. Comparing the item’s Xml property before and after the save event the most significant difference was that the fAllDayEvent field (ows_fAllDayEvent attribute in the Xml property) was missing in the former one.

Since the All Day Event column was a mandatory one, it was first a surprise that a such event existed. The only possible solution via the UI (we did not assumed that somebody manipulated the items via code) was the All Events view. Since there were a few custom columns inserted to this view, the All Day Event column was simply removed from the view to provide enough space for the new columns. If we switch this view to the Datasheet View, we can enter new items without performing the validation rules or even saving a default value for the All Day Event field, and so the same error can be reproduced.

March 28, 2013

Dynamically populating Group Calendars, Part I – the basics

Filed under: Calendar, JavaScript, jQuery, SP 2010 — Tags: , , , — Peter Holpar @ 13:54

A few month ago I was working on a prototype of an internal SharePoint application to make it easier to HR and project managers to track and administer the status (like holiday, sick leave) of the employees.

My plan included a group calendar to administer the employee status (note: this feature seems to be already obsolete in SP 2013, see section Group Work site template and Group Work solution on TechNet). User should be able to choose a project from the ribbon, and the calendars of the project members would be displayed in the group calendar view. The ribbon extension itself was not difficult (I will discuss it in the next part of this post), however the dynamic refresh of the group calendar UI was really a bit challenging. The main topic of this post is how to add / remove users to / from the group calendar.

The only post I found on a similar issue is related to resources (I recommend you to read that post first, as I don’t repeat all of the details discussed there!), but it provided at least a good starting point for me, and the discovery of the JavaScript libraries and a lot of fiddlering began…

I won’t describe here all the steps I followed, but rather the most important results.

Note: that the methods and properties below are not officially documented, and using them from your custom code is not supported. Be aware, that they can be changed due to Service Packs or other updates without any prior notice, immediately breaking solutions build upon these techniques, so use them at your own risks.

The bulk of the functionality related to the group calendar is encapsulated into the SP.UI.ApplicationPages.Calendar.js (see the debug version: SP.UI.ApplicationPages.Calendar.debug.js). If not mentioned otherwise, all of the described JS objects can be found in this file.

The SP.UI.ApplicationPages.ResolveEntity (defined in SP.js / SP.debug.js) is primarily a data container to hold information on a calendar entity resolved by SharePoint.

When you add a new user, group or resource entity to the group calendar (for example using the People Picker, either the text box or the dialog), or add / edit / delete events, the calendar control sends an asynchronous request to the server to resolve the entity to be able to refresh the UI, and the “Loading calendar…” message is displayed.

image

To provide the async communication between the calendar control on the webpage and the data on the server side, the CalendarService.ashx ASP.NET Web Handler is used. For example, if we are adding the calendar of the CONTOSO Administrator to the group calendar the following data is send through a POST request to /_layouts/CalendarService.ashx:

cmd=query&listName=ecd80beb-7ba7-4ad0-810a-3e2a95f07c30&viewName=e7d89ac0-7bd6-42b7-9ee5-de7e8b4773bb&dataSourceId=00000000-0000-0000-0000-000000000000&viewType=weekgroup&entity=1;#CONTOSO\administrator;Administrator@contoso.com&selectedDate=&options=1

image

The response is a JSON representation of the events for the Administrator (assuming this user has a single meeting as illustrated above):

[{"Options":1,"Table":null,"DatePicker":null,"Dates":null,"RangeJDay":null,"Navs":null,"Items":{"Data":[[0,1,2,150563,150563,3,3,4,5,5,0,60,0,0,0,6,7]],"Strings":["8","Admin event","Office","3/25/2013","5:00 am","6:00 am","","0x7fffffffffffffff"]}}]

or as visualized by Fiddler:

image

Parameters sent to the CalendarService.ashx should be straightforward. The parameter entity is the key of the entity (in this case 1;#CONTOSO\administrator;Administrator@contoso.com) to be resolved, we have this value from the People Picker. I don’t discuss now the process, how the People Picker gets this values when resolving users asynchronously, it would worth another post.

See the get_key function of the ResolveEntity object to understand how the key for different entity types are built up. For example, in case of a user having an e-mail address the key ($8_0 property of the ResolveEntity) looks like this:

this.$8_0 = ‘1;#’ + this.accountName + ‘;’ + this.email;

Compare this pattern with the value we used above for CONTOSO\administrator.

The entityType property of the ResolveEntity defines the type of entity. Supported values are:

SP.UI.ApplicationPages.ResolveEntity.typE_EVENT = ‘0’;
SP.UI.ApplicationPages.ResolveEntity.typE_USER = ‘1’;
SP.UI.ApplicationPages.ResolveEntity.typE_RESOURCE = ‘2’;
SP.UI.ApplicationPages.ResolveEntity.typE_EXCHANGE = ‘3’;

The value typE_USER means either a user or a group, the isGroup property of the ResolveEntity object makes a difference between user and group subtypes.

How to add entities to the calendar?

We should use the SP.UI.ApplicationPages.CalendarSelector (defined in SP.js / SP.debug.js) and the SP.UI.ApplicationPages.RibbonCalendarSelector (defined in SP.UI.ApplicationPages.Calendar.js / SP.UI.ApplicationPages.Calendar.debug.js) objects to achieve this goal.

// calSel is of tpye SP.UI.ApplicationPages.CalendarSelector
var calSel = SP.UI.ApplicationPages.CalendarSelector.instance();
// sel is of tpye SP.UI.ApplicationPages.RibbonCalendarSelector
var sel = calSel.getSelector(1, scopeKey);
sel.selectEntities(entitiesXml, false);

By calling the getSelector function the the fix value of 1 means a user entity type, and scopeKey is the context identifier for the calendar, and can be extracted from the page content when working with jQuery:

var calRootDiv = jQuery(".ms-acal-rootdiv");
var scopeKey = jQuery(calRootDiv).attr(‘ctxid’);

The entities defined by the entitiesXml will be appended to the existing list of items in the calendar. The format of the XML is shown for resources in the original post from Thomas Zepeda McMillan, and will be further investigated for user entities in my forthcoming post.

How to remove entities from the calendar?

You can think that setting the Append attribute of the Entities node of the XML we pass as parameter to the selectEntities function to a value to False has the effect that the new entities would be not appended to the existing list of entities, but the former entities would be cleared first, however, based on my experience, that is not the case. So we have to find a way to remove existing entities first.

As a first step, we could lookup the SP.UI.ApplicationPages.CalendarContainer for the specific scope (the SP.UI.ApplicationPages.CalendarInstanceRepository object is defined in SP.js / SP.debug.js):

var calCont = SP.UI.ApplicationPages.CalendarInstanceRepository.lookupInstance(scopeKey);

Using this CalendarContainer we can get a reference to the SP.UI.ApplicationPages.EntityPaginator:

var entPag = calCont.$a_1;

Entities displayed on the calendar are stored as an  Array of SP.UI.ApplicationPages.ResolveEntity objects:

var arr = entPag.$1y_1;
// just test: display some of the properties
alert(arr[0].entityType + ‘, ‘ + arr[0].get_key() + ‘, ‘ + arr[0].displayName + ‘, ‘ + arr[0].accountName);

The $AH function of the EntityPaginator removes the item with the specified index from the array plus refreshes the UI. For example, to remove first entity we should call:

entPag.$AH(0);

To remove all of the items, we should iterate through the array:

while(arr.length > 0) {
  entPag.$AH(0);
}

The $5P_1 method of EntityPaginator notifies all event handlers of "pagechanged" to force a control refresh. We should be able to remove all of the entities via clearing the Array content and call the $5P_1 method to refresh the UI directly as:

Array.clear(arr); // or Array.clear(entPag.$AH);
entPag.$5P_1();

however I found that this method – although might seem OK at first sight – is not working perfectly, especially when one would like to page in calendar between weeks.

Handling calendar paging / refresh events

Although not strictly related to the topic, it may be useful to know, that we can subscribe our custom event handlers for such events.

Using the add_$9x and the remove_$9x functions of the EntityPaginator can we inject (and remove) our event handlers for calendar refresh events (like paging):

entPag.add_$9x(Function.createDelegate(null, function() { alert(‘paged’); } ));

or alternatively we can apply a direct approach like this one:

function pageChanged() {
  alert(‘page changed’);
}

entPag.get_events().addHandler(‘pagechanged’, pageChanged);

Next steps

In this post I discussed the fundamentals we need to fulfill the original goal to create a dynamic group calendar view. In the next post I show you how to develop the ribbon extension that utilizes the methods described now.

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.

October 9, 2012

Accessing meeting request properties from mail event receivers

Filed under: Calendar, Incoming email, SP 2010 — Tags: , , — Peter Holpar @ 23:34

Assume you have an incoming mail enabled SharePoint calendar, you send meeting requests to the calendar and need to access their properties (like the start or end time of the meeting)  from the code of your SPEmailEventReceiver.

The incoming meeting requests contain the information about the meeting as a separate attachment in the iCalendar format.

You can read about the iCalendar standard on Wikipedia, like

“By default, iCalendar uses the UTF-8 character set.”

moreover

“iCalendar data has the MIME content type text/calendar.”

Although for parsing this content you can’t find the necessary methods in the core SharePoint object model, SharePoint uses the CalendarReader (and related) objects located in the Microsoft.Internal.Mime assembly from GAC when processing incoming meeting requests.

The following code illustrates, how to get a property by its name (see the Wikipedia article for possible values) from the SPEmailMessage object received in your event handler.

  1. private string GetCalendarProp(SPEmailMessage mail, string propName)
  2.         {
  3.             string propValue = null;
  4.  
  5.             foreach (SPEmailAttachment att in mail.Attachments)
  6.             {
  7.                 if (att.ContentType == "text/calendar")
  8.                 {
  9.                     using (Stream stream = att.ContentStream)
  10.                     {
  11.                         CalendarReader cr = new CalendarReader(stream, "utf-8", CalendarComplianceMode.Loose);
  12.                         while (cr.ReadNextComponent() && string.IsNullOrEmpty(propValue))
  13.                         {
  14.                             if (cr.ComponentId == ComponentId.VEvent)
  15.                             {
  16.                                 CalendarPropertyReader pr = cr.PropertyReader;
  17.  
  18.                                 while (pr.ReadNextProperty() && (string.IsNullOrEmpty(propValue)))
  19.                                 {
  20.                                     if (pr.Name == propName)
  21.                                     {
  22.                                         propValue = pr.ReadValueAsString();
  23.                                     }
  24.                                 }
  25.                             }
  26.                         }
  27.                     }
  28.                     break;
  29.                 }
  30.             }
  31.  
  32.             return propValue;
  33.         }

Because it is rather cumbersome to test event receivers, you can temporarily stop the SPTimer service (don’t forget to restart it later!), send your meeting request to SharePoint, and when the mail arrives to the Drop folder of SMTP, copy a backup to another folder. Next, you can use a console application with a method like this one below to read the SPEmailMessage directly from the file:

  1. private void SPEmailMessageTest()
  2. {
  3.     using (FileStream fs = new FileStream(@"C:\temp\appointment – 159d46ff01cda09100000003.eml", FileMode.Open))
  4.     {
  5.         SPEmailMessage mail = new SPEmailMessage(fs, "test");
  6.         string uid = GetCalendarProp(mail, "UID");
  7.     }
  8.  
  9. }

The sample method above reads the UID property of the meeting that is an internal ID to look up and update former corresponding calendar items.

September 15, 2012

Issues processing incoming e-mails to a calendar

Filed under: Calendar, Incoming email, SP 2010 — Tags: , , — Peter Holpar @ 21:16

These are rather trivial issues, but might be useful for others, so I post them here.

Issue 1:

Recently we created an incoming e-mail enabled calendar to track events at a company. One of the users complained because the mails sent to the specified e-mail address did not appear in the calendar, even after the list was set to accept all mails regardless of the security configured. Other users had no such problem.

I’ve checked the SMTP log first, and found there info about the mails but no errors at all. However, in the SharePoint log file I found this one:

Warning –  An error occurred while processing the incoming e-mail file C:\Inetpub\mailroot\Drop\{id of the e-mail}.eml. The error was: Value cannot be null. Parameter name: stream.

Although it is not easy to identify the source of the problem from this message, the reason for this warning turned out to be that the user was sending simple mails an no meeting requests to the list.

Issue 2:

Meeting requests were sent to a group calendar, but none of them was visible on the web UI, when the users clicked on the name of the calendar list on the Quick Launch.

No error in the SMTP nor in the SharePoint logs.

Reason: the items are visible in the non-default views (like Current Events or All Events). The default Calendar view shows only items where the value of the Attendees field is not empty, however, this field is not populated based on the incoming meeting request.

‘Value does not fall within the expected range’ exception when playing around SharePoint calendars

Filed under: Bugs, Calendar, SP 2010 — Tags: , , — Peter Holpar @ 20:02

Recently I worked with the group calendar feature of SharePoint 2010 (SP1), and observed the following behavior.

We created a new group calendar, and added a new calendar view to the group calendar. A little bit later we decided to switch back our group calendar to a standard one (List Settings / General Settings / Title, description and navigation / Use this calendar to share member’s schedule? : No).

After the modifications, our appended view throws error, when one would like to edit it (ViewEdit.aspx). In this case the following error is displayed:

image

With stack trace:

[ArgumentException: Value does not fall within the expected range.] Microsoft.SharePoint.SPCalendarViewStyleCollection.get_Item(String strTemplate) +21492543 Microsoft.SharePoint.SPCalendarViewStyleCollection.SetDefaultStyleFromExistingView() +341 Microsoft.SharePoint.SPCalendarViewStyleCollection.InitViewStylesXML() +1697 Microsoft.SharePoint.SPCalendarViewStyleCollection.get_Count() +42 ASP._layouts_viewedit_aspx.__Render__control14(HtmlTextWriter __w, Control parameterContainer) in c:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\TEMPLATE\LAYOUTS\viewedit.aspx:1140 System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children) +115 System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children) +240 System.Web.UI.HtmlControls.HtmlContainerControl.Render(HtmlTextWriter writer) +42 System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children) +240 System.Web.UI.HtmlControls.HtmlForm.RenderChildren(HtmlTextWriter writer) +253 System.Web.UI.HtmlControls.HtmlForm.Render(HtmlTextWriter output) +87 System.Web.UI.HtmlControls.HtmlForm.RenderControl(HtmlTextWriter writer) +53 System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children) +240 System.Web.UI.HtmlControls.HtmlContainerControl.Render(HtmlTextWriter writer) +42 System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children) +240 System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children) +240 Microsoft.SharePoint.WebControls.UnsecuredLayoutsPageBase.RenderChildren(HtmlTextWriter writer) +58 System.Web.UI.Page.Render(HtmlTextWriter writer) +38 Microsoft.SharePoint.WebControls.UnsecuredLayoutsPageBase.Render(HtmlTextWriter writer) +58 System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +4240

We receive error as well, when would like to display the appended view.

image

[ArgumentException: Value does not fall within the expected range.] Microsoft.SharePoint.SPCalendarViewStyleCollection.get_Item(String strTemplate) +21492543 Microsoft.SharePoint.SPCalendarViewStyleCollection.SetDefaultStyleFromExistingView() +341 Microsoft.SharePoint.SPCalendarViewStyleCollection.InitViewStylesXML() +1697 Microsoft.SharePoint.SPCalendarViewStyleCollection.get_DefaultViewStyle() +16 Microsoft.SharePoint.WebPartPages.ListViewWebPart.CreateChildControls() +721 Microsoft.SharePoint.WebPartPages.WebPartMobileAdapter.CreateChildControls() +72 System.Web.UI.Control.EnsureChildControls() +132 System.Web.UI.Control.PreRenderRecursiveInternal() +61 System.Web.UI.Control.PreRenderRecursiveInternal() +224 System.Web.UI.Control.PreRenderRecursiveInternal() +224 System.Web.UI.Control.PreRenderRecursiveInternal() +224 System.Web.UI.Control.PreRenderRecursiveInternal() +224 System.Web.UI.Control.PreRenderRecursiveInternal() +224 System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +3394

The default views (including the default Calendar view) have no such issues until we make no modifications before switching back to the standard Calendar list. When we set the default scope of the view – for example, from Week Group to Day Group (then saving), and back to the Week Group again (saving again) – , then we experienced the same issue.

image

The source of the problem seems to be the Default Scope. When we have a group calendar, and set the Default Scope explicitly to a value (like Day Group or Week Group), that is not compatible with the standard calendar, then after altering the list to a standard calendar our view will be erroneous due to the selected scope.

The default calendar seems to have no Default Scope set explicitly, it uses the default scopes (Week Group for the group calendar and Month for the standard one).

You can check it through the SchemaXml property of the View, look for the defined (or missing) CalendarViewStyles node.

You can fix the issue by setting back your calendar to a group calendar, and selecting a Default Scope in your view, that is compatible with the standard calendar (any value without Group in its name). An interesting programmatic approach can be read here.

December 3, 2009

Disabling the text box associated with the DateTimeControl from code

Filed under: Calendar, SharePoint — Tags: , — Peter Holpar @ 02:23
It may be sometimes useful to have a DateTimeControl where users can only pick values from the calendar but are not able to enter dates in the text box. Instead of disabling the textbox, it is a bit more elegant to hide it at all.

You can implement this as illustrated in the following code. It is important to note that using inline code in this example serves only simplicity and is not a recommended approach. I suggest you to put your code in code behind files, or even better in separate assemblies.

<%@ Inherits="System.Web.UI.Page" %>
<%@ Register TagPrefix="spuc" Namespace="Microsoft.SharePoint.WebControls"
Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral,
PublicKeyToken=71e9bce111e9429c" %>

<form runat="server">
    <spuc:DateTimeControl runat="server" ID="DTC" />
</form>

<script runat="server" language="C#">
    void Page_Load(Object sender, EventArgs e)
    {
        ((TextBox)DTC.Controls[0]).Style.Add("display", "none");
    }
</script>

Originally I planned to have DTC.Controls[0].Visible = false; in Page_Load, but it had no effect as required, so we had to play with styles.

You can use this technique to create a custom picker-only DateTime field, or one, where you can decide which component (text box, picker or both) is visible.

September 9, 2009

How to default calendar item to All day Event?

Filed under: Calendar, Reflection, SharePoint — Tags: , , — Peter Holpar @ 00:43

It was the question in one of the MSDN forum threads.

My first idea was a JavaScript or jQuery included in the NewForm.aspx of the specific Calendar list instance, but this solution would have a few drawbacks. First, it requires you to tamper with one of the form pages of the list using SharePoint Designer – one activity don’t like too much. Second, since the scripts can run only after the page is loaded, and the page is loaded with All Day Event checkbox not selected, and time controls for Start Time and End Time visible, selecting the All Day Event option and hiding the four controls (hours and minutes for both Start Date and End Date) might requires either a postback of the page or more line of script I wish to write now. Third, the script on the customized page should run each time the page is displayed.

My second approach was to modify the default value of the field. The first challange here is that one can not find the All Day Event field in the list of fields when using the „Customize Calendar” page, although the field is there when you check the list of fields in the All Events view.

The All Day Event field is a special out-of-the-box field. When you check the Type property of the „All Day Event” field in a calendar list, it returns AllDayEvent. The class that represents this field is the SPFieldAllDayEvent class, that is inherited from the SPFieldBoolean. The field has no default value defied, as the DefaultValue returns null. When you try to modify this property and call the Update() method of the field, you can see that the it has no effect, the DefaultValue of the field remains null.

It is because this field is „inherited” to the list from the Event content type. You can verify this by attaching the content types of the calendar list (there is only one by default, that is Event), and checking the FieldLinks collection of the Event content type. It includes the All Day Event. Here comes the next problem. The SPFieldLink class has no property that can be used to set the default value. Or maybe it has? Yes, it has, just it is not public.

Here comes reflection into the picture. The property is called Default, it is internal and fortunately has both get and set accessors.

Here is the code one can use to set its value, for example from a command line utility:

 

// Don’t forget this

using System.Reflection;

...

using(SPSite site = 

new SPSite("http://yoursite"))

{

 using(SPWeb web = 

site.OpenWeb())

 {

 SPList list = 

web.Lists["Calendar"];

 SPContentType ct = 

list.ContentTypes["Event"];

 SPFieldLink fieldLink = 

ct.FieldLinks["All Day Event"];

 // Now comes the magic 

🙂

 Type type = typeof(SPFieldLink);

 

PropertyInfo pi = type.GetProperty("Default", BindingFlags.NonPublic | 

BindingFlags.Instance);

 pi.SetValue(fieldLink, "1", 

null);

 ct.Update();

 }

} 

After running the code, you can try to create a new calendar item, and see that now the All Day Event checkbox is selected, and only the date field for the Start Time and End Time fields are visible.

The advantage of this solution is that you should not modify the NewForm.aspx, it must be run only once and the form is loaded every time with the default value already set. The possible drawback is that tampering with the reflection might be not really supported and can have unforeseeable side-effects to your SharePoint installation, so the code above is really only for demonstration purposes, please use that at your own risk.

Finally I should note, that beyond the specific example I discussed above, the method can be used to set list level default values for the site columns.

Older Posts »

Create a free website or blog at WordPress.com.