Second Life of a Hungarian SharePoint Geek

April 20, 2016

How to Start the Wrong SharePoint Workflow Unintentionally from the UI

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

A few weeks ago we wanted to start a specific workflow on a SharePoint list item, that is located in a list that has multiple custom workflows (none of them has a workflow initiation form) associated with it. We performed the action at the very same time with a colleague of mine (having only 4 seconds difference, as it turned out later), independently from each other. After a while, when we checked the status of the item, we found, that two workflows started on the item. The one, we both wanted to start, was started by my colleague, and I’ve started an other workflow. My colleague said I might have clicked the wrong workflow on the workflow.aspx web page, but I was sure I clicked the right one.

What happened?

I was able to reproduce the issue in our test environment by loading the Workflow page in two separate browser tabs, and starting the same workflow in each of them. I was pretty confident, that the developers of the page made the mistake to try to start the selected workflow by its index in the array of available workflows, instead of the Id of the workflow association.To be sure, I’ve checked the source of the workflow.aspx file (located in the LAYOUTS folder, having a lot of in-line code) and the class behind it, the Microsoft.SharePoint.ApplicationPages.WorkflowPage class (located in the Microsoft.SharePoint.ApplicationPages assembly).

Note: If you try to start the last workflow on the page (or as a special case of it, you have only a single associated workflow) twice in two separate browser, you get an error on the second try. In this case you have the following entry (Level: Unexpected) in the ULS logs:

System.ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection.  Parameter name: index    at System.Collections.ArrayList.get_Item(Int32 index)     at Microsoft.SharePoint.ApplicationPages.WorkflowPage.OnLoad(EventArgs e)     at System.Web.UI.Control.LoadRecursive()     at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)

The OnLoad method of the WorkflowPage class calls the ConstructStartArray method if it is not a page post back. In the ConstructStartArray a new ArrayList is created in the m_alwaStart field, and it is populated by the available workflows, by iterating through the workflow associations on the list, on the content type, and on the web level. In each case, the FCanStartWorkflow method of the base class (WorkflowPageBase) is invoked to ensure the current user has the permission to start the workflow manually (see the SPWorkflowAssociation.PermissionsManual property) and if there is no running instance of this workflow type on the item already (via the FIsRunningWt method of the same class). By the end of the ConstructStartArray method the ArrayList in the m_alwaStart field contains the workflows the user can start on the item. So far so good.

Let’s see how this list is rendered in the in-line code of the workflow.aspx page.

<%
    bool fColumnPosition=true;
    int iwa = 0;
    strImageUrl = "/_layouts/images/availableworkflow.gif";
    foreach (SPWorkflowAssociation wa in m_alwaStart)
    {
        string strIniUrl = GenerateStartWorkflowLink(wa);
        if (strIniUrl == null)
            strIniUrl = "javascript:StartWorkflow(" + Convert.ToString(iwa) + ")";
%>

<%

    iwa++;

%>

As you can see, the parameter used with the StartWorkflow method is really a simple counter, the index of the workflow association in the ArrayList in the m_alwaStart field.

The StartWorkflow JavaScript method simply sets a form value (iwaStart) and posts back the page:

function StartWorkflow(iwa)
    var elIwaStart = document.getElementById("iwaStart");
    elIwaStart.value = iwa;
    theForm.submit();
}

The server side GenerateStartWorkflowLink method of the WorkflowPage class, that you can also see in the inline-code above should display the workflow initiation form for the workflow association, if any exists.

Back to the server side, and let’s see what happens with the value posted back by the StartWorkflow method in the OnLoad method of the WorkflowPage class. If the request is a post back, than it reads the index of the workflow to start, and looks up the workflow by this index from the array of workflow associations in the m_alwaStart field:

int num2 = Convert.ToInt32(base.Request.Form["iwaStart"]);
if (num2 >= 0)
{
    base.StartWorkflow((SPWorkflowAssociation) this.m_alwaStart[num2]);
}

