Second Life of a Hungarian SharePoint Geek

January 31, 2016

December 1, 2015

Delete a SharePoint List Template Programmatically

Filed under: PowerShell, SP 2013 — Tags: , — Peter Holpar @ 01:08

Last week I had to “reproduce” a custom list having a rather complex structure from a root web site into a sub web site. I mean, I had to create another list having the same fields and views like the original one, but located in another web site.

I fulfilled the requirements via a PowerShell script by creating a temporary list template (without including content), creating a list based on the template, and finally deleting the list template (as the title of this post promises), since we didn’t want to allow others to create the same list in the webs they have write access to.

Note: There are several alternative ways to copy a list from one site to another, for example via the SharePoint Content Deployment and the Migration API (see samples here, here and here), or via the Export-SPWeb and Import-SPWeb PowerShell Cmdlets. as illustrated in this example. Of course, each of these solutions has its own pros and cons, and it really depends on the exact requirements (do you need to copy the content, or just the structure; do you need to copy security or not; do you need to copy the versions or not, etc.) which way we may or should choose. The comparison of these methods is beyond the scope of the current post, but it is useful to know these alternatives.

But back to the deletion of the list template. The solutions I found on the web after a quick search were IMHO a bit complicated as it should be (see this and this one for example), as they iterate through the list items in the List Template Gallery list to find the file / list item that corresponds to our template.

Based on my experience, we can find the template directly via the indexer of the SPListTemplateCollection by name. It results in solution that is so simple:

$listTemplates = $web.Site.GetCustomListTemplates($web)
$listTemplate = $listTemplates[$listName]
$listTemplateFile = $web.GetFile("_catalogs/lt/" + $listTemplate.InternalName)
$listTemplateFile.Delete()

If you need the full code we used to copy the list, it is here as well:

$url = "http://YourSharePointSite"

$targetWebUrl = "/SubSite"

$listName = "CustomListName"

$site = Get-SPSite $url
$sourceWeb = $site.RootWeb
$targetWeb = Get-SPWeb ($url + $targetWebUrl)

$list = $sourceWeb.Lists[$listName]
# create a temporary list template
# we copy the list without data, so the last param is 0
$list.SaveAsTemplate($listName, $listName, $listName, 0)

$listTemplates = $site.GetCustomListTemplates($sourceWeb)
$listTemplate = $listTemplates[$listName]
# create the list in the target web
# get the list description from the source list
$targetWeb.Lists.Add($listName, $list.Description, $listTemplate)

# delete the temporary list template, we don’t need it anymore
$listTemplateFile = $sourceWeb.GetFile("_catalogs/lt/" + $listTemplate.InternalName)
$listTemplateFile.Delete()

November 24, 2015

Recovering Passwords for SharePoint Managed Accounts

Filed under: PowerShell, Reflection, Security, SP 2013 — Tags: , , , — Peter Holpar @ 23:47

We have a SharePoint 2013 farm on a Windows 2008 R2 server. Recently we found this error in the Windows event logs in relation with the web application pool account:

Event ID 1511 – Windows cannot find the local profile and is logging you on with a temporary profile. Changes you make to this profile will be lost when you log off.

We tried to solve the issue based on the information we found in this post, but at the step below we faced the problem, that the password stored for the web application pool account (in this case we assumed domain\wa_pool_account) in our password repository does not work any more.

runas /u:domain\wa_pool_account /profile cmd

The web application pool account is registered as a managed account in SharePoint, at the original password has been already automatically changed by the system.

We could reset the password for the managed account as described in this article, but before changing the password I wanted to be sure there is no way to recover the current password from the system. I found a blog post and the related PowerShell code in TechNet Gallery, but I found the method described there (creating a new web application, and using an external tool, appcmd.exe) a bit overkill.

Instead of this I came up with an alternative solution that query the password directly from the SPManagedAccount object, via its private m_Password field (of type SPEncryptedString) that we can access by using Reflection. The public SecureStringValue property of the SPEncryptedString class returns an instance of the System.Security.SecureString class, and as illustrated here, we can “decode” its value to a simple string via Marshaling.

Using this approach, recovering the managed account password is so simple:

