Sonntag, 3. November 2019

ErgebnisTypen automatisiert aktualisieren

Wenn im Rahmen eines Projektes Anzeigevorlagen (DisplayTemplates) und Ergebnistypen (ResultTypes) erstellt werden, müssen die Ergebnistypen immer wieder aktualisiert werden.
Die entsprechende Meldung stellt sich so dar:

Viele Projekte erschweren sich das Leben, indem dieser Schritt immer manuell vollzogen werden muss - auch wenn der Rest automatisiert ist.

Das aktualisieren ist eigentlich (on premises) kein Hexenwerk: Set-SPEnterpriseSearchResultItemType kann verwendet werden um die DisplayProperties zu aktualisieren.

Ich habe ein Skript erstellt, mit dem alle ResultTypes automatisch aktualisiert werden können:

<#  
.SYNOPSIS  
    Updates the DisplayProperties of the ResultItemTypes.
.Notes
    This script currently only updates Site-Level-ResultTypes.
    
.Example
    Update-ResultItemTypesProperties -SiteUrl https://my.sharepoint.com/sites/search
    Updates DisplayProperties of all non builtin ResultTypes
#>
[CmdletBinding()]
param(
    [Parameter(Mandatory=$true)]
    [string]$SiteUrl
)
$ErrorActionPreference="Stop"

$site = Get-SPSite $SiteUrl     
$ssa = Get-SPEnterpriseSearchServiceApplication
$owner = Get-SPEnterpriseSearchOwner -Level SPSite -SPWeb $site.RootWeb
# TODO: Ssa and SPWeb are also possible locations for owner...

$masterPageGallery = $site.GetCatalog("MasterPageCatalog")

$resultTypes = Get-SPEnterpriseSearchResultItemType -Owner $owner -SearchApplication $ssa | ? { -not $_.Builtin }
$resultTypes | % {
    Write-Verbose "Processing: $($_.Name)"
    $displayTemplateFileName = Split-Path -Path $_.DisplayTemplateUrl -Leaf
    $displayTemplate = $masterPageGallery.Items | ? {$_.Name -eq $displayTemplateFileName}
    $propPairs = $displayTemplate[[guid]"a0dd6c22-0988-453e-b3e2-77479dc9f014"];
    $propsString = ""
    if(-not [string]::IsNullOrWhiteSpace($propPairs)){
        $props = $propPairs.Split(",") | %{ $_.Split(":") | Select -Last 1 } | %{ $_.Trim("'") }
        $propsString = [string]::Join(",", $props)    
    }

    if($_.DisplayProperties -eq $propsString) {
        # match
        return;
    }

    Set-SPEnterpriseSearchResultItemType  -Identity $_ -Owner $owner -SearchApplication $ssa -DisplayProperties $propsString
}

Mikael Svenson hat dazu auch einen c# Schnipsel veröffentlicht, der die entsprechende Aktualisierung durchführt. Dieser kann z.B. in einem FeatureReceiver verwendet werden.

Freitag, 25. Oktober 2019

SharePoint Benutzer-Spracheinstellungen mit PowerShell setzen.

Ich falle immer mal wieder darüber: Wie kann man für alle Benutzer die Spracheinstellungen per PowerShell setzen?

Die Frage ist schnell beantwortet und eine kurze Suche führt meistens etwas wie das folgende zutage:

