Vor ein paar Monaten bekam ich eine Anfrage, ein Skript zu entwickeln, um eine Datei mit Metadaten in eine SharePoint-Dokumentbibliothek hochzuladen.
Das Skript würde durch eine Workflow-Anwendung angestossen werden, welche unter anderem PowerShell-Skripts starten und an diese Parameter übergeben kann.
Diese Anwendung lief jedoch nicht auf einem SharePoint-Server, so war es mir nicht möglich das Server Side Object Model zu verwenden, welches ich im Normalfall für ein solches Szenario gewählt hätte und welches ich auch heute noch bevorzuge, da es noch immer mächtiger ist (als das Client Side Object Model) und man auf das ganze Async-„Gemurks“ verzichten kann.
Der eigentliche Datei-Upload gestalltet sich mit CSOM auch äusserst einfach. Was mir aber grosse Probleme bereitete, war die Übergabe von Benutzer-Logins an das Skript, respektive, dass diese dann korrekt verwendet wurden.
In den ersten Anläufen hatte ich das Phänomen, dass wenn ich Benutzer-Logins (via String-Array) übermittelte, nur die ungeraden übernommen wurden.
Initial hatte ich das sehr dirty (Schande über mich) gelöst, indem ich einfach alle Einträge im Array jeweils verdoppelte.
Da ich aber solche Hacks nicht sehr sauber finde und diese ja nicht gerade sehr wartungsfreundlich sind, da wenn jemand der in einem Jahr den Code anschaut sich denkt „Wtf?! Warum?“, beschloss ich der Sache etwas tiefer auf den Grund zu gehen und eine bessere Lösung zu finden.
Leider schien ich mit dieser Problematik alleine zu sein. Ich fand im Internet zwar einige Beispiele für das setzen von Metadaten via CSOM bei Datei-Uploads, jedoch nie eine Beispiel für das setzen eines „People Picker“-Feldes, welches mehrere Werte erlaubte (FieldUserValueCollection).
Nach etwas Debugging und Ausprobieren konnte ich das Problem aber dann doch noch eingrenzen und schlusssendlich lösen.
Das Problem lag daran, dass bei den Einträgen für die Benutzer auch ein Wert (LookupValue) vorhanden sein muss (welcher, wenn ich mich richtig erinnere, nicht gesetzt werden konnte, da der Setter read-only ist).
Der Feldwert darf dann nicht einfach als FieldUserValue-Array gesetzt werden, sondern muss folgendes Format haben, welches ihr vielleicht schon kennt, wenn ihr auf einem Lookup-Wert schon einmal „.ToString()“ ausgeführt habt („[User-ID];#[User-Anzeigename]“):
"32;#Peter Muster;#22;#Petra Meier;#31;#Hans Meister;#"
Hier der Auszug der relevanten Stelle, mit der Erstellung eines „FieldUserValue“-Objektes (gut könnte man strenggenommen auch weglassen und den $valueUsers-String direkt aufbauen) und der anschliessenden Umwandlung der Collection in einen String:
{
...
$user = $web.EnsureUser($userLogin)
# User must be loaded to pass a valid user ID as lookup ID
$ctx.Load($user)
$ctx.ExecuteQuery()
if ($user.Id -ne $null)
{
$fieldUserValue = New-Object Microsoft.SharePoint.Client.FieldUserValue
$fieldUserValue.LookupId = $user.Id
$userFieldValueCollection += $fieldUserValue
}
else
{
throw "User for login '$userLogin' not found!"
}
}
$valueUsers = ($userFieldValueCollection | % { $_.LookupId.ToString() + ";#DUMMY;#" }) -join ""
Hier noch das komplette Beispiel-Skript:
param(
[Parameter(mandatory=$true)]
[string] $SourceFilePath,
[Parameter(mandatory=$true)]
[string] $ValueTitle,
[Parameter(mandatory=$true)]
[string] $ValueChoice,
[Parameter(mandatory=$false)]
[string] $ValueComments,
[Parameter(mandatory=$false)]
[bool] $ValueSendNotification = $true,
[Parameter(mandatory=$true)]
[string[]] $ValueUserLogins
)
# Set target site collection URL
$siteUrl = "http://server.name"
$targetLibraryInternalName = "Documents"
# Get script directory
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
# Load SharePoint Client Dlls
Add-Type -Path (Resolve-Path (Join-Path -Path $ScriptDir -ChildPath "Microsoft.SharePoint.Client.dll"))
Add-Type -Path (Resolve-Path (Join-Path -Path $ScriptDir -ChildPath "Microsoft.SharePoint.Client.Runtime.dll"))
# Create Client Context (connect to target site)
$ctx = New-Object Microsoft.SharePoint.Client.ClientContext($siteUrl)
$web = $ctx.Web
$ctx.Load($web)
$ctx.ExecuteQuery()
Write-Host "Connected to web: $($web.Url)" -ForegroundColor Yellow
# Get target library
$targetLibrary = $ctx.Web.GetList($web.ServerRelativeUrl + "/" + $targetLibraryInternalName)
# Prepare 'Viewers' value
[Microsoft.SharePoint.Client.FieldUserValue[]]$userFieldValueCollection = @()
foreach ($userLogin in $ValueUserLogins)
{
$user = $web.EnsureUser($userLogin)
# User must be loaded to pass a valid user ID as lookup ID
$ctx.Load($user)
$ctx.ExecuteQuery()
if ($user.Id -ne $null)
{
$fieldUserValue = New-Object Microsoft.SharePoint.Client.FieldUserValue
$fieldUserValue.LookupId = $user.Id
$userFieldValueCollection += $fieldUserValue
}
else
{
throw "User for login '$userLogin' not found!"
}
}
# Needs beside the LookupId also a dummy LookupValue, otherwise only odd entries are stored!
$valueUsers = ($userFieldValueCollection | % { $_.LookupId.ToString() + ";#DUMMY;#" }) -join ""
# Upload file
$fci = New-Object Microsoft.SharePoint.Client.FileCreationInformation
$fci.Overwrite = $true
$fci.Content = [System.IO.File]::ReadAllBytes($SourceFilePath)
$fci.URL = [System.IO.Path]::GetFileName($SourceFilePath)
$uploadFile = $targetLibrary.RootFolder.Files.Add($fci)
Write-Host "File uploaded to target library"
# Set metadata properties on uploaded file (must be done on item of file)
Write-Host "Writing metadata to fields"
$listItem = $uploadFile.ListItemAllFields
$listItem["Title"] = $ValueTitle
$listItem["MyChoice"] = $ValueChoice
$listItem["Comments"] = $ValueComments
$listItem["SendNotification"] = $ValueSendNotification
$listItem["Readers"] = $valueUsers
$listItem.Update()
Write-Host "Executing pending queries (write metadata to file)"
$ctx.Load($uploadFile)
$ctx.Load($listItem)
$ctx.ExecuteQuery()
Write-Host "Uploaded file '$($uploadFile.Name)', received ID: $($listItem.Id)" -ForegroundColor Yellow
Das Skript kann wie folgt aufgerufen werden:
.\Skript.ps1 -SourceFilePath "X:\SourceFiles\Test Document.docx" -ValueTitle "Title Value" -ValueChoice "My Choice Value" -ValueComments "Test Comment" -ValueUserLogins @("Domain\username1", "Domain\username2") –ValueSendNotification $true
Wichtig: damit ihr via PowerShell CSOM verwenden könnt, müsst ihr noch die SharePoint-Client-DLLs zur Verfügung haben (findet ihr im SharePoint-Installationsverzeichnis) und diese im Skript Verzeichnis ablegen, darum die beiden Aufrufe im Skript:
# Load SharePoint Client Dlls
Add-Type -Path (Resolve-Path (Join-Path -Path $ScriptDir -ChildPath "Microsoft.SharePoint.Client.dll"))
Add-Type -Path (Resolve-Path (Join-Path -Path $ScriptDir -ChildPath "Microsoft.SharePoint.Client.Runtime.dll"))