$ma = Get-SPManagedAccount domain\wa_pool_account
$maType = $ma.GetType()

$bindingFlags = [Reflection.BindingFlags]::NonPublic -bor [Reflection.BindingFlags]::Instance

$m_Password = $maType.GetField("m_Password", $bindingFlags)
$pwdEnc = $m_Password.GetValue($ma)

$ssv = $pwdEnc.SecureStringValue
$ptr = [System.Runtime.InteropServices.Marshal]::SecureStringToGlobalAllocUnicode($ssv)
[System.Runtime.InteropServices.Marshal]::PtrToStringUni($ptr)

October 1, 2015

Change in the User Resolution in SharePoint 2013 People Picker

Filed under: Active Directory, People Picker, PowerShell, SP 2013 — Tags: , , , — Peter Holpar @ 22:31

After a SharePoint 2010 to SharePoint 2013 migration our users complained, that in the multiple Active Directory domain environment they have (I wrote about it recently) the People Picker does not resolve the users the same way it did earlier. Only a subset of the users was resolved, users from a few domains were not included in the results at all.

The reason of this issue is a change in the GetTrustedDomains method of the Microsoft.SharePoint.Utilities.SPUserUtility class. Now (in SP 2013) it includes an extra condition, checking the value of  SPWebService.ContentService.PeoplePickerSearchInMultipleForests.

If you need the same behavior as in the SP 2010 version, you should set the value of  the PeoplePickerSearchInMultipleForests property to true.

You can achieve it using PowerShell:

$cs = [Microsoft.SharePoint.Administration.SPWebService]::ContentService
$cs.PeoplePickerSearchInMultipleForests = $true
$cs.Update()

or via C#:

SPWebService.ContentService.PeoplePickerSearchInMultipleForests = true;
SPWebService.ContentService.Update();

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"

$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"
$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"
$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"
$targetURL = "http://mysites.company.com/personal/john.doe"

# 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"
$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"
$siteAdmin = New-Object Microsoft.SharePoint.Administration.SPSiteAdministration($targetURL )
$siteAdmin.OwnerLoginName = "newDomain\john.doe"

July 23, 2015

Managing Project Server Views via PSI from PowerShell

Filed under: ALM, PowerShell, Project Server, PSI — Tags: , , , — Peter Holpar @ 07:17

If you would like to manage Project Server views from code you will find very few helpful resources (if any) on the web. The object models simply do not include classes related to this (neither on the server side nor on the client side). Although the PSI contains a View service, it is intended for internal use. Of course, that intention could not stop us to use the service at our own risk. Below I give you some useful code samples to illustrate the usage of the View service.

First of all, we create the proxy assembly, load the required Microsoft.Office.Project.Server.Library assembly in the process as well, and define some shortcuts to make it easier to reference enum and property values later on.

$pwaUrl = "http://YourProjectServer/pwa"
$svcPSProxy = New-WebServiceProxy -Namespace PSIProxy -Uri ($pwaUrl + "/_vti_bin/PSI/View.asmx?wsdl") -UseDefaultCredential
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Project.Server.Library")
$ViewConstants = [Microsoft.Office.Project.Server.Library.ViewConstants]
$ViewType = [Microsoft.Office.Project.Server.Library.ViewConstants+ViewType]

If you now the unique ID of your view, it is easy to display all of the fields and security categories associated with the view:

$viewId = [Guid]"63d3499e-df27-401c-af58-ebb9607beae8"
$view = $svcPSProxy.ReadView($viewId)
$view.ViewReportFields | % { $_.CONV_STRING }
$view.SecurityCategoryObjects | % { $_.WSEC_CAT_NAME }

If the view ID is unknown, you can get it based on the name and type of the view:

$viewName = "Your Report"
$viewType = $ViewType::PORTFOLIO

$views = $svcPSProxy.ReadViewSummaries()
$viewId = ($views.ViewReports | ? { $_.WVIEW_NAME -eq $viewName -and $_.WVIEW_TYPE -eq $viewType }).WVIEW_UID

You can list all of the views:

$views = $svcPSProxy.ReadViewSummaries()
$views.ViewReports | % {
  Write-Host $_.WVIEW_NAME ($ViewType$_.WVIEW_TYPE)
}