Problem: this array might be not the same, as the one returned on the first page load. If a workflow that precedes the workflow we want to start (or the same workflow) is started in the meantime, the workflow associations are changed (for example, workflows are registered or removed on the web, on the list or on the content type level), or the permissions are changed, it is possible (or even very likely) that the user starts another workflow, not the one he clicked on on the web UI.

Solution: would be to use the Id (of type Guid) of the Microsoft.SharePoint.Workflow.SPWorkflowAssociation instance as the identifier of the item in the array instead of  the index / position in the array.

That would mean in the in-line code, instead of using the iwa counter:

strIniUrl = "javascript:StartWorkflow(" + Convert.ToString(wa.Id) + ")";

and in the OnLoad method, handling the post back as:

Guid waId = Guid.Parse(base.Request.Form["iwaStart"] as string);
base.StartWorkflow(this.m_alwaStart.ToArray().First<SPWorkflowAssociation>(wa => wa.Id == waId));

Note: I could reproduce this buggy behavior in SharePoint 2010 and in SharePoint 2013 using site collections that were not upgraded to the SharePoint 2013 mode. However, as long as I see, “native” SharePoint 2013 sites do suffer from the same kind of problem.

September 29, 2015

People Picker is very slow when searching users

Filed under: Active Directory, People Picker, SP 2010 — Tags: , , — Peter Holpar @ 22:12

The environment of a customer of us consists of several Active Directory domains, a few of them were recently migrated from former domains.

Users of the SharePoint sites complained that when they try to look up users via the People Picker, the result is displayed only after a delay of  30-40 seconds, instead of the former 3-5 seconds.

I’ve tried to catch the problem using Wireshark, filtering for the LDAP protocol, as described in this post. However, I found no problem with the requests / responses, except for a delay of about 30 seconds, although no request using this protocol was sent in this time lag. Obviously, the sender process waited for a response sent using another protocol.

Removing the LDAP filter in Wireshark, I found these retransmission attempts:

No.     Time            Source                        Destination   Protocol Length  Info
3241    44.218621000    IP of the SharePoint Server   IP of the DC    TCP    66    53607 > msft-gc [SYN] Seq=0 Win=8192 Len=0 MSS=1460 WS=256 SACK_PERM=1
3360    47.217136000    IP of the SharePoint Server   IP of the DC    TCP    66    [TCP Retransmission] 53607 > msft-gc [SYN] Seq=0 Win=8192 Len=0 MSS=1460 WS=256 SACK_PERM=1
3791    53.221414000    IP of the SharePoint Server   IP of the DC    TCP    62    [TCP Retransmission] 53607 > msft-gc [SYN] Seq=0 Win=8192 Len=0 MSS=1460 SACK_PERM=1

The msft-gc is an LDAP-like protocol used to query the Global Catalog (GC) in the Active Directory (uses port 3268). The retransmission timeout (RTO) value of the packet 3360 was 3 sec., the RTO of the packet 3791 was 9 sec., both causing delay in the user search process.

The source IP was the address of the SharePoint server, the IP address in the destination is the address of a former Domain Controller (DC). The server, that acted as DC of a domain that was already migrated was online, but the DC-role was already demoted on it . The IP address of the server was registered in DNS, so the server could be PINGed, but it did not respond to LDAP requests (including msft-gc) anymore.

The entries in the ULS logs has provided further evidence, that there is an issue with the Global Catalog in the AD forest (see the SearchFromGC method in the stack trace below):.