[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Server")
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Server.UserProfiles")

$ctx =  [Microsoft.Office.Server.ServerContext]::GetContext((Get-SPWebApplication | Select -First 1))
$upm = new-object Microsoft.Office.Server.UserProfiles.UserProfileManager -ArgumentList @($ctx)
$lang = "de-DE,en-US"

$enum = $upm.GetEnumerator()
while ($enum.MoveNext()) {
  $up = $enum.Current
  write-host "$($up.DisplayName) ($($up.AccountName))"

  $up["SPS-MUILanguages"].Value = $lang
  $up["SPS-ContentLanguages"].Value= $lang
  $up.Commit()
}

Meistens sieht dann in der Zentraladministration alles gut aus:


Aber für "einige" (oder auch alle...) Benutzer erscheinen die Werte dann nicht in den MySites:


Das "Geheimnis" ist ein TimerJob ("User Profile Service Application_LanguageAndRegionSync") der allerdings nur richtig arbeitet, wenn die eigenschaft 'SPS-RegionalSettings-Initialized' auf true gesetzt ist. Im Standard läuft dieser Job jede Minute - ein Start, direkt nach der Anpassung kann aber auch nicht schaden.
Das Finale Skript muss also so aussehen:

[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Server") | out-null
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Server.UserProfiles") | out-null

$ctx =  [Microsoft.Office.Server.ServerContext]::GetContext((Get-SPWebApplication | Select -First 1))
$upm = new-object Microsoft.Office.Server.UserProfiles.UserProfileManager -ArgumentList @($ctx)
$lang = "de-DE,en-US"

$enum = $upm.GetEnumerator()
while ($enum.MoveNext()) {
  $up = $enum.Current
  write-host "$($up.DisplayName) ($($up.AccountName))"

  $up["SPS-MUILanguages"].Value = $lang
  $up["SPS-ContentLanguages"].Value= $lang
  $up["SPS-RegionalSettings-Initialized"].Value = $true
  $up.Commit()
}

Get-SPTimerJob | ?{ $_.Name -like "*user*profile*languageandregion*" } | %{ $_.RunNow() }

Danach erscheinen auch die Anpassungen in der MySite:


Was gibt es noch dazu zu sagen?

  • Die Sprach-codes müssen "korrekt" sein - also "de-DE" nicht "de-de".
  • Der Text darf keine leerzeichen enthalten - also "de-DE,en-US", nicht "de-DE, en-US"

Dienstag, 13. August 2019

LightCore bindings für Quartz.Net und Topshelf

Schon vor etwas längerer Zeit habe ich Bindings erstellt um zum einen Topshelf und zum anderen Quartz.Net "besser" mit LightCore verwenden zu können.

LightCore ist ein sehr schneller und einfach zu verwendender DI-Container.

Topshelf und Quarz.Net können beide sehr gut mit DI-Containern verwendet werden, allerdings gab es keine Anbindung an LightCore.

Wenn Quartz.LightCore und Topshelf.LightCore verwendet werden sieht der code so aus:

// setup LightCore
var builder = new ContainerBuilder();
/* some fancy setup here */
var container = builder.Build();

// setup Quartz
var scheduler = await new StdSchedulerFactory()
    .GetScheduler()
    .UseLightCoreResolverJobFacotry(container);
scheduler.ScheduleJob(
    JobBuilder.Create().Build(),
    TriggerBuilder.Create().StartNow().Build());

// setup Topshelf
var host = HostFactory.Run(x =>
{
    x.UseLightCore(container); // Enable LightCore
    x.Service(s =>
    {
        s.ConstructUsingLightCore(); // Construct service using LightCore
        s.WhenStarted(tc => tc.Start());
        s.WhenStopped(tc => tc.Stop());
        /* more Topshelf code... */
    });
});

Die NuGet-Pakete finden sich unter Install-Package Topshelf.LightCore und Install-Package Quartz.LightCore

Montag, 15. April 2019

Rekursives Hochladen von Ordnern in SharePoint mit PNP

Die Frage heute beim Kunden war: Kann ich "schnell" und mit PNP (d.h. ohne server-code) einen kompletten Ordner - mit Unterordnern - in eine SharePoint-Bibliothek hochladen?

Die Antwort ist ja, aber... (nicht "einfach so")
Ich habe hier ein Skript vorbereitet, dass die Arbeit übernimmt:

  • Erstellen eines Ordners im SharePoint, falls gewünscht
  • Hochladen aller Dateien aus dem Quell-Ordner
  • Rekursive Verarbeitung aller Unterverzeichnisse
<#  
.SYNOPSIS  
    Uploads a Folder, including all Files and subfolders
.Notes
    This Cmdlet assumes you have PNP installed and Connect-PNPOnline was issued before this command.
.Example
    Add-PNPFolderRecursive -Path C:\temp -Folder /SiteAssets -NoRootFolderCreation
    Uploads all of c:\temp to /SiteAssets
.Example
    Add-PNPFolderRecursive -Path C:\temp -Folder /SiteAssets
    Creates a "temp" folder in /SiteAssets, then uploads all of c:\temp to the newly created /SiteAssets/temp
#>
[CmdletBinding()]
param(
    [Parameter(Position=0, 
        Mandatory=$true)]
    $Path,
    [Parameter(Position=1, 
        Mandatory=$true)]
    $Folder,
    [Parameter(Mandatory=$false)]
    [switch]$NoRootFolderCreation
)
$ErrorActionPreference="Stop"

$dstFolder = Get-PNPFolder $Folder
Write-Verbose "Acquired folder: $($dstFolder.ServerRelativeUrl)"

if(!$NoRootFolderCreation.IsPresent) {
    $folderName = Split-Path -Leaf $Path
    Write-Verbose "Creating target-folder $folderName"
    Add-PNPFolder -Name $folderName -Folder $dstFolder.ServerRelativeUrl
    $dstFolder = Get-PNPFolder "$($dstFolder.ServerRelativeUrl)/$($folderName)"
    Write-Verbose "Acquired folder: $($dstFolder.ServerRelativeUrl)"
}

# get all childs of "path" and upload the files...
$files = Get-ChildItem -File -Path $Path

$files | % {
    Write-Verbose "Uploading $($_.FullName)"
    Add-PNPFile -Path $_.FullName -Folder $dstFolder.ServerRelativeUrl | Out-Null
}

# recursive call subfolders
$foders = Get-ChildItem -Directory -Path $Path

$foders | %{
    Write-Verbose "Descending to Folder: $($_.FullName)"
    Invoke-Expression -Command ($PSCommandPath + " -Path $($_.FullName) -Folder $($dstFolder.ServerRelativeUrl) ")
}
Write-verbose "Done for $Path"

Achtung: Ich bin nicht sicher, wie sich das Skript verhält, wenn sehr viele und tiefe Ordnerstrukturen vorliegen - aufgrund der Rekursion im Skript selber vermute ich, dass die Performance ab einem bestimmten Punkt deutlich einbrechen wird.