To change the order of the first two fields in the view:

$view = $svcPSProxy.ReadView($viewId)
$view.ViewReportFields[0].WVIEW_FIELD_ORDER = 1
$view.ViewReportFields[1].WVIEW_FIELD_ORDER = 0
$svcPSProxy.UpdateView($view)

To change the order of two arbitrary fields (based on their name) in the view:

$fieldName1 = "Finish"
$fieldName2 = "Owner"
$view = $svcPSProxy.ReadView($viewId)
$field1 = $view.ViewReportFields | ? { $_.CONV_STRING -eq $fieldName1 }
$field2 = $view.ViewReportFields | ? { $_.CONV_STRING -eq $fieldName2 }
$field1Order = $field1.WVIEW_FIELD_ORDER
$field2Order = $field2.WVIEW_FIELD_ORDER
$field1.WVIEW_FIELD_ORDER = $field2Order
$field2.WVIEW_FIELD_ORDER = $field1Order
$svcPSProxy.UpdateView($view)

To remove a field from a view:

$fieldToRemoveName = "Ende"
$view = $svcPSProxy.ReadView($viewId)
$fieldToRemove = $view.ViewReportFields | ? { $_.CONV_STRING -eq $fieldToRemoveName }
$fieldToRemove.Delete()
$svcPSProxy.UpdateView($view)

To delete the view itself:

[Void]$svcPSProxy.DeleteViewReports($viewId)

To create a new view using an existing view as a template:

$newViewName = "New View"
[Void]$svcPSProxy.CopyViewReports($viewId, $newViewName)
$newView = $svcPSProxy.ReadViewSummaries().ViewReports | ? { $_.WVIEW_NAME -eq $newViewName -and $_.WVIEW_TYPE -eq $viewType }

To list all of the fields available in a given type (in this case, for tasks):

$svcPSProxy.ReadProjectFields($ViewConstants::ViewTABLE_TASK_UID ).ViewFields | % { $_.CONV_STRING }

To append a new field at the end of the fields in the view:

$fieldToAppendName = "% Work Complete"

$fieldToAppend = $svcPSProxy.ReadProjectFields($ViewConstants::ViewTABLE_TASK_UID ).ViewFields | ? { $_.CONV_STRING -eq $fieldToAppendName }
$view = $svcPSProxy.ReadView($viewId)
$maxFieldOrder = ($view.ViewReportFields | % { $_.WVIEW_FIELD_ORDER } | measure -Maximum).Maximum

$newField = $view.ViewReportFields.NewViewReportFieldsRow()

$newField.WFIELD_UID = $fieldToAppend.WFIELD_UID
$newField.CONV_STRING = $fieldToAppend.CONV_STRING
$newField.WFIELD_TEXTCONV_TYPE = $fieldToAppend.WFIELD_TEXTCONV_TYPE
$newField.WTABLE_UID = $fieldToAppend.WTABLE_UID
$newField.WFIELD_IS_CUSTOM_FIELD = $fieldToAppend.WFIELD_IS_CUSTOM_FIELD
$newField.WFIELD_NAME_SQL = $fieldToAppend.WFIELD_NAME_SQL
$newField.WFIELD_IS_MULTI_VALUE = $fieldToAppend.WFIELD_IS_MULTI_VALUE
$newField.WFIELD_LOOKUP_TABLE_UID = $fieldToAppend.WFIELD_LOOKUP_TABLE_UID
$newField.WVIEW_UID = $view.ViewReports.WVIEW_UID
$newField.WVIEW_FIELD_ORDER = $maxFieldOrder + 1
$newField.WVIEW_FIELD_WIDTH = 100
$newField.WVIEW_FIELD_AUTOSIZE = 1
$newField.WVIEW_FIELD_CUSTOM_LABEL = [System.DBNull]::Value
$newField.WVIEW_FIELD_IS_READ_ONLY = 0

$view.ViewReportFields.AddViewReportFieldsRow($newField)
$svcPSProxy.UpdateView($view)

To inject a new field in the view before another field having a specified name:

$fieldInjectBeforeName = "% Complete"
$fieldToInjectName = "% Work Complete"