08/06/2015 13:26:34.08     w3wp.exe (0x66BC)                           0x9670    SharePoint Foundation             General                           72e9    Medium      Error in resolving user ‘UserName‘ : System.Runtime.InteropServices.COMException (0x8007203A): The server is not operational.       at System.DirectoryServices.DirectoryEntry.Bind(Boolean throwIfFail)     at System.DirectoryServices.DirectoryEntry.Bind()     at System.DirectoryServices.DirectoryEntry.get_AdsObject()     at System.DirectoryServices.DirectorySearcher.FindAll(Boolean findMoreThanOne)     at Microsoft.SharePoint.WebControls.PeopleEditor.SearchFromGC(SPActiveDirectoryDomain domain, String strFilter, String[] rgstrProp, Int32 nTimeout, Int32 nSizeLimit, SPUserCollection spUsers, ArrayList& rgResults)     at Microsoft.SharePoint.Utilities.SPUserUtility.ResolveAgainstAD(String input, Boolean inputIsEmailOnly, SPActiveDirectoryDomain globalCatalog, SPPrincipalType scopes, SPUserCo…    04482a74-c00f-4005-9cd3-11f765eca7a0
08/06/2015 13:26:34.08*    w3wp.exe (0x66BC)                           0x9670    SharePoint Foundation             General                           72e9    Medium      …llection usersContainer, TimeSpan searchTimeout, String customFilter)     at Microsoft.SharePoint.Utilities.SPActiveDirectoryPrincipalResolver.ResolvePrincipal(String input, Boolean inputIsEmailOnly, SPPrincipalType scopes, SPPrincipalSource sources, SPUserCollection usersContainer)     at Microsoft.SharePoint.Utilities.SPUtility.ResolvePrincipalInternal(SPWeb web, SPWebApplication webApp, Nullable`1 urlZone, String input, SPPrincipalType scopes, SPPrincipalSource sources, SPUserCollection usersContainer, Boolean inputIsEmailOnly, Boolean alwaysAddWindowsResolver).    04482a74-c00f-4005-9cd3-11f765eca7a0

Removing the orphaned DC entry from the AD  resolved the People Picker problem as well.

August 7, 2015

How to process the output of the stsadm EnumAllWebs operation

Filed under: PowerShell, SP 2010, Upgrade — Tags: , , — Peter Holpar @ 10:48

Recently I wrote about how to process the output of the Test-SPContentDatabase PowerShell cmdlet.

If you use the EnumAllWebs operation of the stsadm command, you have similar options as well. However, you have the output in XML format in this case, so we should use XPath expressions to access the information we need.

First, save the result into a text file:

stsadm -o EnumAllWebs -DatabaseName YourContentDB -IncludeFeatures -IncludeWebParts >> C:\Output.xml

then load its content into an XML object:

$reportXml = [Xml] (Get-Content C:\Output.xml)

To list the missing features:

Select-Xml -Xml $reportXml -XPath ‘//Site’ | % { $siteId = $_.Node.Id; Select-Xml -Xml $_.Node -XPath ‘Features/Feature[@Status="Missing"]/@Id’ | % { (Get-SPFeature -Site $siteId -Identity $_.Node.Value).DisplayName } }

Note, that site templates created by saving an existing site as a site template from the UI might be reported as missing features as explained here.

To get an overview of the site templates:

Select-Xml -Xml $reportXml -XPath ‘//Web/@TemplateName’ | % { $_.Node.Value } | Group { $_ }

image

To get an overview of the missing web-scoped features:

Select-Xml -Xml $reportXml -XPath ‘//Web/Features/Feature[@Status="Missing"]/@Id’ | % { $_.Node.Value } | Group { $_ } | % { Write-Host $_.Name – Count is $_.Count }

Finally, to get an overview of the missing web parts:

Select-Xml -Xml $reportXml -XPath ‘//Web/WebParts/WebPart[@Status="Missing"]/@Id’ | % { $_.Node.Value } | Group { $_ } | % { Write-Host $_.Name – Count is $_.Count }

Computing Hash of SharePoint Files

Filed under: PowerShell, SP 2010 — Tags: , — Peter Holpar @ 10:46

Recently we had to compare master pages of hundreds of sites to find out, which ones contain customizations. Comparing the content of files means in practice typically to compare the hash values calculated based on the file content.

But how to compute the hash of a file stored in SharePoint?

If you had the SharePoint sites mapped to the Windows File System via WebDAV, you could use the Get-FileHash cmdlet from PowerShell 4.0 (or above). In PowerShell 5.0 the Get-FileHash cmdlet is able to compute the hash of input streams as well, so we could use SPFile.OpenBinaryStream method to access the file content, and then compute its hash value.

Since I have only PowerShell 2.0 on my SharePoint server, I had to create my own hashing solution, the Get-SPFileHash function:

function Get-SPFileHash($fileUrl) 

  $site = New-Object Microsoft.SharePoint.SPSite $fileUrl
  $web = $site.OpenWeb()
  $file = $web.GetFile($fileUrl)
  $bytes = $file.OpenBinary()
  $md5 = New-Object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider
  $hash = [System.BitConverter]::ToString($md5.ComputeHash($bytes))
  return $hash
}

Having this method, it is easy to create a summary of all sites, their master pages, and the hash values of the master page content:

$siteUrl = "http://YourSharePointSite&quot;

$site = Get-SPSite $siteUrl

$site.AllWebs | % {
  $obj = New-Object PSObject
  $obj | Add-Member NoteProperty Url($_.Url)
  $obj | Add-Member NoteProperty MasterUrl($_.MasterUrl)
  $obj | Add-Member NoteProperty FileHash(Get-SPFileHash ($siteUrl + $_.MasterUrl))
  Write-Output $obj
} | Export-CSV "C:\data\MasterPageHash.csv"

You can import the resulting CSV file into Excel and process its content as you wish.

Note: If you simply double-click on the CSV file created by the former script in Windows Explorer, it is not opened in the format you probably wish: values separated into columns. Instead of that, the first column would contain the entire line. You should first prepare the file: open it in Notepad, optionally remove the first header line, and save the file again, changing the encoding from ANSI to Unicode. Next, start Excel, and open the CSV file from Excel, setting the separator character to Comma on the second page of the Text Import Wizard.

August 2, 2015

PowerShell Scripts around the SID

Filed under: Active Directory, Migration, PowerShell, SP 2010 — Tags: , , , — Peter Holpar @ 23:38

If you ever migrated SharePoint users you should be familiar either with the Move-SPUser cmdlet or its predecessor, the migrateuser stsadm operation:

$sourceURL = "http://mysites.company.com&quot;
$web = Get-SPWeb $sourceURL
$user = $web.SiteUsers["domain\jdoe"]
Move-SPUser -Identity $user -NewAlias "newDomain\john.doe" –IgnoreSID

or

stsadm -o migrateuser –oldlogin "domain\jdoe" -newlogin "newDomain\john.doe" -ignoresidhistory

As you see, both method relies on the SID (or on its ignorance), but what is this SID and how can we read its value for our SharePoint or Active Directory users?

Each user in the Active Directory (AD) has a security identifier (SID) that is a unique, immutable identifier, allowing the user to be renamed without affecting its other properties.

Reading the SID of a SharePoint user from PowerShell is so simple as:

$web = Get-SPWeb http://YourSharePoint.com
$user = $web.AllUsers["domain\LoginName"]
$user.Sid

To be able to work with Active Directory from PowerShell, you need of course the Active Directory cmdlets. If your machine has no role in AD, you should install this PowerShell module using the steps described in this post.

Once you have this module installed, and you imported it via “Import-Module ActiveDirectory”, you can read the SID of a user in AD:

$user = Get-ADUser UserLoginNameWithoutDomain -Server YourDomainController.company.com
$user.SID.Value

Where UserLoginNameWithoutDomain is the login name of the user without the domain name, like jdoe in case of domain\jdoe, and YourDomainController.company.com is your DC responsible for the domain of your user.

If you need the SID history from AD as well, it’s a bit complicated. In this case I suggest you to read this writing as well.

$ADQuery = Get-ADObject –Server YourDomainController.company.com`
        -LDAPFilter "(samAccountName=UserLoginNameWithoutDomain )" `
        -Property objectClass, samAccountName, DisplayName, `
        objectSid, sIDHistory, distinguishedname, description, whenCreated |
        Select-Object * -ExpandProperty sIDHistory
$ADQuery | % { 
  Write-Host $_.samAccountName
  Write-Host Domain $_.AccountDomainSid.Value 
  Write-Host SID History
  $_.sIDHistory | % {
    $_.Value     
  }
  Write-Host ——————–
}

Tasks regarding to MySites Migration and Automating them via PowerShell

Filed under: Migration, PowerShell, SP 2010 — Tags: , , — Peter Holpar @ 22:40

Recently we have performed a domain migration for a customer, where we had to migrate the MySites of the users as well. In this blog post I share the relevant PowerShell scripts we used to support the migration.In our case it was a SharePoint 2010 farm, however for SharePoint 2013 you should have the same tasks as well, so hopefully you find the scripts useful.

The user naming convention has been changed during the migration, for example a user John Doe had a login name in the source domain (let’s call it simply domain) like jdoe, he has a new login name john.doe in the target domain (let’s call it newDomain).

As you now, each MySite is a separate site collection under a site collection root (like http://mysites.company.com/personal), the last part of the site collection URL is built based on the login name (for example, it was originally http://mysites.company.com/personal/jdoe). Of course, the customer wanted the MySite URLs to reflect the changes in the login name naming conventions (it should be changed http://mysites.company.com/personal/john.doe)

First, we had to migrate the SharePoint user and its permissions using the Move-SPUser cmdlet:

$sourceURL = "http://mysites.company.com/personal/jdoe&quot;
$web = Get-SPWeb $sourceURL
$user = $web.SiteUsers["domain\jdoe"]
Move-SPUser -Identity $user -NewAlias "newDomain\john.doe" –IgnoreSID

We cannot simply change the URL of the site collection. We have to backup it and restore using the new URL as described in this post and illustrated here:

$sourceURL = "http://mysites.company.com/personal/jdoe&quot;
$targetURL = "http://mysites.company.com/personal/john.doe&quot;

# Location for the backup file
$backupPath = "E:\data\mysite.bak"

Try
{
   # Set the Error Action
   $ErrorActionPreference = "Stop"

  Write-Host "Backing up the Source Site Collection…"-ForegroundColor DarkGreen
  Backup-SPSite $sourceURL -Path $backupPath -force
  Write-Host "Backup Completed!`n"

  # Delete source Site Collection
  Write-Host "Deleting the Source Site Collection…"
  Remove-SPSite -Identity $sourceURL -Confirm:$false
  Write-Host "Source Site Deleted!`n"

  # Restore Site Collection to new URL
  Write-Host "Restoring to Target Site Collection…"
  Restore-SPSite $targetURL -Path $backupPath -Confirm:$false
  Write-Host "Site Restored to Target!`n"

  # Remove backup files
  Remove-Item $backupPath
}
Catch
{
  Write-Host "Operation Failed. Find the Error Message below:" -ForegroundColor Red
  Write-Host $_.Exception.Message -ForegroundColor Red
}
Finally
{
   # Reset the Error Action to Default
   $ErrorActionPreference = "Continue"
}
 
Write-host "Process Completed!"

Of course, we have to change the MySite URL in the user profile properties as well as described here. We used the following script: 

$waUrl = "http://mysites.company.com&quot;
$wa = Get-SPWebApplication -Identity $waUrl

# Create Service Context for User Profile Manager
$context = Get-SPServiceContext $wa.Sites[0]

# Get User Profile Manager instance
$upm = New-Object Microsoft.Office.Server.UserProfiles.UserProfileManager($context)

# Get the user profile for owner of the personal site
$up = $upm.GetUserProfile("newDomain\john.doe")
$up["PersonalSpace"].Value = "/personal/john.doe"
$up.Commit()

Each user is by default the primary site collection administrator of his own MySite. In my former posts I already discussed how we can change the primary site collection administrator with or without elevated permissions. See this posts for reference to change the account to the one from the new domain.

For example, the simplest version:

$targetURL = "http://mysites.company.com/personal/john.doe&quot;
$siteAdmin = New-Object Microsoft.SharePoint.Administration.SPSiteAdministration($targetURL )
$siteAdmin.OwnerLoginName = "newDomain\john.doe"

July 9, 2015

Creating custom SharePoint permission levels via PowerShell

Filed under: Permissions, PowerShell, Security, SP 2010 — Tags: , , , — Peter Holpar @ 15:36

Today I had to create some custom permissions via code, however the best post I found in this theme included only C# code, and I was to use PowerShell. Although it is not very difficult to translate the code, it is not trivial as well. So I thought I share my “translation”, and hope somebody find it useful.

Copy a permission level:
$contributorRole = $web.RoleDefinitions.GetByType([Microsoft.SharePoint.SPRoleType]::Contributor)
$customRole = New-Object Microsoft.SharePoint.SPRoleDefinition($contributorRole)

Add a permission:
$customRole.BasePermissions = $customRole.BasePermissions -bor [Microsoft.SharePoint.SPBasePermissions]::CreateGroups

Add multiple permissions:
$customRole.BasePermissions = $customRole.BasePermissions -bor ([Microsoft.SharePoint.SPBasePermissions]::ApplyStyleSheets -bor [Microsoft.SharePoint.SPBasePermissions]::ApproveItems)

Add all permissions:
$customRole.BasePermissions = $customRole.BasePermissions -bor [Microsoft.SharePoint.SPBasePermissions]::FullMask

Remove a permission:
$customRole.BasePermissions = $customRole.BasePermissions -band -bnot [Microsoft.SharePoint.SPBasePermissions]::DeleteListItems

Remove multiple permissions:
$customRole.BasePermissions = $customRole.BasePermissions -band -bnot ([Microsoft.SharePoint.SPBasePermissions]::DeleteVersions -bor [Microsoft.SharePoint.SPBasePermissions]::EditListItems)

Remove all permissions:
$customRole.BasePermissions = $customRole.BasePermissions -band [Microsoft.SharePoint.SPBasePermissions]::EmptyMask

Test for a permission:
$permissionTest = ($customRole.BasePermissions -band [Microsoft.SharePoint.SPBasePermissions]::DeleteListItems) -eq [Microsoft.SharePoint.SPBasePermissions]::DeleteListItems

Save a permission level:
$customRole.Name = "Your custom permission"
$customRole.Description = "Description of your custom permission level"
$web.RoleDefinitions.Add($customRole)
$web.Update()

February 28, 2015

People Picker does not work when ActiveX Filtering is enabled in Internet Explorer

Filed under: SP 2010 — Tags: — Peter Holpar @ 18:26

Last week we had a strange issue with one of the users. She used a simple SharePoint application that includes a People Picker control to enable users to assign items to other employees.

A pre-defined user should have been selected as a default value in the field via server side code, however it was empty on page load in this case. If the user typed the name or login name of any employees in the text box, the name was not resolved by the control as expected, although as we checked the network traffic by Fiddler, we saw that the request for name resolution was sent to the server and the server responded with the resolved entity. If the user tried to select the assignee via browsing the entities, she was able select the employee, but as she clicked the OK button in the Select People and Groups Webpage Dialog, the selection was lost and the text box remained empty.

Other users (using the same browser version, Internet Explorer 9, and having the very same permission on the system) had no problem with the application.

Fortunately, I noticed an unusual icon near to the URL in the browser, saying “Some content is blocked to help protect your privacy”.

image

As I found out, it was a result of the user activated unconsciously the ActiveX Filtering feature in her browser.

image

The solution was turning off ActiveX filtering for the site, however, one can turn off the whole feature, if it is not needed.

image

I was able to reproduce the issue when working with SharePoint 2010 up to the Internet Explorer version 11. In case of SharePoint 2013 the issue seems to be fixed, at least, I was not able to reproduce it with IE 10.

February 18, 2015

Strange (?) Behavior of the Client Object Model You should be Aware of

Filed under: Managed Client OM, PS 2013, SP 2010, SP 2013 — Tags: , , , — Peter Holpar @ 00:28

Note: The code examples in this post are based on the managed client OM (C#), however, you can expect the same behavior when working with the JavaScript / Silverlight versions of the client OM, both for SharePoint 2010 and 2013, furthermore, the behavior I describe applies to the client OM of Project Server 2013 as well.

Assume you have a SharePoint list including two fields, Title is a text column, and User is a field of type People or Group. You would like to update both of these fields via the client object model.

In the first case we simply set fix values for the item with Id=1, the text ‘Test’ for Title and an integer (2) as user Id for the User field.

using (var context = new ClientContext(webUrl))
{
  var web = context.Web;
  var list = web.Lists.GetByTitle("TestList");

  var item = list.GetItemById(1);
  item["Title"] = "Test";

  var userValue = new FieldUserValue();
  userValue.LookupId = 2;
  item["User"] = userValue;

  item.Update();
 
  context.ExecuteQuery();
}

Everything works as expected, both of the fields are updated by the query.

In the second case, we would like to set the same values for the item with Id=2, but in this case we get the user Id from the Id of the current user (that has “accidentally” the value 2 in this case). To get that value, we call the ExecuteQuery in the middle of the code, after setting the Title field, and before setting the User field and before invoking the Update method on the item. Finally we invoke the ExecuteQuery method again.

using (var context = new ClientContext(webUrl))
{
  var web = context.Web;
  var list = web.Lists.GetByTitle("TestList");

  var item = list.GetItemById(2);
  item["Title"] = "Test";

  var userValue = new FieldUserValue();
  var cu = web.CurrentUser;
  context.Load(cu, u => u.Id);
  context.ExecuteQuery();

  userValue.LookupId = cu.Id;
  item["User"] = userValue;

  item.Update();
 
  context.ExecuteQuery();
}

One could expect that both of the fields would be updated, however, the Title field won’t be in fact, and we will understand why, after having a look at the requests (for example, using Fiddler) sent by the client OM.

In the first case, there is a single request that includes setting both of the fields (see the SetFieldValue methods in the screenshot below, one for each field) and invoking the Update method.

image

In the second case there are two requests sent by the client OM, one for each in ExecuteQuery code.

The first request includes a single SetFieldValue method for the Title field, the query for the Id of the current user is not included in the screenshot for the sake of simplicity:

image

The second request includes a single SetFieldValue method for the User field, and invoking the Update method.

image

You can see, that calling ExecuteQuery for the the Id of the current user submits the change of the Title field as well, and thereby clears the “dirty” flag of the field (that means it won’t be re-submitted by the OM once again in later requests), but due to the lack of invoking the Update method in this request, this value change has no effect, it is simply not persisted. In the second query we call the Update method, but in this request only the change in the User field was submitted, it has no effect on the Title field.

Lesson learned: The requests sent by the client OM are independent on the server side, no session state or context is persisted between the requests. Only data changed since the last request or required to be able to perform the current request on the server side are sent by the client OM. You should consider the code blocks between (or before) calling ExecuteQuery as they were separate server side console applications you try to run one after another. In this case you won’t expect either, that setting a field without calling the Update method may have any effect on the persisted value of the field. If you are aware of the client OM architecture internals, this is just the expected behavior. If you are not, you might have such surprises in the future.

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.

Older Posts »

Blog at WordPress.com.