$fieldToInject = $svcPSProxy.ReadProjectFields($ViewConstants::ViewTABLE_TASK_UID ).ViewFields | ? { $_.CONV_STRING -eq $fieldToInjectName }

$view = $svcPSProxy.ReadView($viewId)

$fieldInjectBeforeOrder = ($view.ViewReportFields | ? { $_.CONV_STRING -eq $fieldInjectBeforeName }).WVIEW_FIELD_ORDER

$view.ViewReportFields | ? { $_.WVIEW_FIELD_ORDER -ge $fieldInjectBeforeOrder } | % { $_.WVIEW_FIELD_ORDER++ }

$newField = $view.ViewReportFields.NewViewReportFieldsRow()

$newField.WFIELD_UID = $fieldToInject.WFIELD_UID
$newField.CONV_STRING = $fieldToInject.CONV_STRING
$newField.WFIELD_TEXTCONV_TYPE = $fieldToInject.WFIELD_TEXTCONV_TYPE
$newField.WTABLE_UID = $fieldToInject.WTABLE_UID
$newField.WFIELD_IS_CUSTOM_FIELD = $fieldToInject.WFIELD_IS_CUSTOM_FIELD
$newField.WFIELD_NAME_SQL = $fieldToInject.WFIELD_NAME_SQL
$newField.WFIELD_IS_MULTI_VALUE = $fieldToInject.WFIELD_IS_MULTI_VALUE
$newField.WFIELD_LOOKUP_TABLE_UID = $fieldToInject.WFIELD_LOOKUP_TABLE_UID
$newField.WVIEW_UID = $view.ViewReports.WVIEW_UID
$newField.WVIEW_FIELD_ORDER = $fieldInjectBeforeOrder
$newField.WVIEW_FIELD_WIDTH = 100
$newField.WVIEW_FIELD_AUTOSIZE = 1
$newField.WVIEW_FIELD_CUSTOM_LABEL = [System.DBNull]::Value
$newField.WVIEW_FIELD_IS_READ_ONLY = 0

$view.ViewReportFields.AddViewReportFieldsRow($newField)
$svcPSProxy.UpdateView($view)

The last code sample shows how to create a new Gantt-view from scratch, appending a single field and a single security category to it:

$viewRepDS = New-Object PSIProxy.PWAViewReportsDataSet
$newView = $viewRepDS.ViewReports.NewViewReportsRow()
$newView.WVIEW_UID = [Guid]::NewGuid()
$newView.WVIEW_NAME = "New Report 2"
$newView.WVIEW_DESCRIPTION = "Test report description"

$fieldToAppendName = "% Arbeit abgeschlossen"

$fieldToAppend = $svcPSProxy.ReadProjectFields($ViewConstants::ViewTABLE_TASK_UID ).ViewFields | ? { $_.CONV_STRING -eq $fieldToAppendName }

$newField = $viewRepDS.ViewReportFields.NewViewReportFieldsRow()

$newField.WFIELD_UID = $fieldToAppend.WFIELD_UID
$newField.CONV_STRING = $fieldToAppend.CONV_STRING
$newField.WFIELD_TEXTCONV_TYPE = $fieldToAppend.WFIELD_TEXTCONV_TYPE
$newField.WFIELD_IS_CUSTOM_FIELD = $fieldToAppend.WFIELD_IS_CUSTOM_FIELD
$newField.WFIELD_NAME_SQL = $fieldToAppend.WFIELD_NAME_SQL
$newField.WFIELD_IS_MULTI_VALUE = $fieldToAppend.WFIELD_IS_MULTI_VALUE
$newField.WFIELD_LOOKUP_TABLE_UID = $fieldToAppend.WFIELD_LOOKUP_TABLE_UID
$newField.WVIEW_UID = $newView.WVIEW_UID
$newField.WVIEW_FIELD_ORDER = 0
$newField.WVIEW_FIELD_WIDTH = 100
$newField.WVIEW_FIELD_AUTOSIZE = 1
$newField.WVIEW_FIELD_CUSTOM_LABEL = [System.DBNull]::Value
$newField.WVIEW_FIELD_IS_READ_ONLY = 0
$viewRepDS.ViewReportFields.AddViewReportFieldsRow($newField)

$newSecCat = $viewRepDS.SecurityCategoryObjects.NewSecurityCategoryObjectsRow()
$newSecCat.WSEC_CAT_UID = [Microsoft.Office.Project.Server.Library.PSSecurityCategory]::MyProjects
$newSecCat.WSEC_OBJ_TYPE_UID = [Microsoft.Office.Project.Server.Library.PSSecurityObjectType]::View
$newSecCat.WSEC_OBJ_UID = $newView.WVIEW_UID
$viewRepDS.SecurityCategoryObjects.AddSecurityCategoryObjectsRow($newSecCat)

$newView.WVIEW_TYPE = $ViewType::PORTFOLIO
$newView.WVIEW_DISPLAY_TYPE = $ViewConstants::ViewDISPLAYTYPE_GANTT
$newView.WGANTT_SCHEME_UID =  $ViewConstants::GanttSchemeUidProjectCenter
$newView.WVIEW_SPLITTER_POS = 250
#  Group by (see [pub].[MSP_WEB_GROUP_SCHEMES] table in Project DB for possible values)
$newView.WGROUP_SCHEME_UID = [Guid]::Empty

$viewRepDS.ViewReports.AddViewReportsRow($newView)
$svcPSProxy.UpdateView($viewRepDS)

July 22, 2015

Create Project Server Enterprise Custom Fields via PSI from PowerShell

Filed under: ALM, PowerShell, Project Server, PSI — Tags: , , , — Peter Holpar @ 22:38

Last year I already wrote about how one can manage the Project Server Enterprise Custom Fields via the Managed Client Object Modell. We could transfer the code samples of that post from C# to PowerShell, but because of the limitations of the Managed Client Object Modell I use the PSI interface instead in this case. What are those limitations? Not all of the properties available in PSI are exposed by the Client OM, see for example the MD_PROP_SUMM_GRAPHICAL_INDICATOR field, that we can use to set the rules of graphical indicators defined for the fields. I’ll show you an example for getting and setting the indicator rules in a later post, in the current one I only show you the technique we can use to create the Enterprise Custom Fields via PSI.

One can find an existing description with code sample in Step 3 and 4 of this post, that achieves the same goal, however, I don’t like that approach for several reasons, for example, because of  we have to generate the proxy assembly based on the WSDL in the code itself. Instead of that I find the following code much more simple:

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

$pwaUrl = "http://YourProjectServer/pwa"

# create shortcuts
#
http://stackoverflow.com/a/1049010
$PSDataType = [Microsoft.Office.Project.Server.Library.PSDataType]
$Entities = [Microsoft.Office.Project.Server.Library.EntityCollection]::Entities

$svcPSProxy = New-WebServiceProxy -Namespace PSIProxy -Uri ($pwaUrl + "/_vti_bin/psi/CustomFields.asmx?wsdl") -UseDefaultCredential

$customFieldDataSet = New-Object PSIProxy.CustomFieldDataSet 

$customFieldRow = $customFieldDataSet.CustomFields.NewCustomFieldsRow()   
$customFieldRow.MD_PROP_UID = [Guid]::NewGuid()
$customFieldRow.MD_PROP_NAME = "Custom Project Field"
$customFieldRow.MD_PROP_TYPE_ENUM = $PSDataType::STRING
$customFieldRow.MD_ENT_TYPE_UID = $Entities.ProjectEntity.UniqueId
$customFieldRow.MD_PROP_IS_REQUIRED = $false
$customFieldRow.MD_PROP_IS_LEAF_NODE_ONLY = $false
$customFieldRow.MD_PROP_DESCRIPTION = "Test Field Desc."
$customFieldRow.SetMD_LOOKUP_TABLE_UIDNull()
$customFieldRow.SetMD_PROP_DEFAULT_VALUENull()
$customFieldDataSet.CustomFields.AddCustomFieldsRow($customFieldRow)

$svcPSProxy.CreateCustomFields($customFieldDataSet, $false, $true)

If you have casting issues when using the Namespace parameter of the New-WebServiceProxy cmdlet, you should read this post.

Older Posts »

The Shocking Blue Green Theme. Blog at WordPress.com.

Follow

Get every new post delivered to your Inbox.

Join 61 other followers