Quantcast
Channel: Second Life of a Hungarian SharePoint Geek
Viewing all 206 articles
Browse latest View live

Re-creating the Missing Out-of-the-Box Search Locations using PowerShell

$
0
0

Recently we migrated a SharePoint 2010 web application into an existing SharePoint 2013 farm to free up the old SharePoint 2010 farm for “recycling” (it was the last application running in the old farm). The web application has a single site collection, and contains a single business application with views and list forms having a lot of customizations via “jQuery magic”. Since a replacement of the business application is planed for the near future, we decided not to upgrade the site collection to the SharePoint 2013 user interface (version 15). Leaving it in the SharePoint 2010 mode (version 14) ensures the views and forms are working further without any modifications in the JavaScript codes. After a few days a user complained, that when searching the local web site an error is displayed instead of the search results on the _layouts/OSSSearchResults.aspx page:

Unable to display this Web Part. To troubleshoot the problem, open this Web page in a Microsoft SharePoint Foundation-compatible HTML editor such as Microsoft SharePoint Designer. If the problem persists, contact your Web server administrator

In the ULS logs we found these entries:

CoreResultsWebpart: Couldnt find location with internal name LocalSearchIndex
CoreResultsDatasourceView: Couldnt find location with internal name LocalSearchIndex

On the web we found a post from Sushant Dukhande with the description of the issue, and a suggestion for the solution.

Using the script on that site it turned out, that the LocationConfigurations property of the search proxy is really empty. In an other environment, where we tested the migration we had no such issue.

Sushant Dukhande suggests to re-provision the search application. It might really solve the problem, however in our case I felt it to be an intense change, and searched for an alternative solution. Having a look into what happens under the cover of a provisioning process, I found the method responsible for provisioning the missing search locations. It is the internal static CreateOOBLocations method of the Microsoft.Office.Server.Search.Administration.LocationFactory class (in the Microsoft.Office.Server.Search assembly).

First, we need a reference to the search service application. You can get it like this (assuming it is named "Search Service Application"):

$ssa = Get-SPEnterpriseSearchServiceApplication "Search Service Application"

or via this script (as long as you are sure, you have a single instance of this service application type in your farm):

[Microsoft.Office.Server.Search.Administration.SearchServiceApplication]$ssa = Get-SPServiceApplication | ? { $_.TypeName -eq "Search Service Application" }

To display the names of the existing search locations:

$locConfigs = $ssa.LocationConfigurations
$locConfigs | % { $_.InternalName }

The following PowerShell script shows how to invoke the CreateOOBLocations method passing the search service application as parameter using PowerShell and Reflection:

$searchAssembly = [Microsoft.Office.Server.Search.Administration.SearchServiceApplication].Assembly
$locationFactory_Type = $searchAssembly.GetType("Microsoft.Office.Server.Search.Administration.LocationFactory")

$bindingFlags = [Reflection.BindingFlags]::NonPublic -bor [Reflection.BindingFlags]::Static
$mi_CreateOOBLocations = $locationFactory_Type.GetMethod("CreateOOBLocations", $bindingFlags)
$mi_CreateOOBLocations.Invoke($null, @([Microsoft.Office.Server.Search.Administration.SearchServiceApplication]$ssa))

Invoking the CreateOOBLocations method might be not always the solution for you. The same is true for the re-provisioning process suggested by Sushant Dukhande, since it invokes the same method as well. The problem, that this method has a condition, before provisioning all of the default search locations:

if (searchApp.LocationConfigurations.Count < 1)
{
    LocationConfigurationCollection locationConfigurationsInternal = searchApp.GetLocationConfigurationsInternal(true);
    CreateLiveLocation(locationConfigurationsInternal);
    CreateLiveSuggestionsLocation(locationConfigurationsInternal);
    CreateLocalSharepointLocation(locationConfigurationsInternal);
    CreateLocalPeopleLocation(locationConfigurationsInternal);
    CreateLocalFSSharePointLocation(locationConfigurationsInternal);
}

I don’t see, how our farm “lost” its search locations, but if it is possible to “lose” only a subset of the search locations (for example, only the one called LocalSearchIndex), it won’t be re-created by the CreateOOBLocations method, as the count of search location is still not zero.

In this case, the solution may be to re-create only the missing search location via the corresponding method. In the case of the LocalSearchIndex search location it is the CreateLocalSharepointLocation method of the LocationFactory class:

$locConfigs = $ssa.LocationConfigurations
$mi_CreateLocalSharepointLocation = $locationFactory_Type.GetMethod("CreateLocalSharepointLocation", $bindingFlags)
$mi_CreateLocalSharepointLocation.Invoke($null, @([Microsoft.Office.Server.Search.Administration.LocationConfigurationCollection]$locConfigs))

After fixing the issue in the farm, I’ve tested our other farms as well to find out, whether they are affected by the same problem or not. In one of the farm, the script provided in the post I mentioned earlier detected the issue, although I was sure, there is no problem with the search. It turned out to be a false positive test. This farm has its search service as a shared service from another farm, and the user account the script was run with had no permission on the search service in that remote farm. The script simply hid away the access denied error.

However, if we create a LocationConfigurationCollection instance via its internal constructor (either with a parameter of type SearchServiceApplication or of type SearchServiceApplicationProxy), the access denied error is displayed in the case the user has no permissions, and the items of the collection can be accessed if there is no problem with the permissions.

Let’s see first the script using the SearchServiceApplication:

$bindingFlags = [Reflection.BindingFlags]::NonPublic -bor [Reflection.BindingFlags]::Instance
$ci_LocationConfigurationCollection = [Microsoft.Office.Server.Search.Administration.LocationConfigurationCollection].GetConstructor($bindingFlags, $null, @([Microsoft.Office.Server.Search.Administration.SearchServiceApplication]), $null)
$locConfigs = $ci_LocationConfigurationCollection.Invoke(@([Microsoft.Office.Server.Search.Administration.SearchServiceApplication]$ssa))
$locConfigs | % { $_.InternalName }

As I wrote, you can achieve the same via a service proxy. It is useful for example, if the application is connected to a shared search service of another farm. First, we get the proxy as:

$url = "http://YourSharePointApp/&quot;
$site = Get-SPSite $url
$serviceContext = [Microsoft.SharePoint.SPServiceContext]::GetContext($site)
$ssaAppProxy = $serviceContext.GetDefaultProxy([Microsoft.Office.Server.Search.Administration.SearchServiceApplicationProxy])

Next, we can use the same script as earlier, but in this case we invoke the internal constructor having the SearchServiceApplicationProxy parameter type:

$bindingFlags = [Reflection.BindingFlags]::NonPublic -bor [Reflection.BindingFlags]::Instance
$ci2_LocationConfigurationCollection = [Microsoft.Office.Server.Search.Administration.LocationConfigurationCollection].GetConstructor($bindingFlags, $null, @([Microsoft.Office.Server.Search.Administration.SearchServiceApplicationProxy]), $null)
$locConfigs = $ci2_LocationConfigurationCollection.Invoke(@([Microsoft.Office.Server.Search.Administration.SearchServiceApplicationProxy]$ssaAppProxy))
$locConfigs | % { $_.InternalName }



“Cannot find an SPSite object” Error From PowerShell

$
0
0

Last week I learned something new about a well known error message. We’ve tried to access a SharePoint site collection / site via the Get-SPSite / Get-SPWeb cmdlets in a test environment, but got the errors:

Get-SPWeb : Cannot find an SPSite object that contains the following Id or Url: http://YourSharePoint

and

Get-SPSite : Cannot find an SPSite object that contains the following Id or Url: http://YourSharePoint

In my practice this means either you mistyped the URL (what the message really suggests), using the wrong protocol (HTTP instead of  HTTPS or the opposite one), or your user has not the required permission (like membership in the the SharePoint_Shell_Access role, you can add via the Add-SPShellAdmin cmdlet) in the content database. In this case the URL was right, and I have db_owner permissions on the DBs, so I had to look for another reason.

Calling the Get-SPWebApplication cmdlet returned the URL of  every web applications in the farm, however when I tried to list the site collections in any (!) of these web applications, like this:

$wa = Get-SPWebApplication http://YourSharePoint
$wa.Sites

it gave me this error on the screen:

An error occurred while enumerating through a collection: The HTTP service
located at
http://localhost:32843/SecurityTokenServiceApplication/securitytoken.svc is
unavailable.  This could be because the service is too busy or because no
endpoint was found listening at the specified address. Please ensure that the
address is correct and try accessing the service again later..

Similarly, by checking the stack trace (via the value of  $StackTrace) after invoking the Get-SPSite / Get-SPWeb cmdlets, I received an other evidence of the problem with the communication with token service:

   at System.Net.HttpWebRequest.GetResponse()
   at System.ServiceModel.Channels.HttpChannelFactory`1.HttpRequestChannel.Http
ChannelRequest.WaitForReply(TimeSpan timeout)

The corresponding details from ULS logs:

Entering BeginProcessing Method of Get-SPSite.
Leaving BeginProcessing Method of Get-SPSite.
Entering ProcessRecord Method of Get-SPSite.
SecurityTokenServiceSendRequest: RemoteAddress: ‘http://localhost:32843/SecurityTokenServiceApplication/securitytoken.svc&#8217; Channel: ‘Microsoft.IdentityModel.Protocols.WSTrust.IWSTrustChannelContract’ Action: ‘http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/Issue&#8217; MessageId: ‘urn:uuid:b7dfe754-fdd4-4177-bec6-f8992a097a6b’
SPSecurityContext: Request for security token failed with exception: System.ServiceModel.ServerTooBusyException: The HTTP service located at http://localhost:32843/SecurityTokenServiceApplication/securitytoken.svc is unavailable.  This could be because the service is too busy or because no endpoint was found listening at the specified address. Please ensure that the address is correct and try accessing the service again later. —> System.Net.WebException: The remote server returned an error: (503) Server Unavailable.     at System.Net.HttpWebRequest.GetResponse()     at System.ServiceModel.Channels.HttpChannelFactory`1.HttpRequestChannel.HttpChannelRequest.WaitForReply(TimeSpan timeout)     — End of inner exception stack trace —    Server stack trace:      at System.ServiceModel.Chann…
…els.HttpChannelUtilities.ProcessGetResponseWebException(WebException webException, HttpWebRequest request, HttpAbortReason abortReason)     at System.ServiceModel.Channels.HttpChannelFactory`1.HttpRequestChannel.HttpChannelRequest.WaitForReply(TimeSpan timeout)     at System.ServiceModel.Channels.RequestChannel.Request(Message message, TimeSpan timeout)     at System.ServiceModel.Channels.ServiceChannel.Call(String action, Boolean oneway, ProxyOperationRuntime operation, Object[] ins, Object[] outs, TimeSpan timeout)     at System.ServiceModel.Channels.ServiceChannelProxy.InvokeService(IMethodCallMessage methodCall, ProxyOperationRuntime operation)     at System.ServiceModel.Channels.ServiceChannelProxy.Invoke(IMessage message)    Exception rethrown at [0]:      at System.Runtime.Remoting….
…Proxies.RealProxy.HandleReturnMessage(IMessage reqMsg, IMessage retMsg)     at System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(MessageData& msgData, Int32 type)     at Microsoft.IdentityModel.Protocols.WSTrust.IWSTrustContract.Issue(Message message)     at Microsoft.IdentityModel.Protocols.WSTrust.WSTrustChannel.Issue(RequestSecurityToken rst, RequestSecurityTokenResponse& rstr)     at Microsoft.IdentityModel.Protocols.WSTrust.WSTrustChannel.Issue(RequestSecurityToken rst)     at Microsoft.SharePoint.SPSecurityContext.SecurityTokenForContext(Uri context, Boolean bearerToken, SecurityToken onBehalfOf, SecurityToken actAs, SecurityToken delegateTo, SPRequestSecurityTokenProperties properties)
An exception occurred when trying to issue security token: The HTTP service located at http://localhost:32843/SecurityTokenServiceApplication/securitytoken.svc is unavailable.  This could be because the service is too busy or because no endpoint was found listening at the specified address. Please ensure that the address is correct and try accessing the service again later..
Microsoft.SharePoint.PowerShell.SPCmdletPipeBindException: Cannot find an SPSite object with Id or Url: http://YourSharePoint. —> System.ServiceModel.ServerTooBusyException: The HTTP service located at http://localhost:32843/SecurityTokenServiceApplication/securitytoken.svc is unavailable.  This could be because the service is too busy or because no endpoint was found listening at the specified address. Please ensure that the address is correct and try accessing the service again later. —> System.Net.WebException: The remote server returned an error: (503) Server Unavailable.     at System.Net.HttpWebRequest.GetResponse()     at System.ServiceModel.Channels.HttpChannelFactory`1.HttpRequestChannel.HttpChannelRequest.WaitForReply(TimeSpan timeout)     — End of inner exception stack…
… trace —    Server stack trace:      at System.ServiceModel.Channels.HttpChannelUtilities.ProcessGetResponseWebException(WebException webException, HttpWebRequest request, HttpAbortReason abortReason)     at System.ServiceModel.Channels.HttpChannelFactory`1.HttpRequestChannel.HttpChannelRequest.WaitForReply(TimeSpan timeout)     at System.ServiceModel.Channels.RequestChannel.Request(Message message, TimeSpan timeout)     at System.ServiceModel.Channels.ServiceChannel.Call(String action, Boolean oneway, ProxyOperationRuntime operation, Object[] ins, Object[] outs, TimeSpan timeout)     at System.ServiceModel.Channels.ServiceChannelProxy.InvokeService(IMethodCallMessage methodCall, ProxyOperationRuntime operation)     at System.ServiceModel.Channels.ServiceChannelProxy.Invoke(IMessage mess…
…age)    Exception rethrown at [0]:      at System.Runtime.Remoting.Proxies.RealProxy.HandleReturnMessage(IMessage reqMsg, IMessage retMsg)     at System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(MessageData& msgData, Int32 type)     at Microsoft.IdentityModel.Protocols.WSTrust.IWSTrustContract.Issue(Message message)     at Microsoft.IdentityModel.Protocols.WSTrust.WSTrustChannel.Issue(RequestSecurityToken rst, RequestSecurityTokenResponse& rstr)     at Microsoft.IdentityModel.Protocols.WSTrust.WSTrustChannel.Issue(RequestSecurityToken rst)     at Microsoft.SharePoint.SPSecurityContext.SecurityTokenForContext(Uri context, Boolean bearerToken, SecurityToken onBehalfOf, SecurityToken actAs, SecurityToken delegateTo, SPRequestSecurityTokenProperties properties)     at Microsoft.SharePoi…
…nt.SPSecurityContext.SecurityTokenForLegacyLoginContext(Uri context)     at Microsoft.SharePoint.SPSite.InitUserToken(SPRequest request)     at Microsoft.SharePoint.SPSite.SPSiteConstructor(SPFarm farm, Guid applicationId, Guid contentDatabaseId, Guid siteId, Guid siteSubscriptionId, SPUrlZone zone, Uri requestUri, String serverRelativeUrl, Boolean hostHeaderIsSiteName, SPUserToken userToken, Boolean appWebRequest, String appHostHeaderRedirectDomain, String appSiteDomainPrefix, String subscriptionName, String appSiteDomainId, Uri primaryUri)     at Microsoft.SharePoint.SPSite..ctor(SPFarm farm, Uri requestUri, Boolean contextSite, Boolean swapSchemeForPathBasedSites, SPUserToken userToken)     at Microsoft.SharePoint.SPSite..ctor(SPFarm farm, Uri requestUri, Boolean contextSite, SPUserToke…
…n userToken)     at Microsoft.SharePoint.SPSite..ctor(String requestUrl)     at Microsoft.SharePoint.PowerShell.SPSitePipeBind.Read(Boolean exactUrl)     — End of inner exception stack trace —     at Microsoft.SharePoint.PowerShell.SPSitePipeBind.Read(Boolean exactUrl)     at Microsoft.SharePoint.PowerShell.SPCmdletGetSite.InternalValidate()     at Microsoft.SharePoint.PowerShell.SPCmdlet.ProcessRecord()
Error Category: InvalidData    Target Object  Microsoft.SharePoint.PowerShell.SPCmdletGetSite  Details  NULL  RecommendedAction NULL
Leaving ProcessRecord Method of Get-SPSite.
Entering EndProcessing Method of Get-SPSite.
Leaving EndProcessing Method of Get-SPSite.

Checking the web server I found that the application pool “SecurityTokenServiceApplicationPool” was accidentally stopped, causing issues with the authentication. Restarting it solved my problem.

After solving the issue I played further with the token service, and stopped the web site for "SharePoint Web Services" in IIS, just to learn how the error messages differ in this case. Here is what I found.

The message for the failure of the the Get-SPSite / Get-SPWeb cmdlets is the same as the original case, however the stack trace ($StackTrace) is an other one:

   at System.Management.Automation.Internal.PipelineProcessor.SynchronousExecut
eEnumerate(Object input, Hashtable errorResults, Boolean enumerate)
   at System.Management.Automation.PipelineOps.InvokePipeline(Object input, Boo
lean ignoreInput, CommandParameterInternal[][] pipeElements, CommandBaseAst[] p
ipeElementAsts, CommandRedirection[][] commandRedirections, FunctionContext fun
cContext)
   at System.Management.Automation.Interpreter.ActionCallInstruction`6.Run(Inte
rpretedFrame frame)
   at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.
Run(InterpretedFrame frame)

Trying to enumerate the site collection in the web application (see above) results in this error:

An error occurred while enumerating through a collection: There was no
endpoint listening at
http://localhost:32843/SecurityTokenServiceApplication/securitytoken.svc that
could accept the message. This is often caused by an incorrect address or SOAP
action. See InnerException, if present, for more details..
At line:1 char:1
+ $wa.Sites
+ ~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (Microsoft.Share…rePoint.SPS
   ite]:SPEnumerator`1) [], RuntimeException
    + FullyQualifiedErrorId : BadEnumeration

And finally the ULS trace for this scenario (as you can see this time for Get-SPWeb instead of Get-SPSite) :

Entering BeginProcessing Method of Get-SPWeb.
Leaving BeginProcessing Method of Get-SPWeb.
Entering ProcessRecord Method of Get-SPWeb.
SecurityTokenServiceSendRequest: RemoteAddress: ‘http://localhost:32843/SecurityTokenServiceApplication/securitytoken.svc&#8217; Channel: ‘Microsoft.IdentityModel.Protocols.WSTrust.IWSTrustChannelContract’ Action: ‘http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/Issue&#8217; MessageId: ‘urn:uuid:bc713eb8-c1f1-453c-b333-86ef8e4411a5’
SPSecurityContext: Request for security token failed with exception: System.ServiceModel.EndpointNotFoundException: There was no endpoint listening at http://localhost:32843/SecurityTokenServiceApplication/securitytoken.svc that could accept the message. This is often caused by an incorrect address or SOAP action. See InnerException, if present, for more details. —> System.Net.WebException: Unable to connect to the remote server —> System.Net.Sockets.SocketException: No connection could be made because the target machine actively refused it 127.0.0.1:32843     at System.Net.Sockets.Socket.DoConnect(EndPoint endPointSnapshot, SocketAddress socketAddress)     at System.Net.ServicePoint.ConnectSocketInternal(Boolean connectFailure, Socket s4, Socket s6, Socket& socket, IPAddress& address,…
… ConnectSocketState state, IAsyncResult asyncResult, Exception& exception)     — End of inner exception stack trace —     at System.Net.HttpWebRequest.GetRequestStream(TransportContext& context)     at System.Net.HttpWebRequest.GetRequestStream()     at System.ServiceModel.Channels.HttpOutput.WebRequestHttpOutput.GetOutputStream()     — End of inner exception stack trace —    Server stack trace:      at System.ServiceModel.Channels.HttpOutput.WebRequestHttpOutput.GetOutputStream()     at System.ServiceModel.Channels.HttpOutput.Send(TimeSpan timeout)     at System.ServiceModel.Channels.HttpChannelFactory`1.HttpRequestChannel.HttpChannelRequest.SendRequest(Message message, TimeSpan timeout)     at System.ServiceModel.Channels.RequestChannel.Request(Message message, TimeSpan timeout) …
…    at System.ServiceModel.Channels.ServiceChannel.Call(String action, Boolean oneway, ProxyOperationRuntime operation, Object[] ins, Object[] outs, TimeSpan timeout)     at System.ServiceModel.Channels.ServiceChannelProxy.InvokeService(IMethodCallMessage methodCall, ProxyOperationRuntime operation)     at System.ServiceModel.Channels.ServiceChannelProxy.Invoke(IMessage message)    Exception rethrown at [0]:      at System.Runtime.Remoting.Proxies.RealProxy.HandleReturnMessage(IMessage reqMsg, IMessage retMsg)     at System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(MessageData& msgData, Int32 type)     at Microsoft.IdentityModel.Protocols.WSTrust.IWSTrustContract.Issue(Message message)     at Microsoft.IdentityModel.Protocols.WSTrust.WSTrustChannel.Issue(RequestSecurityToken rst, Re…
…questSecurityTokenResponse& rstr)     at Microsoft.IdentityModel.Protocols.WSTrust.WSTrustChannel.Issue(RequestSecurityToken rst)     at Microsoft.SharePoint.SPSecurityContext.SecurityTokenForContext(Uri context, Boolean bearerToken, SecurityToken onBehalfOf, SecurityToken actAs, SecurityToken delegateTo, SPRequestSecurityTokenProperties properties)
An exception occurred when trying to issue security token: There was no endpoint listening at http://localhost:32843/SecurityTokenServiceApplication/securitytoken.svc that could accept the message. This is often caused by an incorrect address or SOAP action. See InnerException, if present, for more details..
Microsoft.SharePoint.PowerShell.SPCmdletPipeBindException: Cannot find an SPSite object that contains the following Id or Url: http://YourSharePoint. —> System.ServiceModel.EndpointNotFoundException: There was no endpoint listening at http://localhost:32843/SecurityTokenServiceApplication/securitytoken.svc that could accept the message. This is often caused by an incorrect address or SOAP action. See InnerException, if present, for more details. —> System.Net.WebException: Unable to connect to the remote server —> System.Net.Sockets.SocketException: No connection could be made because the target machine actively refused it 127.0.0.1:32843     at System.Net.Sockets.Socket.DoConnect(EndPoint endPointSnapshot, SocketAddress socketAddress)     at System.Net.ServicePoint.Conne…
…ctSocketInternal(Boolean connectFailure, Socket s4, Socket s6, Socket& socket, IPAddress& address, ConnectSocketState state, IAsyncResult asyncResult, Exception& exception)     — End of inner exception stack trace —     at System.Net.HttpWebRequest.GetRequestStream(TransportContext& context)     at System.Net.HttpWebRequest.GetRequestStream()     at System.ServiceModel.Channels.HttpOutput.WebRequestHttpOutput.GetOutputStream()     — End of inner exception stack trace —    Server stack trace:      at System.ServiceModel.Channels.HttpOutput.WebRequestHttpOutput.GetOutputStream()     at System.ServiceModel.Channels.HttpOutput.Send(TimeSpan timeout)     at System.ServiceModel.Channels.HttpChannelFactory`1.HttpRequestChannel.HttpChannelRequest.SendRequest(Message message, TimeSpan timeo…
…ut)     at System.ServiceModel.Channels.RequestChannel.Request(Message message, TimeSpan timeout)     at System.ServiceModel.Channels.ServiceChannel.Call(String action, Boolean oneway, ProxyOperationRuntime operation, Object[] ins, Object[] outs, TimeSpan timeout)     at System.ServiceModel.Channels.ServiceChannelProxy.InvokeService(IMethodCallMessage methodCall, ProxyOperationRuntime operation)     at System.ServiceModel.Channels.ServiceChannelProxy.Invoke(IMessage message)    Exception rethrown at [0]:      at System.Runtime.Remoting.Proxies.RealProxy.HandleReturnMessage(IMessage reqMsg, IMessage retMsg)     at System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(MessageData& msgData, Int32 type)     at Microsoft.IdentityModel.Protocols.WSTrust.IWSTrustContract.Issue(Message message) …
…    at Microsoft.IdentityModel.Protocols.WSTrust.WSTrustChannel.Issue(RequestSecurityToken rst, RequestSecurityTokenResponse& rstr)     at Microsoft.IdentityModel.Protocols.WSTrust.WSTrustChannel.Issue(RequestSecurityToken rst)     at Microsoft.SharePoint.SPSecurityContext.SecurityTokenForContext(Uri context, Boolean bearerToken, SecurityToken onBehalfOf, SecurityToken actAs, SecurityToken delegateTo, SPRequestSecurityTokenProperties properties)     at Microsoft.SharePoint.SPSecurityContext.SecurityTokenForLegacyLoginContext(Uri context)     at Microsoft.SharePoint.SPSite.InitUserToken(SPRequest request)     at Microsoft.SharePoint.SPSite.SPSiteConstructor(SPFarm farm, Guid applicationId, Guid contentDatabaseId, Guid siteId, Guid siteSubscriptionId, SPUrlZone zone, Uri requestUri, String s…
…erverRelativeUrl, Boolean hostHeaderIsSiteName, SPUserToken userToken, Boolean appWebRequest, String appHostHeaderRedirectDomain, String appSiteDomainPrefix, String subscriptionName, String appSiteDomainId, Uri primaryUri)     at Microsoft.SharePoint.SPSite..ctor(SPFarm farm, Uri requestUri, Boolean contextSite, Boolean swapSchemeForPathBasedSites, SPUserToken userToken)     at Microsoft.SharePoint.SPSite..ctor(SPFarm farm, Uri requestUri, Boolean contextSite, SPUserToken userToken)     at Microsoft.SharePoint.SPSite..ctor(String requestUrl)     at Microsoft.SharePoint.PowerShell.SPSitePipeBind.Read(Boolean exactUrl)     — End of inner exception stack trace —     at Microsoft.SharePoint.PowerShell.SPSitePipeBind.Read(Boolean exactUrl)     at Microsoft.SharePoint.PowerShell.SPCmdletGetW…
…eb.InternalValidate()     at Microsoft.SharePoint.PowerShell.SPCmdlet.ProcessRecord()
Error Category: InvalidData    Target Object  Microsoft.SharePoint.PowerShell.SPCmdletGetWeb  Details  NULL  RecommendedAction NULL
Leaving ProcessRecord Method of Get-SPWeb.
Entering EndProcessing Method of Get-SPWeb.
Leaving EndProcessing Method of Get-SPWeb.

Hopefully these details help someone to solve the issue easier and faster when it comes.


Handling PSI Errors in PowerShell Scripts

$
0
0

Recently I work pretty much with PSI calls from PowerShell, to automate such administrative tasks, that are not available in the Project Server Client Object Model, like setting the rules of graphical indicators for the custom fields (see full code here). When using such code, sooner or later you receive an PSI exception, either due to lack of data, an invalid data, or simply because the entity you are working with are not checked out to you.

For example, if you call the CreateCustomFields as shown in the former post:

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

you may receive a PSI error that is displayed by default so in PowerShell:

Exception calling "CreateCustomFields" with "3" argument(s):
"ProjectServerError(s) LastError=CustomFieldLowerOrderBitsOutOfRange
Instructions: Pass this into PSClientError constructor to access all error
information"
At line:1 char:1
+ $svcPSProxy.CreateCustomFields($customFieldDataSet, $false, $true)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : SoapException

To display the details of the error, I’ve implemented the error handling similar to the one you find in several PSI-related C# article on MSDN (see this one, for example):

Try {
    $svcPSProxy.CreateCustomFields($customFieldDataSet, $false, $true)
}
Catch [System.Web.Services.Protocols.SoapException] {
    $ex = $_.Exception
    $clientError = New-Object Microsoft.Office.Project.Server.Library.PSClientError($ex)   
    $psiErrors = $clientError.GetAllErrors()
    $psiErrors | % {
        $err = $_
        Write-Host ([int]$err.ErrId) $err.ErrName
        For($i = 0; $i -lt $err.ErrorAttributes.Length; $i++) {
            Write-Host $err.ErrorAttributeNames()[$i] $err.ErrorAttributes[$i];
        }
    }
}

In my case, the specific error information is displayed as error code – error name pairs, like:

11521 CustomFieldMaskDoesNotMatchEntityType
11522 CustomFieldLowerOrderBitsOutOfRange

Note 1: I typically omit this kind of error handling from my code posted here on the blog just not to disturb you with details that are not relevant to the problem discussed actually in the post. It may be OK for you to run the code as it is posted as long as there is no error, however if an exception is thrown, it is good to know how to access the details of the problem. Alternatively, you can use Fiddler to capture the server response, and check the raw XML for the error information.

Note 2: A comprehensive list of PSI error codes is available here, including a short description for each error type. See the Microsoft.Office.Project.Server.Library.PSErrorID enumeration as well.


Reusing PSI Proxy Objects from PowerShell

$
0
0

Assume you create a PowerShell script that invokes PSI to perform some actions on Project Server. For example, creating a custom field as described in my former post. You save the script as a .ps1 file and invoke it from the PowerShell shell. Assume it has some parameters and your goal is to invoke it multiple times with various parameter sets. On of the first step in the script is of course the creation in the PSI proxy object, as shown in the original version:

$pwaUrl = "http://YourProjectServer/pwa&quot;
$svcPath = "/_vti_bin/psi/CustomFields.asmx?wsdl"

$svcPSProxy = New-WebServiceProxy -Namespace PSIProxy -Uri ($pwaUrl + $svcPath) -UseDefaultCredential

later in your code you invoke a method on the proxy object:

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

On the first run of the script it performs the actions without error, however on the next (and on each later) run it gives you an exception like this:

Cannot convert argument "cfds", with value: "PSIProxy.CustomFieldDataSet", for
"CreateCustomFields" to type "PSIProxy.CustomFieldDataSet": "Cannot convert
the "PSIProxy.CustomFieldDataSet" value of type "PSIProxy.CustomFieldDataSet"
to type "PSIProxy.CustomFieldDataSet"."
At line:1 char:1
+ $svcPSProxy.CreateCustomFields($customFieldDataSet, $false, $true)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodException
    + FullyQualifiedErrorId : MethodArgumentConversionInvalidCastArgument

Do you see the strange error message?

Cannot convert the "PSIProxy.CustomFieldDataSet" value of type "PSIProxy.CustomFieldDataSet" to type "PSIProxy.CustomFieldDataSet".

An object having a specific type cannot be converted to a type having the same type name. Very weird. PowerShell seems to cache the object types created dynamically by the New-WebServiceProxy cmdlet on the first run, and these types seem to be not compatible (at least, in the .NET-sense) with the ones created on the next runs. The single (or at least the most simple) solution seems to be to restart the shell after each run, but it is not very nice, to say the least.

Fortunately, I’ve found a better way in this thread for the “recycling” of the proxy object created on the first execution. Note, that the solution I find there is not the accepted answer as I wrote this post. See the answer from existenz7 on February 07, 2013 1:08 PM.

So I’ve changed the proxy creation part in my script to the form:

If ($global:svcPSProxy -eq $null)
{
  Write-Host "Connecting PSI proxy at $pwaUrl …"
  $global:svcPSProxy = New-WebServiceProxy -Namespace PSIProxy -Uri ($pwaUrl + $svcPath) -UseDefaultCredential
}
Else
{
  Write-Host "Reusing existing PSI proxy"
}

You can invoke the proxy method just like earlier:

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

Note: I typically omit this kind of proxy creation from my code posted here on the blog just not to disturb you with details that are not relevant to the problem discussed actually in the post. However, I suggest you to apply the same technique to avoid the type incompatibility issue mentioned above.


How to Start the Wrong SharePoint Workflow Unintentionally from the UI

$
0
0

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.


Project Server Displays Incorrect Effective Rights for Resources

$
0
0

We observed the following – in my opinion buggy – behavior in case of Project Server 2013 (patch state: 2016 March CU):

In our project web sites we have a web page that should display the name of the project and the title of the project owner. The name is displayed using client-side technologies, that means JavaScript and the Project Server JavaScript object model. See the code snippets below. Note, that these are parts of an AngularJS applications and cannot be used alone, but only part of the whole application. I show the code only to provide you an overview, about what I’m writing here.

The “business logic” from the controller:

  1. var promise = OurCustomService.getProjInfo($scope);
  2. promise.then(function (pi) {
  3.     var projName = pi.project.get_name();
  4.     // for some users the get_owner() mehtod returns null
  5.     var projManName = pi.project.get_owner().get_title();            
  6. }, function (errorMsg) {
  7.     console.log("Error: " + errorMsg);
  8. });
  9.  
  10.     }, function (errorMsg) {
  11.         console.log("Error: " + errorMsg);
  12.     });
  13. });

…and the service code:

  1. this.getProjInfo = function ($scope) {
  2.     var deferred = $q.defer();
  3.  
  4.     var ctx = new SP.ClientContext.get_current();
  5.  
  6.     var projContext = PS.ProjectContext.get_current();
  7.     projContext.set_isPageUrl(ctx.get_isPageUrl);
  8.     var proj = projContext.get_projects().getById($scope.projectId);
  9.     projContext.load(proj, "Name", "Owner.Title");
  10.  
  11.     projContext.executeQueryAsync(
  12.         function () {
  13.             deferred.resolve(
  14.                 {
  15.                     project: proj
  16.                 });
  17.         },
  18.         function (sender, args) {
  19.             deferred.reject('Request failed. ' + args.get_message() + '\n' + args.get_stackTrace());
  20.         }
  21.     );
  22.  
  23.     return deferred.promise;
  24. };

We found that this solution does not work for a lot of our users. Having a look via Internet Explorer (F12) Developer Tools in the code running with their credentials I found that the object returned by the pi.project.get_owner() expression is null, causing an exception as I want to access the get_title() method of this null object. It was obviously a security issue. To be able to access the title of the project owner (that is a resource as well), the user should have the View Enterprise Resource Data category permission in relation to the project owner resource.

When checking the Owner property of the Project via REST (the Guid in the URL is the ID of the given project):

http://YourProjectServer/PWA/_api/ProjectServer/Projects(‘1EF03FA9-2F7A-E411-80D4-005056B47337&#8217;)/Owner

the users having the problem received null as result, however, other users having more permissions (including the required one) received the full info of the project owner as expected.

Similarly, we have checked the resources available for the user via the REST query:

http://YourProjectServer/PWA/_api/ProjectServer/EnterpriseResources

The result for the “problematic” users did not contained the resource that is the project owner, however for the other users (the ones who had no problem with the AngularJS application mentioned above) the result included this resource as well.

No problem, it sounds OK up to this point.

However, when we selected any of  “problematic” these users on the Manage Users page in PWA Settings, clicked Check Effective Rights, change the Permission Type to Category Permission – Resource, and selected the selected the project owner (the one, the user has in practice no permission at all) in the Security Object – Resource list, the report shows, that the user has View Enterprise Resource Data permission via a group (let’s say All Users) and a category (let’s say My Project Team). Then we clicked other resources in the Security Object – Resource list as well, and found, that based on the report, the user should have View Enterprise Resource Data permission to almost all of these resources either, although based on the REST query above (http://YourProjectServer/PWA/_api/ProjectServer/EnterpriseResources) he has permission only a very few of them.

That is pretty strange. The users are really member of the All Users group, and the My Project Team category is really assigned to the All Users group.

The resources affected by the My Project Team category are selected by the “They are members of a Project Team on a project owned by the User” rule:

image

Members of the All Users group have View Enterprise Resource Data permission on resources included in the My Project Team category:

image

The resources displayed by the Effective Rights page as ones the “problematic” users have permission to are however no team members of the users at all!

How is it possible? In this post I don’t want to bore you with very deep technical details (I plan to post these details in a follow-up post later), the most important facts are, that the objects and stored procedures used to check the permissions when you want to access a resource differ from the ones used to display the effective rights.

For example, when checking the “They are members of a Project Team on a project owned by the User” rule, the pub.MSP_WEB_FN_SEC_ResourcesInCategoryRule3 table-valued function is used, where @res_uid parameter is the resource ID of the current user. It should return the ID of all of the resources that are affected by this category rule:

SELECT RES_UID AS OBJ_UID
FROM pub.MSP_ASSIGNMENTS
WHERE WRES_UID_MANAGER = @res_uid
  AND WASSN_DELETED_IN_PROJ = 0

UNION

SELECT PR.RES_UID AS OBJ_UID
FROM pub.MSP_PROJECTS P
INNER JOIN pub.MSP_PROJECT_RESOURCES PR ON PR.PROJ_UID = P.PROJ_UID
WHERE P.WRES_UID = @res_uid

That means, resources returned by the query if the resource that belongs to the current user (the one that wants to access another resource) is either an assignment owner of  a non-deleted project task assignment where the target resource (the one the current user want to access) is the assignment resource (first part of the UNION query), or there is a project that has the current user as project manager and the target resource as project resource (second part of the UNION query). That sounds logically.

On the contrary, when displaying the effective rights, the pub.MSP_WEB_FN_SEC_GetEffectiveCategories_NONCLAIMSCOMPLIANT tabled-value function is called by the pub.MSP_WEB_SP_SEC_ReadUserEffectiveRightsWithCategoryPermissions_NONCLAIMSCOMPLIANT stored procedure. This function uses the following condition to check the “They are members of a Project Team on a project owned by the User” rule, where @res_uid parameter is the resource ID of the current user, and the @wsec_obj_uid parameter is the ID of the target resource. It should insert the value 3 into the temporary @rule_table is the target resource is affected by the category rule:

IF EXISTS (SELECT TOP 1 RES_UID FROM MSP_ASSIGNMENTS WHERE WRES_UID_MANAGER = @res_uid AND RES_UID = @wsec_obj_uid)
    OR EXISTS (SELECT TOP 1 RES_UID FROM MSP_PROJECT_RESOURCES WHERE RES_UID = @wsec_obj_uid)
    OR EXISTS (SELECT TOP 1 WRES_UID as RES_UID FROM MSP_PROJECTS WHERE WRES_UID = @res_uid)
BEGIN
    INSERT INTO @rule_table(WSEC_OBJ_RULE_TYPE) VALUES (3)
END

As far as I see, this condition is wrong. It says that there should be an assignment having the current user as an assignment owner and the target resource as assignment resource (see first part of the UNION in the first SQL query above, differs in checking the WASSN_DELETED_IN_PROJ flag), or there is any project, where the target resource is a resource, or there is any project where the current user is the project manager (compare with the second part of the UNION query above, condition this time is total wrong). It means we may receive a false positive on the Effective Rights page for each resources, that are resources on any project, and for all resources if the current user (the one we check the effective rights for) is a project manager of any project. In fact, we should receive a positive value in all of these cases (as long as there is no explicit deny), it is a false positive, when there is no other, valid positive value via other categories.

I think one should re-arrange the condition like this:

IF EXISTS (SELECT TOP 1 RES_UID FROM MSP_ASSIGNMENTS WHERE WRES_UID_MANAGER = @res_uid AND RES_UID = @wsec_obj_uid AND WASSN_DELETED_IN_PROJ = 0)
    OR EXISTS (SELECT TOP 1 PR.RES_UID FROM MSP_PROJECTS P INNER JOIN MSP_PROJECT_RESOURCES PR ON PR.PROJ_UID = P.PROJ_UID
    WHERE PR.RES_UID = @wsec_obj_uid AND P.WRES_UID = @res_uid)
BEGIN
    INSERT INTO @rule_table(WSEC_OBJ_RULE_TYPE) VALUES (3)
END

or even better, one could simply re-use the logic implemented in the pub.MSP_WEB_FN_SEC_ResourcesInCategoryRule3 table-valued function:

IF EXISTS (SELECT TOP 1 OBJ_UID FROM pub.MSP_WEB_FN_SEC_ResourcesInCategoryRule3(@res_uid) WHERE OBJ_UID = @wsec_obj_uid)
BEGIN
    INSERT INTO @rule_table(WSEC_OBJ_RULE_TYPE) VALUES (3)
END


“Project Professional needs to be installed to open the project. Install Project and try again.” Warning, When Trying to Open Project in Microsoft Project

$
0
0

Recently a few users complained that they receive this warning in Internet Explorer (version 11) every time they try to open a project from PWA using Microsoft Project (for example, by clicking the Gantt-icon left to the project name in the Project Center):

Action Not Available
Project Professional needs to be installed to open the project. Install Project and try again.

Despite of the warning, the project was opened without other problems, but the users found this behavior disturbing. Other users with the same browser version did not have any problem opening projects in the same way.

I found this and this related entries on the web, but none of them helped to solve the issue in my case.

As it turned out, a JavaScript component called ProjectServer.Imported.ActiveXTaskLauncher (defined in 15\TEMPLATE\LAYOUTS\INC\PWA\LIBRARY\ps.core.js) is responsible for starting Project Professional from the browser.

The way the external application should be started depends on the browser version, in our case this code block performs the action:

var h=setTimeout(function(){
    a:;
    window.removeEventListener("blur",c);
    d()
},1e3);
function c(){
    a:;
    clearTimeout(h);
    window.removeEventListener("blur",c)
}
var i=window.addEventListener("blur",c);
window.location.href=b

where function d() contains displaying the warning:

function d(){
        a:;
        PJMessageBox(PJMessageType.Error,PWA.Res.GENERAL_ERROR_ACTION_NOT_AVAILABLE,PWA.Res.WEBPARTS_PROJECTCENTER_PROJPRO_REQUIRED,SP.UI.DialogResult.OK,null)
    }

Although it is not obvious at the first sight, what the first code block does, it starts a timer that should display the warning in 1e3 (that is 103 in JavaScript notation) milliseconds ( = 1 sec.) by calling function d(), unless this action is cancelled by function c() if a focus loss of the browser is detected via the event listener subscribed for the event blur.

The value of the b variable, that is used to set the value of the window.location.href in the last line of the first cod block above:

ms-project:osp|u|https://YourProjectServer/PWA/|g|50e73545-3ee3-489c-963d-0e0f06050737|p|<>\\YourProject|r|0

where the Guid is the ID of the PWA site collection. This value is passed to the ProjectServer.Imported.ActiveXTaskLauncher constructor by the GetActiveXTaskLauncher method of the PJ.ProjectDrilldownSatelliteImpl (15\TEMPLATE\LAYOUTS\INC\PWA\LIBRARY\ProjectDrilldownSatellite.js).

In our case the issue was performance-related: Project Professional was simply not started within the 1 second limit. The quick and dirty “solution” was to increase the time-out value to 1e4 (that is 10 seconds) by editing the ps.core.js.

Note: this workaround is generally considered as a bad practice, as it contains editing of  standard Project Server files. The patch state of the farm is 2016 March CU. I hope the 2016 April CU provides a solution for the problem, as its description at the Project Server 2013 Cumulative Updates page contains this one in the list of fixed issues:

Even though you have already installed Project 2013, you may still receive the following error message from PWA:
Action Not Available.
Project Professional needs to be installed to open the project. Install Project and try again.


PWA Settings Link is not Available on the Central Administration Web Site

$
0
0

After un-provisioning and re-provisioning a PWA instance in a SharePoint farm, we found, that the PWA Settings link is no more available at the General Application Setting on the Central Administration site.

image

When trying to access the PWA Settings pages, we had two types of errors:

No Project Web App instances found. Create at least one PWA instance before accessing this page.

In the ULS logs:

Application error when access /_layouts/15/pwa/Admin/Admin.aspx, Error=No Project Web App instances found. Create at least one PWA instance before accessing this page.   at Microsoft.Office.Project.PWA.PJBasePage..ctor(String pwaUrl)     at Microsoft.Office.Project.PWA.PJWebPage..ctor(String url)     at __ASP.FastObjectFactory_app_web_admin_aspx_ad835a1b_9lzjizur.Create_ASP__layouts_15_pwa_admin_admin_aspx()     at System.Web.Compilation.BuildManager.CreateInstanceFromVirtualPath(VirtualPath virtualPath, Type requiredBaseType, HttpContext context, Boolean allowCrossApp)     at System.Web.UI.PageHandlerFactory.GetHandlerHelper(HttpContext context, String requestType, VirtualPath virtualPath, String physicalPath)     at System.Web.HttpApplication.MaterializeHandlerExecutionStep.System.Web.Ht…
…tpApplication.IExecutionStep.Execute()     at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)
Microsoft.SharePoint.SPException: No Project Web App instances found. Create at least one PWA instance before accessing this page.    at Microsoft.Office.Project.PWA.PJBasePage..ctor(String pwaUrl)     at Microsoft.Office.Project.PWA.PJWebPage..ctor(String url)     at __ASP.FastObjectFactory_app_web_admin_aspx_ad835a1b_9lzjizur.Create_ASP__layouts_15_pwa_admin_admin_aspx()     at System.Web.Compilation.BuildManager.CreateInstanceFromVirtualPath(VirtualPath virtualPath, Type requiredBaseType, HttpContext context, Boolean allowCrossApp)     at System.Web.UI.PageHandlerFactory.GetHandlerHelper(HttpContext context, String requestType, VirtualPath virtualPath, String physicalPath)     at System.Web.HttpApplication.MaterializeHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute…
…()     at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)
Getting Error Message for Exception Microsoft.SharePoint.SPException: No Project Web App instances found. Create at least one PWA instance before accessing this page.     at Microsoft.Office.Project.PWA.PJBasePage..ctor(String pwaUrl)     at Microsoft.Office.Project.PWA.PJWebPage..ctor(String url)     at __ASP.FastObjectFactory_app_web_admin_aspx_ad835a1b_9lzjizur.Create_ASP__layouts_15_pwa_admin_admin_aspx()     at System.Web.Compilation.BuildManager.CreateInstanceFromVirtualPath(VirtualPath virtualPath, Type requiredBaseType, HttpContext context, Boolean allowCrossApp)     at System.Web.UI.PageHandlerFactory.GetHandlerHelper(HttpContext context, String requestType, VirtualPath virtualPath, String physicalPath)     at System.Web.HttpApplication.MaterializeHandlerExecutionStep.System.Web.H…
…ttpApplication.IExecutionStep.Execute()     at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

and

File Not Found

In the ULS logs:

Application error when access /_layouts/15/pwa/Admin/Admin.aspx, Error=The site with the id 5584122d-2bdd-48b5-bd15-7885712b9892 could not be found.   at Microsoft.SharePoint.SPSite..ctor(Guid id, SPFarm farm, SPUrlZone zone, SPUserToken userToken)     at Microsoft.Office.Project.PWA.PJContext.GetContextForCentralAdmin(Boolean isWebServiceCall, Guid siteID)     at Microsoft.Office.Project.PWA.PJBasePage..ctor(String pwaUrl)     at Microsoft.Office.Project.PWA.PJWebPage..ctor(String url)     at __ASP.FastObjectFactory_app_web_admin_aspx_ad835a1b_9lzjizur.Create_ASP__layouts_15_pwa_admin_admin_aspx()     at System.Web.Compilation.BuildManager.CreateInstanceFromVirtualPath(VirtualPath virtualPath, Type requiredBaseType, HttpContext context, Boolean allowCrossApp)     at System.Web.UI.PageHand…
…lerFactory.GetHandlerHelper(HttpContext context, String requestType, VirtualPath virtualPath, String physicalPath)     at System.Web.HttpApplication.MaterializeHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
System.IO.FileNotFoundException: The site with the id 5584122d-2bdd-48b5-bd15-7885712b9892 could not be found.    at Microsoft.SharePoint.SPSite..ctor(Guid id, SPFarm farm, SPUrlZone zone, SPUserToken userToken)     at Microsoft.Office.Project.PWA.PJContext.GetContextForCentralAdmin(Boolean isWebServiceCall, Guid siteID)     at Microsoft.Office.Project.PWA.PJBasePage..ctor(String pwaUrl)     at Microsoft.Office.Project.PWA.PJWebPage..ctor(String url)     at __ASP.FastObjectFactory_app_web_admin_aspx_ad835a1b_9lzjizur.Create_ASP__layouts_15_pwa_admin_admin_aspx()     at System.Web.Compilation.BuildManager.CreateInstanceFromVirtualPath(VirtualPath virtualPath, Type requiredBaseType, HttpContext context, Boolean allowCrossApp)     at System.Web.UI.PageHandlerFactory.GetHandlerHelper(HttpConte…
…xt context, String requestType, VirtualPath virtualPath, String physicalPath)     at System.Web.HttpApplication.MaterializeHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
Getting Error Message for Exception System.Web.HttpException (0x80004005): Exception of type ‘System.Web.HttpException’ was thrown. —> System.IO.FileNotFoundException: The site with the id 5584122d-2bdd-48b5-bd15-7885712b9892 could not be found.     at Microsoft.SharePoint.SPSite..ctor(Guid id, SPFarm farm, SPUrlZone zone, SPUserToken userToken)     at Microsoft.Office.Project.PWA.PJContext.GetContextForCentralAdmin(Boolean isWebServiceCall, Guid siteID)     at Microsoft.Office.Project.PWA.PJBasePage..ctor(String pwaUrl)     at Microsoft.Office.Project.PWA.PJWebPage..ctor(String url)     at __ASP.FastObjectFactory_app_web_admin_aspx_ad835a1b_9lzjizur.Create_ASP__layouts_15_pwa_admin_admin_aspx()     at System.Web.Compilation.BuildManager.CreateInstanceFromVirtualPath(VirtualPath virtualP…
…ath, Type requiredBaseType, HttpContext context, Boolean allowCrossApp)     at System.Web.UI.PageHandlerFactory.GetHandlerHelper(HttpContext context, String requestType, VirtualPath virtualPath, String physicalPath)     at System.Web.HttpApplication.MaterializeHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()     at System.Web.HttpApplication.MaterializeHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()     at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

PowerShell cmdlets have not found any site with the given Id (5584122d-2bdd-48b5-bd15-7885712b9892) as well, thus I made a query against the SharePoint configuration database to find out more details:

SELECT [Id],
    [ClassId],
    [ParentId],
    [Name],
    [Status],
    [Version],
    [Properties]
FROM
    [SharePointConfigDB].[dbo].[Objects]
WHERE
    [Properties] LIKE ‘%5584122d-2bdd-48b5-bd15-7885712b9892%’

Note: You should replace the database name and the Guid in the query, if you happen to have the same issue, and would like to follow my steps.

This query returned a configuration object of type Microsoft.Office.Project.Server.Administration.ProjectSite, having a Name = 5584122d-2bdd-48b5-bd15-7885712b9892, ClassId = FB30E224-C739-43A7-8BEA-11D170824A6A, and an Id = 13B1DAD3-B005-46D9-B712-CDF7CC2FEAFF.

I was able to delete the orphaned object via the command:

STSADM -o deleteconfigurationobject -id 13B1DAD3-B005-46D9-B712-CDF7CC2FEAFF

This deletion solved our problem.

Note: There is no guarantee, that this solution works for you as well. Don’t forget to create a backup of your configuration database before you performs this kind of changes!



How to Copy a Document between Folders of a SharePoint Document Library using VBA and REST

$
0
0

One of our customers wanted to copy Office documents (.docx, .xlsx, .xlsm) from a folder of a SharePoint document library into another one using Excel macros. The Excel document, that contains the macro, is located in the root of the document library.

For some mysterious reason, that we could not have really identified, the files were created having a size of 0 bytes at the target location for these types of files, when they were copied using the “classical” methods (more on these methods a bit later), although other file types, like text or image files could be copied without problem.

So what are those classical methods, most of them available already in other blogs or forum threads?

Version 1

Copy the files using the FileSystemObject.

You should add a reference to the Windows Script Host Obejct Model library in VBA.

Note: We convert the URL of the document library into an UNC form in the ConvertPath method. That means, it converts a URL like http://YourSharePoint/DocLib into \\YourSharePoint\DocLib. However, if you have configured HTTPS for your SharePoint, you need to convert the URL into this form: \\YourSharePoint@SSL\DavWWWRoot\DocLib. In this case, you should either extend the ConvertPath method, or simply use a fix path in your code as a quick and dirty solution.

Function ConvertPath(path) As String
  ConvertPath = Replace(path, " ", "%20")
  ConvertPath = Replace(ConvertPath, "/", "\")
  ConvertPath = Replace(ConvertPath, "http:", "")
End Function

Private Sub CopyFiles1()
  Dim sDocPath As String
  Dim sDocPathConv As String
  Dim sFileName As String
  Dim sTargetPath As String
  Dim sSourcePath As String
  Dim fso As FileSystemObject

  Set fso = New FileSystemObject ‘ CreateObject("Scripting.FileSystemObject")
  sDocPath = ThisWorkbook.Path
  sFileName = "WorkBook.xlsx"

  sDocPathConv = ConvertPath(sDocPath)

  sSourcePath = sDocPathConv  & "\Folder1\" & sFileName
  Debug.Print "Source: " & sSourcePath
       
  sTargetPath = sDocPathConv  & "\Folder2\" & sFileName
  Debug.Print "Target: " & sTargetPath

  fso.CopyFile sSourcePath, sTargetPath, True

End Sub

Version 2

Copy the files using SharePoint document library as mapped drive using the FileSystemObject.

In addition to the Windows Script Host Obejct Model library, you need an additional reference to the WSHControllerLibrary as well.

See a similar sample here.

The MapNetworkDrive method seems to handle the conversion of the SharePoint doc. lib. URL into an UNC form, but you might still need to invoke the conversion method if you receive this error on mapping the drive:

800704DC – The operation being requested was not performed because the user has not been authenticated

Private Sub CopyFiles2()
  Dim sDocPath As String
  Dim sFileName As String
  Dim sTargetPath As String
  Dim sSourcePath As String
  Dim sDriveLetter As String
  Dim fso As FileSystemObject
  Dim net As WshNetwork

  ‘ drive letter should be available (not mapped to a share already, to avoid error ‘80070055 – The local device is already in use’)
  sDriveLetter = "S:"
  sFileName = "WorkBook.xlsx"
   
  Set fso = New FileSystemObject ‘ CreateObject("Scripting.FileSystemObject")
  sDocPath = ThisWorkbook.Path

  ‘sDocPath = ConvertPath(sDocPath)

  Set net = New WshNetwork ‘ CreateObject("WScript.Network")
  Debug.Print "Path to map: " & sDocPath
  net.MapNetworkDrive sDriveLetter, sDocPath

  sSourcePath = sDriveLetter & "\Folder1\" & sFileName
  Debug.Print "Source: " & sSourcePath

  sTargetPath = sDriveLetter "\Folder2\" & sFileName
  Debug.Print "Target: " & sTargetPath

  fso.CopyFile sSourcePath, sTargetPath, True

  net.RemoveNetworkDrive sDriveLetter

  Set net = Nothing
  Set fso = Nothing

End Sub

Version 3

We could have downloaded the file, and upload it via web service calls, but I felt this second part simply far too complex.

Version 4

We have used a method to create temporary folder names:

Private Declare Function GetTempPath Lib "kernel32" Alias "GetTempPathA" (ByVal nBufferLength As Long, ByVal lpBuffer As String) As Long

Private Declare Function GetTempFileName Lib "kernel32" Alias "GetTempFileNameA" _
  (ByVal lpszPath As String, _
  ByVal lpPrefixString As String, _
  ByVal wUnique As Long, _
  ByVal lpTempFileName As String) As Long

Public Function Get_Temp_File_Name( _
  Optional sPrefix As String = "VBA", _
  Optional sExtension As String = "") As String

  Dim sTmpPath As String * 512
  Dim sTmpName As String * 576
  Dim nRet As Long
  Dim F As String

  nRet = GetTempPath(512, sTmpPath)
  If (nRet > 0 And nRet < 512) Then
    nRet = GetTempFileName(sTmpPath, sPrefix, 0, sTmpName)
    If nRet <> 0 Then F = Left$(sTmpName, InStr(sTmpName, vbNullChar) – 1)
    If sExtension > "" Then
      Kill F
      If Right(F, 4) = ".tmp" Then F = Left(F, Len(F) – 4)
      F = F & sExtension
    End If

    Get_Temp_File_Name = F
  End If

End Function

Then, instead of copying directly between the document library folders in Version 2 and 3, we copied the file first from source folder to the local temporary file in the file system, then from the temporary file to the target folder.

That means, instead of

fso.CopyFile sSourcePath, sTargetPath, True

we used this:

Dim tempPath As String

tempPath = Get_Temp_File_Name
Debug.Print "Temp path: " & tempPath

fso.CopyFile sSourcePath, tempPath, True
fso.CopyFile tempPath, sTargetPath, True
fso.DeleteFile tempPath

It did not help to remedy our problem with the empty Office files created during the copy operation.

Version 5

This is our last sample code, and it is the one that works at the customer without problem with the file size / content. In this case we utilize the getfilebyserverrelativeurl and copyto methods of the files and folders REST API.

Using this method has a further benefit, that – depending on the file size – might be even a significant one. In contrast with the other methods described earlier, this one does not download / upload the file content. It sends only a command to the server to copy the file, after we perform the authentication in the GetDigest method.

For this example to work, you need a reference to the Microsoft XML, v6.0 library in VBA.

Private Function GetDigest(url As String)
  Dim http As MSXML2.XMLHTTP

  Set http = New MSXML2.XMLHTTP

  http.Open "POST", url + "/_api/contextinfo", False
  http.setRequestHeader "ContentType", "application/json;odata=verbose"
  http.send ""

  GetDigest = http.responseXML.SelectSingleNode("//d:FormDigestValue").nodeTypedValue
 
  Set http = Nothing

End Function

Private Sub CopyFiles3()
  Dim webAppUrl As String
  Dim serverRelUrlOfSite As String
  Dim siteUrl As String
  Dim docLibName As String
  Dim serverRelUrlOfDocLib As String
  Dim sourcePath As String
  Dim targetPath As String

  Dim http As MSXML2.XMLHTTP
  Dim digest As String
  Dim url As String

  webAppUrl = "http://YourSharePoint&quot;
  serverRelUrlOfSite = "/subsite1/subsite1.2"
  docLibName = "YourDocLib"
 
sFileName = "WorkBook.xlsx"

  siteUrl = webAppUrl & serverRelUrlOfSite
  serverRelUrlOfDocLib = serverRelUrlOfSite & "/" & docLibName

  sourcePath = "/Folder1/" & sFileName
  Debug.Print "Source: " & sourcePath

  targetPath = "/Folder2/" & sFileName"
  Debug.Print "Target: " & targetPath

  ‘ get the authentication digest
  digest = GetDigest(siteUrl)
  Set http = New MSXML2.XMLHTTP

  url = siteUrl & "/_api/web/getfilebyserverrelativeurl(‘" & serverRelUrlOfDocLib & sourcePath & "’)/copyto(strnewurl=’" & serverRelUrlOfDocLib & targetPath & "’,boverwrite=true)"

  http.Open "POST", url, False
  http.setRequestHeader "X-RequestDigest", digest

  http.send ""
  
  Set http = Nothing

End Sub

If you need to move the files instead of copying, you should simply use the MoveTo method instead of the CopyTo method.

Note: As you see, this code does not contain any error handling, so please extend it if you would like to use it in production.


Reference Default Calendar Scopes in a URL

$
0
0

As you know, you can create calendar views with various default scopes (e.g. Day / Week / Month, see screenshot below).

image

It means a single .aspx page per view, for example CalendarDay.aspx, CalendarWeek.aspx, CalendarMonth.aspx. You can then link these pages in HTML hyperlinks or send a link in mail by referring the given page.

But if you try to avoid creating and administering these three views for each of your calendars, is it possible to have only a single, universal page (like Calendar.aspx), and still have the ability to create a URL for the page that determines, with which scope the calendar would be displayed by default?

Unfortunately, it is not possible out-of-the-box, but with some scripting we can find a way.

Add a Script Editor Web Part (category: Media and Content) to the calendar view page after the calendar itself. Include the following script in the web part:

  1. <script type="text/javascript">
  2.  
  3. function getQueryStringParameter(paramToRetrieve) {
  4.     var params =
  5.     document.URL.split("?")[1].split("&");
  6.     var strParams = "";
  7.     for (var i = 0; i < params.length; i = i + 1) {
  8.         var singleParam = params[i].split("=");
  9.         if (singleParam[0] == paramToRetrieve)
  10.             return singleParam[1];
  11.     }
  12. }
  13.  
  14. var timer;
  15.  
  16. function registerMoveView() {
  17.  
  18.     var cirInstance = SP.UI.ApplicationPages.CalendarInstanceRepository.firstInstance();
  19.  
  20.     if (cirInstance) {
  21.         MoveView(startView);
  22.         window.clearInterval(timer);
  23.     }
  24.     else if (!timer) {
  25.         timer = window.setInterval(registerMoveView, 100);
  26.     }
  27. }
  28.  
  29. var viewNames = ["month", "week", "day"];
  30.  
  31. var startView = getQueryStringParameter("StartView")
  32.  
  33. if (viewNames.indexOf(startView) != -1) {
  34.     SP.SOD.executeOrDelayUntilScriptLoaded(registerMoveView, "sp.ui.applicationpages.calendar.js");
  35. }
  36.  
  37. </script>

After you save the changes on the page, one can access the calendar with the daily scope using a URL like this:

http://YourSharePoint/subweb/Lists/Calendar/calendar.aspx?StartView=day

One can access the monthly and weekly scopes as well via URL, simply by using the StartView query string values “month” and “week” respectively.

Note 1: The query string values “month”, “week” and “day” are strictly case sensitive, using a value like “Day” displays the default scope defined for the view (see the screenshot above).

Note 2: The sample above assumes you had a single calendar view on your you page. If you happen to have more, it would affect only the first one, see call to the firstInstance method in the script.

Note 3: The calendar instance is first available in the script after the real default scope completely rendered by the browser. We introduced a timer to overcome that issue, but if your browser is slow, you can still experience a blinking effect when switching to the new “default” scope, the one you selected via the StartView query string parameter.


Content Query Web Part Issue on SharePoint 2013 Developer Site – Just another Workaround

$
0
0

Yesterday I was to test the Content Query Web Part (CQWP), but when wanted to edit its settings, I got an error that was logged in ULS like:

Application error when access /Pages/YourPage.aspx, Error=Key cannot be null.  Parameter name: key   at System.Collections.SortedList.IndexOfKey(Object key)     at System.Collections.SortedList.ContainsKey(Object key)     at Microsoft.SharePoint.Publishing.WebControls.ContentByQueryToolPart.AppendListTypes(SortedList sortedListItems, SPListTemplateCollection listTypes)     at Microsoft.SharePoint.Publishing.WebControls.ContentByQueryToolPart.populateListTypeDropDown()     at Microsoft.SharePoint.Publishing.WebControls.ContentByQueryToolPart.populateCBQControls()     at Microsoft.SharePoint.Publishing.WebControls.ContentByQueryToolPart.OnPreRender(EventArgs e)     at System.Web.UI.Control.PreRenderRecursiveInternal()     at System.Web.UI.Control.PreRenderRecursiveInternal()     at System…
….Web.UI.Control.PreRenderRecursiveInternal()     at System.Web.UI.Control.PreRenderRecursiveInternal()     at System.Web.UI.Control.PreRenderRecursiveInternal()     at System.Web.UI.Control.PreRenderRecursiveInternal()     at System.Web.UI.Control.PreRenderRecursiveInternal()     at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
System.ArgumentNullException: Key cannot be null.  Parameter name: key    at System.Collections.SortedList.IndexOfKey(Object key)     at System.Collections.SortedList.ContainsKey(Object key)     at Microsoft.SharePoint.Publishing.WebControls.ContentByQueryToolPart.AppendListTypes(SortedList sortedListItems, SPListTemplateCollection listTypes)     at Microsoft.SharePoint.Publishing.WebControls.ContentByQueryToolPart.populateListTypeDropDown()     at Microsoft.SharePoint.Publishing.WebControls.ContentByQueryToolPart.populateCBQControls()     at Microsoft.SharePoint.Publishing.WebControls.ContentByQueryToolPart.OnPreRender(EventArgs e)     at System.Web.UI.Control.PreRenderRecursiveInternal()     at System.Web.UI.Control.PreRenderRecursiveInternal()     at System.Web.UI.Control.PreRenderRecur…
…siveInternal()     at System.Web.UI.Control.PreRenderRecursiveInternal()     at System.Web.UI.Control.PreRenderRecursiveInternal()     at System.Web.UI.Control.PreRenderRecursiveInternal()     at System.Web.UI.Control.PreRenderRecursiveInternal()     at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
Getting Error Message for Exception System.Web.HttpUnhandledException (0x80004005): Exception of type ‘System.Web.HttpUnhandledException’ was thrown. —> System.ArgumentNullException: Key cannot be null.  Parameter name: key     at System.Collections.SortedList.IndexOfKey(Object key)     at System.Collections.SortedList.ContainsKey(Object key)     at Microsoft.SharePoint.Publishing.WebControls.ContentByQueryToolPart.AppendListTypes(SortedList sortedListItems, SPListTemplateCollection listTypes)     at Microsoft.SharePoint.Publishing.WebControls.ContentByQueryToolPart.populateListTypeDropDown()     at Microsoft.SharePoint.Publishing.WebControls.ContentByQueryToolPart.populateCBQControls()     at Microsoft.SharePoint.Publishing.WebControls.ContentByQueryToolPart.OnPreRender(EventArgs e)    …
… at System.Web.UI.Control.PreRenderRecursiveInternal()     at System.Web.UI.Control.PreRenderRecursiveInternal()     at System.Web.UI.Control.PreRenderRecursiveInternal()     at System.Web.UI.Control.PreRenderRecursiveInternal()     at System.Web.UI.Control.PreRenderRecursiveInternal()     at System.Web.UI.Control.PreRenderRecursiveInternal()     at System.Web.UI.Control.PreRenderRecursiveInternal()     at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)     at System.Web.UI.Page.HandleError(Exception e)     at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)     at System.Web.UI.Page.ProcessRequest(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesA…
…fterAsyncPoint)     at System.Web.UI.Page.ProcessRequest()     at System.Web.UI.Page.ProcessRequest(HttpContext context)     at System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()     at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

As it turned out by the analyzing the source code of the methods in the stack trace, the error is caused by the AppendListTypes method (Microsoft.SharePoint.Publishing.WebControls.ContentByQueryToolPart class in Microsoft.SharePoint.Publishing assembly). This method iterates through the available list templates in the web site just to add their name to a list box of the tool part. The ContainsKey method of the System.Collections.SortedList class is used to check, if the given name already exists in the list, for the case multiple list templates would have the same name. In this case a numeric counter would be appended to the name to avoid name conflicts in the list. So far so good.

But if the Name property of the list template happens to be null, calling the ContainsKey method with this value as key causes an ArgumentNullException. It is the IndexOfKey method of the SortedList class that throws the exception, just to be exact.

We can find out which list templates have null as their Name via this script:

$web = Get-SPWeb http://YourSite
$web.ListTemplates | ? { $_.Name -eq $null }

This script returns in our case a single list template with InternalNameDraftAppsListTemplate”. The type property of the template is SPListTemplateType.DeveloperSiteDraftApps, that means the integer value 1230 (decimal) or 0x4ce (hexadecimal).

Just as remark: there is another template without name, but in this case the name is not null, but it is an empty string, so it does not cause problem for the CQWP tool part. This list template has the internal name “wfsvc”. Its list template type is not included in the SPListTemplateType enumeration, the integer value of the type is 4501. This list template serves as a base for a list instance that is used by SharePoint to keep track of workflows.

My first idea was to set the Name property of the “DraftAppsListTemplate” template via a script like this:

$lt = $web.ListTemplates | ? { $_.Name -eq $null }
$lt.Name = ‘DraftAppsListTemplate’
$lt.Update()

Of course it fails, due to the read-only behavior of the Name property and due the lack of the Update method on the SPListTemplate class.

As a second try, I searched for possible solutions on the web, and found this forum thread, where it is suggested as a workaround to temporary inactivate the feature the list template belongs to while one wants to edit the CQWP properties:

Disable-SPFeature -Identity e374875e-06b6-11e0-b0fa-57f5dfd72085 -Url http://YourSite

I don’t like this idea, as:

– there might be cases that somebody without the permission to activate / inactivate features should edit the CQWP

– inactivating the feature affects the site functionality and may disrupt of work other using the same environment

So instead of that I came up with another workaround. Of course, it is not a fully supported solution so you should use it only for your own risk.

I’ve created a backup copy of the file containing the list template (located at 15\TEMPLATE\FEATURES\Developer\DeveloperListTemplates.xml), and edited the original version, adding the missing DisplayName attribute to the XML definition of the list template.

Note: The DisplayName attribute corresponds to the Name property of the SPListTemplate class, while the Name attribute translates to the InnerName property.

<?xml version="1.0" encoding="utf-8" ?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/"&gt;
  <ListTemplate
      FeatureId="E374875E-06B6-11E0-B0FA-57F5DFD72085"
      BaseType="0"
      Name="DraftAppsListTemplate"
      DisplayName="DraftAppsListTemplate"
      DisableAttachments="TRUE"
      DisallowContentTypes="TRUE"
      FolderCreation="FALSE"
      Hidden="TRUE"
      HiddenList="False"
      OnQuickLaunch="False"
      SecurityBits="11"
      Type="1230"
      VersioningEnabled="FALSE"
      AllowDeletion="FALSE" >
  </ListTemplate>
</Elements>

I saved the changes and performed an IISRESET. After that I had no more list templates with null as Name, and was able to edit the CQWP properties without inactivating any features.

The real solution would be to fix the error in the AppendListTypes method.

First of all, one should avoid possible null values. Instead of the current code:

string name = template.Name;

it would be better:

string name = (template.Name != null) ? template.Name : template.InnerName;

I assume the Name attribute of the ListTemplate schema, that corresponds the InnerName property of the SPListTemplate class is a mandatory one, as described here. So the new expression could not be evaluated to null any more. Although at the same place the DisplayName attribute that corresponds to the Name property of the SPListTemplate class is defined as required either, its lack of the original version of the DeveloperListTemplates.xml file demonstrates, it is not mandatory in practice. If we want to be sure we can append this line to the former one:

name = name ?? string.Empty;

Second, I don’t see if there is really any possible business case, when one would include items from lists created based on the DraftAppsListTemplate in the CQWP. So one should simply skip this list template at all in the modified version of the AppendListTypes method:

switch (type)
{
    case 110:
    case 0x76:
    case 0x75:
    case 0x2776:
    case 600:
    case 1230:
        break;

    default:
    {
       // code block is omitted intentionally
    }
}

Our farm is patched with Cumulative Update, May 2016, might be that Microsoft provides a fix in a later update.


Project Publishing Failed due to Scheduling Conflict

$
0
0

Recently we found an issue in the Project Server on the Manage Queue Jobs page in our PWA (http://YourProjectServer/PWA/_layouts/15/pwa/Admin/queue.aspx). A project publishing failed due to an error. The corresponding event log entries are:

PWA:http://YourProjectServer/PWA, ServiceApp:ProjectServerApplication, User:i:0#.w|YourDomain\FarmUser, PSI: SSP: Scheduling failed., LogLevelManager Warning-ulsID:0x38737A33 has no entities explicitly specified.
PWA:
http://YourProjectServer/PWA, ServiceApp:ProjectServerApplication, User:i:0#.w|YourDomain\FarmUser, PSI: Microsoft.Office.Project.Scheduling.EventHorizonFinishException: Error in the application.     at Microsoft.Office.Project.Scheduling.Calendar.NextShift(Int32& day, Int32& shift, RelTimeInterval& interval)     at Microsoft.Office.Project.Scheduling.Allocate.ScheduleForward(IShiftReader shiftReader, AllocationInfo allocationInfo, RelTimeInterval timeIn, IShiftAvailability shiftAvailability, AssignmentWorkList workList, Int64& work, RelTimeInterval& timeOut)     at Microsoft.Office.Project.Scheduling.Allocate.CalendarTimeBetween(IShiftReader shiftReader, RelTimeInterval timeIn, RelTime& duration)     at Microsoft.Office.Project.Scheduling.Allocate.CalendarTimeBetween(IShiftRea…
…der shiftReader, RelTime start, RelTime finish, RelTime& duration)     at Microsoft.Office.Project.Scheduling.SlackUtil.ComputeFinishSlack(Session session, ProjectSchedules projects, Project project, Task task, TaskSchedule taskSchedule, Calendar calendar, Boolean mixedAsapAlap)     at Microsoft.Office.Project.Scheduling.SlackUtil.ScheduleSlack(Session session, ProjectSchedules projects, Project project, Task task, TaskSchedule taskSchedule, Calendar calendar)     at Microsoft.Office.Project.Scheduling.SlackUtil.ScheduleSlack(Session session, ProjectSchedules projects, SchedulingOptions options)     at Microsoft.Office.Project.Scheduling.SessionSchedulerBase.Schedule(SessionSchedule schedule, SchedulingOptions options)     at Microsoft.Office.Project.Scheduling.Scheduler.ScheduleApply(ISch…
…edulingEngineData data, ISchedulingOptions options, SchedulingDataFilter filter)     at Microsoft.Office.Project.Server.BusinessLayer.ProjectSchedule.Schedule(), LogLevelManager Warning-ulsID:0x38737A34 has no entities explicitly specified.
Microsoft.Office.Project.Scheduling.EventHorizonFinishException: Error in the application.     at Microsoft.Office.Project.Scheduling.Calendar.NextShift(Int32& day, Int32& shift, RelTimeInterval& interval)     at Microsoft.Office.Project.Scheduling.Allocate.ScheduleForward(IShiftReader shiftReader, AllocationInfo allocationInfo, RelTimeInterval timeIn, IShiftAvailability shiftAvailability, AssignmentWorkList workList, Int64& work, RelTimeInterval& timeOut)     at Microsoft.Office.Project.Scheduling.Allocate.CalendarTimeBetween(IShiftReader shiftReader, RelTimeInterval timeIn, RelTime& duration)     at Microsoft.Office.Project.Scheduling.Allocate.CalendarTimeBetween(IShiftReader shiftReader, RelTime start, RelTime finish, RelTime& duration)     at Microsoft.Office.Project.Scheduling.SlackU…
…til.ComputeFinishSlack(Session session, ProjectSchedules projects, Project project, Task task, TaskSchedule taskSchedule, Calendar calendar, Boolean mixedAsapAlap)     at Microsoft.Office.Project.Scheduling.SlackUtil.ScheduleSlack(Session session, ProjectSchedules projects, Project project, Task task, TaskSchedule taskSchedule, Calendar calendar)     at Microsoft.Office.Project.Scheduling.SlackUtil.ScheduleSlack(Session session, ProjectSchedules projects, SchedulingOptions options)     at Microsoft.Office.Project.Scheduling.SessionSchedulerBase.Schedule(SessionSchedule schedule, SchedulingOptions options)     at Microsoft.Office.Project.Scheduling.Scheduler.ScheduleApply(ISchedulingEngineData data, ISchedulingOptions options, SchedulingDataFilter filter)     at Microsoft.Office.Project.Ser…
…ver.BusinessLayer.ProjectSchedule.Schedule() StackTrace:  at Microsoft.Office.Project.Server.Native.dll: (sig=134ce9b5-648a-4c93-8b48-a364a0364a09|2|microsoft.office.project.server.native.pdb, offset=3C16) at Microsoft.Office.Project.Server.Native.dll: (offset=1265D)

Opening the project in Project Professional multiple scheduling conflicts were displayed, see the Tooltip, the End field having red underline, the red time-bars on the Gantt-chart on the right and the value of the Warning field on the screenshot below.

image

These conflicts were the result of a failed manual task scheduling. By right-clicking on the task we can either turn off  the manual scheduling for the given task or fix the issue via the Task Inspector. Optionally, you can hide the warning as well, by setting the value of the Ignore Warning field.

After fixing all of the conflicts, the project publishing was successful.


Project Publishing Failed due to Deleted SharePoint User

$
0
0

In my recent post I wrote about a project publishing issue that was a result of a scheduling conflict.

The other day we had a similar problem with project publishing, but in this special case failed an other sub-process of the publishing process, the task synchronization. Another important difference from the former one is that at the scheduling conflict it was an end-user issue (a business user caused the conflict in the project plan scheduling), and in the case I’m writing about now, it was a mistake of an administrator plus a suboptimal code block in Project Server, that we can consider as a bug as well. But more on that a bit later…

First the symptoms we experienced. On the Manage Queue Jobs page in our PWA (http://YourProjectServer/PWA/_layouts/15/pwa/Admin/queue.aspx) we saw an entry of Job TypeSharePoint Task List Project” and Job State Failed And Blocking Correlation”.

Clicking on the entry displayed this information:

Queue: GeneralQueueJobFailed (26000) – ManagedModeTaskSynchronization.SynchronizeTaskListInManagedModeMessage. Details: id=’26000′ name=’GeneralQueueJobFailed’ uid=’46918ff3-3719-e611-80f4-005056b44e32′ JobUID=’adcad466-44bd-444b-a803-073fd12a2426′ ComputerName=’4fc61930-ef50-461b-b9ef-084a666c61ca’ GroupType=’ManagedModeTaskSynchronization’ MessageType=’SynchronizeTaskListInManagedModeMessage’ MessageId=’1′ Stage=” CorrelationUID=’cd56b408-a303-0002-d428-98cd03a3d101′.

The corresponding entries in the ULS logs:

PWA:http://YourProjectServer/PWA, ServiceApp:ProjectServerApplication, User:i:0#.w|YourDomain\FarmAccount, PSI: [QUEUE] SynchronizeTaskListInManagedModeMessage failed on project 5c21bf1b-c910-e511-80e5-005056b44e34. Exception: System.NullReferenceException: Object reference not set to an instance of an object.     at Microsoft.Office.Project.Server.BusinessLayer.ProjectModeManaged.UpdateAssignedToField(SPWeb workspaceWeb, DataSet taskDS, Guid taskUID, SPListItem listItem)     at Microsoft.Office.Project.Server.BusinessLayer.ProjectModeManaged.SynchronizeTask(SPList list, DataSet taskDS, Dictionary`2 taskMapping, DataRow row, DataView secondaryView, Dictionary`2 redoEntries)     at Microsoft.Office.Project.Server.BusinessLayer.ProjectModeManaged.<>c__DisplayClass1.<SynchronizeTaskListI…
…nManagedMode>b__0(SPWeb workspaceWeb)     at Microsoft.Office.Project.Server.BusinessLayer.Project.<>c__DisplayClass3d.<TryRunActionWithProjectWorkspaceWebInternal>b__3c()     at Microsoft.SharePoint.SPSecurity.<>c__DisplayClass5.<RunWithElevatedPrivileges>b__3()     at Microsoft.SharePoint.Utilities.SecurityContext.RunAsProcess(CodeToRunElevated secureCode)     at Microsoft.SharePoint.SPSecurity.RunWithElevatedPrivileges(WaitCallback secureCode, Object param)     at Microsoft.SharePoint.SPSecurity.RunWithElevatedPrivileges(CodeToRunElevated secureCode)     at Microsoft.Office.Project.Server.BusinessLayer.Project.TryRunActionWithProjectWorkspaceWebInternal(IPlatformContext context, Guid projectUid, Action`1 method, Boolean noThrow, DataRow row)     at Microsoft.Office.Project.Server.Busine…
…ssLayer.ProjectModeManaged.SynchronizeTaskListInManagedMode(Guid projectUid)     at Microsoft.Office.Project.Server.BusinessLayer.Queue.ProcessPublishMessage.ProcessSynchronizeTaskListInManagedModeMessage(Message msg, Group messageGroup, JobTicket jobTicket, MessageContext mContext), LogLevelManager Warning-ulsID:0x000CE687 has no entities explicitly specified.

So we have a NullReferenceException in the UpdateAssignedToField method of the Microsoft.Office.Project.Server.BusinessLayer.ProjectModeManaged class (Microsoft.Office.Project.Server assembly).

From the job message type “ManagedModeTaskSynchronization.SynchronizeTaskListInManagedModeMessage” it was obvious, that we have an issue with the synchronization between the project tasks and the Tasks list of the Project Web Site (PWS) of the project having the ID 5c21bf1b-c910-e511-80e5-005056b44e34”,  and from the method name “UpdateAssignedToField” we could assume, that the problem is caused either by an existing value of the “Assigned To” field, or by constructing a new value we want to update the field with.

We can use the following script to find out, which PWS belongs to the project ID above:

$pwa = Get-SPWeb http://YourProjectServer/PWA
$pwa.Webs | ? { $_.AllProperties[‘MSPWAPROJUID’] -eq ‘5c21bf1b-c910-e511-80e5-005056b44e34’ }

If we have a look at the code of the UpdateAssignedToField method, we see it begins with these lines. These lines are responsible for removing users from the “Assigned To” field (of type SPFieldUserValueCollection) that are no longer responsible for the task. The second part of method (not included below) is responsible for inserting new user entries. I highlighted the line that may cause (and in our case in fact has caused) an error if the value of the assignedTo[i].User expression is null.

bool isModified = false;
SPFieldUserValueCollection assignedTo = listItem["AssignedTo"] as SPFieldUserValueCollection;
DataRowView[] source = taskDS.Tables[1].DefaultView.FindRows(taskUID);
if (assignedTo != null)
{
    for (int i = assignedTo.Count – 1; i >= 0; i–)
    {
        string userName = ClaimsHelper.ConvertAccountFormat(assignedTo[i].User.LoginName);
        if (!source.Any<DataRowView>(resourceRow => (string.Compare(userName, resourceRow.Row.Field<string>("WRES_CLAIMS_ACCOUNT"), StringComparison.OrdinalIgnoreCase) == 0)))
        {
            assignedTo.RemoveAt(i);
            isModified = true;
        }
    }
}

The expression may be null if the user it refers to was deleted from the site. Note, that the expression assignedTo[i].LookupId even in this case returns the ID of the deleted user, and the expression assignedTo[i].LookupValue return its name.

How to detect which projects and which users are affected by the issue? I wrote the script below to display the possible errors:

  1. $rootWeb = Get-SPWeb http://YourProjectServer/PWA
  2.  
  3. $rootWeb.Webs | % {
  4.  
  5.     $web = $_
  6.  
  7.  
  8.     Write-Host ——————————-
  9.     Write-Host $web.Title
  10.  
  11.  
  12.     $foundMissingUsers = New-Object 'Collections.Generic.Dictionary[int,string]'
  13.  
  14.     $list = $web.Lists["Tasks"]
  15.  
  16.     if ($list -ne $null)
  17.     {
  18.         $list.Items | % {
  19.             $_["AssignedTo"] | ? {
  20.                  ($_.User -eq $null) -and (-not $foundMissingUsers.ContainsKey($_.LookupId)) } | % {
  21.                      if ($_ -ne $null ) { $foundMissingUsers.Add($_.LookupId, $_.LookupValue) }
  22.                  }
  23.         }
  24.  
  25.         $foundMissingUsers | % { $_ }
  26.     }
  27. }

Assuming

$allUserIds = $rootWeb.SiteUsers | % { $_.ID }

we could use

$allUserIds -NotContains $_.LookupId

instead of the condition

$_.User -eq $null

in the script above.

Indeed, we could identify two users on two separate projects, that were deleted by mistake, although they have assignments in the project Tasks lists.

We have recreated the users (and assigned the new users to the corresponding enterprise resources), but they have now another IDs. What can we do to fix the problem? The synchronization does not work anymore on these projects (making the project publishing impossible as well) so it does not provide a solution. We could replace the users in the “Assigned To” field, or simply remove the wrong one (it would be re-inserted by the second part of the UpdateAssignedToField method during the next synchronization), but there is an event receiver (Microsoft.Office.Project.PWA.ManagedModeListItemEventHandler) registered on this list, that cancels any changes in the list items when you want to persist the changes via the Update method. To avoid that, we could temporary disable the event firing, as described here.

We used the following script to fix the errors.

  1. $rootWeb = Get-SPWeb http://YourProjectServer/PWA
  2. $siteUsers = $rootWeb.SiteUsers
  3.  
  4.  
  5. # disable event firing to prevent cancelling updates by PreventEdits method (Microsoft.Office.Project.PWA.ManagedModeListItemEventHandler)
  6. # http://sharepoint.stackexchange.com/questions/37614/disableeventfiring-using-powershell
  7. $receiver = New-Object "Microsoft.SharePoint.SPEventReceiverBase"
  8. $type = $receiver.GetType()
  9. [System.Reflection.BindingFlags]$flags = [System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::NonPublic
  10. $method = $type.GetMethod("DisableEventFiring", $flags)
  11. $method.Invoke($receiver, $null)
  12.  
  13.  
  14. $rootWeb.Webs | ? { $_.Title -eq 'YourProjectName' } | % {
  15.  
  16. $web = $_
  17.  
  18. Write-Host ——————————-
  19. Write-Host $web.Title
  20.  
  21. $userPairs = ((122, 3421), (145, 2701))
  22.  
  23. $userPairsResolved = $userPairs | Select-Object -Property `
  24.   @{ Name="OldUserId"; Expression = { $_[0] }},
  25.   @{ Name="NewUser"; Expression = { $up = $_; $siteUsers | ? { $_.ID -eq $up[1] } }}
  26.  
  27. $list = $web.Lists["Tasks"]
  28.  
  29. if ($list -ne $null)
  30. {
  31.     $list.Items | % { $list.Items | % {
  32.         $item = $_
  33.         [Microsoft.SharePoint.SPFieldUserValueCollection]$assignedTo = $item["AssignedTo"]
  34.         if ($assignedTo -ne $null)
  35.         {
  36.             $isModified = $false
  37.  
  38.             # iterate through the assignments
  39.             for($i = 0; $i -lt $assignedTo.Count; $i++)
  40.             {
  41.                 if ($assignedTo[$i].User -eq $null)
  42.                 {
  43.                     $userName = $assignedTo[$i].LookupValue
  44.                     $userid = $assignedTo[$i].LookupId
  45.                     $taskTitle = $item.Title.Trim()
  46.                     Write-Host Task """$taskTitle""" assigned user """$userName""" "($userId)" missing
  47.                     $newUser = $userPairsResolved | ? { $_.OldUserId -eq $userid } | % { $_.NewUser }
  48.                     if ($newUser -ne $null)
  49.                     {
  50.                         $newUserId = $newUser.Id
  51.                         $newUserName = $newUser.Name
  52.                         do { $replaceAssignedTo = Read-Host Would you like to replace the assignment of the missing user with """$newUserName""" "($newUserId)"? "(y/n)" }
  53.                         until ("y","n" -contains $replaceAssignedTo )
  54.  
  55.                         if ($replaceAssignedTo -eq "y")
  56.                         {
  57.                             # step 1: removing the orphaned entry
  58.                             $assignedTo.RemoveAt($i)
  59.  
  60.                             # step 2: create the replacement
  61.                             [Microsoft.SharePoint.SPFieldUserValue]$newUserFieldValue = New-Object Microsoft.SharePoint.SPFieldUserValue($web, $newUser.Id, $newUser.Name)     
  62.                             $assignedTo.Add($newUserFieldValue)
  63.  
  64.                             # set the 'modified' flag
  65.                             $isModified = $true
  66.                         }
  67.                     }
  68.                     else
  69.                     {
  70.                         Write-Host WARNING No user found to replace the missing user with -ForegroundColor Yellow
  71.                     }
  72.                       }
  73.             }
  74.  
  75.             # update only if it has been changed
  76.             if ($isModified)
  77.             {
  78.             $item["AssignedTo"] = $assignedTo
  79.             $item.Update()
  80.             Write-Host Task updated
  81.             }
  82.         }
  83.     }}
  84. }
  85.  
  86. }
  87.  
  88. # re-enabling event fireing
  89. $method = $type.GetMethod("EnableEventFiring", $flags)
  90. $method.Invoke($receiver, $null)

The variable $userPairs contains the array of old user IDnew user ID mappings. In step 1 we remove the orphaned user entry (the one referring the deleted user), in step 2 we add the entry for the recreated user. If you plan to run the synchronization (for example, by publishing the project) after the script, step 2 is not necessary, as the synchronization process inserts the references for the users missing from the value collection.

Note 1: The script runs only on the selected project (in this case “YourProjectName”), to minimize the chance to change another project unintentionally.

Note 2: The script informs a user about the changes it would perform, like to replace a reference to a missing user to another one, and waits a confirmation (pressing the ‘y’ key) for the action on behalf on the user executes the script. If you have a lot of entries to change, and you are sure to replace the right entries, you can remove this confirmation and make the script to finish faster.


How a Missing Permission for a Single User May Crash a Whole SharePoint Custom Web Application

$
0
0

As part of our daily jobs we provide support for several custom-developed SharePoint-based web applications, like purchase order workflows, etc. either. Few of them were developed for MOSS 2007 / SharePoint 2010, and then migrated to SharePoint 2013. The code quality reflects often not the best programming practices as well, to tell the through.

The symptoms

A weird error has arisen in one of that applications from a such coding anti-pattern caused us some headache recently.

Every once in a while the users complained, that the custom pages they use otherwise (for example, a day earlier) throw an exception. Restarting the IIS application pool for the SharePoint web application made the pages to function again, however we considered this as a simple quick-and-dirty workaround until we find out the real reason behind the issue.

In the ULS logs we found the following entries:

Application error when access /_layouts/CustomPages/YourPage.aspx, Error=List ‘Config’ does not exist at site with URL ‘http://YourSharePoint’.   
Microsoft.SharePoint.Client.ResourceNotFoundException: List ‘Config’ does not exist at site with URL ‘
http://YourSharePoint’.
Getting Error Message for Exception System.Web.HttpUnhandledException (0x80004005): Exception of type ‘System.Web.HttpUnhandledException’ was thrown. —> System.TypeInitializationException: The type initializer for ‘Company.Custom.Config’ threw an exception. —> System.ArgumentException: List ‘Config’ does not exist at site with URL ‘
http://YourSharePoint’. —> Microsoft.SharePoint.Client.ResourceNotFoundException: List ‘Config’ does not exist at site with URL ‘http://YourSharePoint’.     — End of inner exception stack trace —     at Microsoft.SharePoint.SPListCollection.GetListByName(String strListName, Boolean bThrowException)     at Company.Custom.Config..cctor()     — End of inner exception stack trace —     at …
…Company.Custom.Config.get_ConfigValue()     at Company.Custom.Pages.Layouts.Company.Custom.YourPage.Page_Load(Object sender, EventArgs e)     at Microsoft.SharePoint.WebControls.UnsecuredLayoutsPageBase.OnLoad(EventArgs e)     at Microsoft.SharePoint.WebControls.LayoutsPageBase.OnLoad(EventArgs e)     at System.Web.UI.Control.LoadRecursive()     at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)     at System.Web.UI.Page.HandleError(Exception e)     at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)     at System.Web.UI.Page.ProcessRequest(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)     at System.Web.UI.Page.ProcessReques…    …t()     at System.Web.UI.Page.ProcessRequest(HttpContext context)     at System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()     at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

Based on the logs the users have obviously problem with accessing the custom list called “Config”. At first we thought the list was deleted accidentally, or the users do not have permission on it, but after a quick check it turned out that the list is there, and the users have no problem accessing it via the web UI.

This list is used by the web application to persist specific application-wide settings as name-value pairs. A custom static class called “Config” is responsible to read up the configuration values from the SharePoint list into a static Dictionary field, and provide them to the other components of the application as static properties of the class. In the original implementation of the class the configuration values were read up from the list in the static constructor and that is without elevated permission, since (at least theoretically) all users should have at least read permissions to the list.

What’s wrong with this approach? Why can’t users that definitely do have permission to the list access it?

If there is at least a single user without permission to the list, and this user happens to be the first one that tries to access the Config object after the application pool of the web application (re)started or recycled by IIS, the static constructor must throw obviously a TypeInitializationException exception, as there is an unhandled exception in the static constructor. The real exception, that caused the problem is included in the InnerException of the TypeInitializationException exception. But why do the other users (having permissions to the list) become the same error?

The answer can be found on MSDN:

”If a static constructor throws an exception, the runtime will not invoke it a second time, and the type will remain uninitialized for the lifetime of the application domain in which your program is running.”

In our case the application domain means the process of the IIS application pool. Although it is not documented in the article mentioned above, but based on our experience on each further access on any static methods / properties the very same exception would be thrown as the first time.

It is not something SharePoint-specific, one can have the same issue with broken SQL Server connection as well.

To illustrate the behavior I extended the code sample I found here, provided by Jon Skeet, author of one of my favorite books, C# in Depth:

  1. using System;
  2. using System.Threading;
  3.  
  4. public static class Config
  5. {
  6.     static Config()
  7.     {
  8.         DateTime now = DateTime.Now;
  9.  
  10.         Console.WriteLine("Static constructor invoked at {0:s}", now);
  11.         throw new ApplicationException(string.Format("List not found, time stamp: {0:s}", now));
  12.     }
  13.  
  14.     public static string Value
  15.     {
  16.         get { return "a value"; }
  17.     }
  18. }
  19.  
  20. class StaticConfigTest
  21. {
  22.     static void Main()
  23.     {
  24.         for (int i = 0; i < 5; i++)
  25.         {
  26.             DateTime now = DateTime.Now;
  27.             Console.WriteLine("Config value read at {0:s}", now);
  28.  
  29.             try
  30.             {
  31.                 var value = Config.Value;
  32.             }
  33.             catch (Exception e)
  34.             {
  35.                 var text = string.Format("'{0}' ({1})", e.Message, e.GetType().Name);
  36.                 var ie = e.InnerException;
  37.                 if (ie != null)
  38.                 {
  39.                     text += string.Format(" '{0}' ({1})", ie.Message, ie.GetType().Name);
  40.                 }
  41.                 Console.WriteLine(text);
  42.             }
  43.  
  44.             // wait 5 secs
  45.             Thread.Sleep(5000);
  46.         }
  47.     }
  48. }

When you execute this code, it will output something like this:

image

The main points you should notice:

  • There is only a single line of “Static constructor invoked”. It means, the static constructor is only invoked once.
  • The main exception is always a TypeInitializationException, the “real” exception (in our case, it is an ApplicationException) is provided in the InnerException property. Based on the ULS logs above, the InnerException is logged out by SharePoint (that is a good thing), and it caused us a bit of confusion (that is not so good, of course).
  • The exception thrown by accessing the static members is always the same as the first one, compare the time stamps values in the output.

How to solve the problem once we know the real reason of the issue?

As we wanted to remedy the issue as fast as possible, the very first step was to resolve the direct cause of the problem, so we granted permissions on the Config list for the few users who did not have access earlier.

For a long-term solution, I think the most important step was to refactor the Config class. The code of the static constructor was transformed to a private static method (let’s call it InitializeIfNeeded). In this method we lock the Dictionary object used to store the configuration values, to provide a kind of  thread safety and support potential concurrent calls. We check next, if the configuration values were already initialized (via a static boolean field called isConfigInitialized). If they were, we exit from the InitializeIfNeeded method. If they were not, we read up the values from the SharePoint list into the Dictionary object, and set the value of the isConfigInitialized field to true. On accessing each of the static properties of the Config class representing the individual configuration values, we invoke first the InitializeIfNeeded method, to ensure the values are available. Using this simple step we can ensure, that users that do have permission on the SharePoint list can accesd the configuration values. Users without permission may have still the problem that the values cannot be read from the list, if they are the first ones to visit the pages. If they are visiting the pages after the values were already initialized (via a visit of a user with permission), they can of course access the configuration values as well.

As next step, in the InitializeIfNeeded method we embedded the code responsible for reading up the values from the SharePoint list into the Dictionary object into an elevated permission block to ensure all user (even the ones without direct permission to the list) can read the values up.

After testing the new version and deploying it into the production system, we can revoke the direct permissions from the Config list for the standard users, and let only administrators to read and change configuration value via the web UI.


How to Invoke PSI Methods using PSContext, Demonstrated by an Example of Reading Delegation Information

$
0
0

The Challange

So you may ask now, why would we like to access PSI functionality from PSContext at all? Let me explain our situation.

Recently we had to extend an existing server side code in our custom Project Server solution to enable delegate users (that means ones acting for another users at the given time) to access the same custom functionality as the user they acting for.

Note: You can manage delegation via PWA Settings / Manage Delegates (either in the Personal Setting or in the Security section), see the page http://YourProjectServer/PWA/_layouts/15/pwa/userdelegation/ManageDelegations.aspx?mgm=true.

To make the things event worse, we had to check the delegation information in code running in the SharePoint web context, and in code that runs as a simple server side process without web context. It is actually not a timer job, but an automated server process scheduled by Windows Task Scheduler for the sake of simplicity, that perform some kind of housekeeping and reporting tasks for the custom application.However, in the case of a time job, an asynchronous event receiver, or a workflow step running without web context, you would have the same issue. Why these two cases (with or without SharePoint web context) differ from each other, will be discussed later in more details.

You probably know, that although the server side object model of the Project Server (available via the Microsoft.ProjectServer.PSContext) covers quite a lot of the functionality of the PSI, there is still a plenty of features that are simply inaccessible via the objects available through PSContext. For example, there is no way to access the delegation information. That means for us, that we should read this information via PSI, as its Resource class provides a ReadDelegations method just for this purpose. If you are not new in Project Server development, you should have already your own experience with PSI. If you have not any experience with that yet, I can say you, that it is rather complex comparing to the server side OM provided by PSContext. You should work with various DataSet and DataTable objects, but the greatest problem in our case that it requires a lot of configuration, either by config files (see the Configuring the Services with app.config section of the Walkthrough: Developing PSI Applications Using WCF article for example) , or via code (see section 11 of  the Walkthrough: Developing PSI Applications Using WCF article, or the Configuring the Services Programmatically section of the Walkthrough: Developing PSI Applications Using WCF article for example), just to be able to start using the PSI web services. If you have more applications (in our case one with, and another one without SharePoint web context), multiple environments (development, test, and production, one with HTTP, another one with HTTPS), and multiple servers in the environments, creating and maintaining the configuration information is typically not a trivial task.

Why could not we simply access the PSI endpoints via the PSContext? It would be great, wouldn’t it? Are you surprised if I say, it is possible? Well, actually it is. It requires an amount of hacking, and probably not a supported solution, but it is technically possible, and it made our life easier. If you take the risk on your own, you can try it as well, however there is no guarantee, that it works for you, or that once it works, a new Project Server patch could not break the functionality. So be warned.

After this introduction you should have already an overview of the problem and hopefully you are ready to read about the technical stuff. If you are not interested in such details, and need only the result of the analysis, you can skip the next two sections.

Connection between the Project Server client side object model and the server side object model

If one dig into the assemblies implementing the server side object model (Microsoft.Project.dll), and the client side object model (Microsoft.Project.Client.dll) of Project Server, then it turns out, that Microsoft has not re-implemented the functionality of PSI, these libraries still utilize the good-old PSI infrastructure either directly (in case of the server side OM) or indirectly (in case of the client side OM, that calls PSI via the server side OM).

How the client side OM invokes the server side OM is not Project Server specific, the infrastructure is inherited from SharePoint. Although there were changes between the SharePoint versions 2010 and 2013, the main concepts remain the same. Both the server side and client side of that bridge were deeply researched and analyzed in my posts five years ago. Those posts should give you enough insight into the internal functionality of the client side OM, this topic is outside of scope of the current post.

It is more exciting (at least, for now), how the Project Server server side OM invokes the PSI infrastructure. Exactly that will I describe in the next section.

Connection between the Project Server server side object model and PSI

The Microsoft.Office.Project.PWA namespace contains a public class called PSI (implemented in assembly Microsoft.Office.Project.Server.PWA). This class exposes more than 20 public properties with postfix “WebService”, each of them returns the WCF proxy of the corresponding PSI channel. For example, the ResourceWebService property returns a reference for a Resource object (namespace: Microsoft.Office.Project.Server.WebServiceProxy, assembly: Microsoft.Office.Project.Server.PWA), inherited from WcfProxyBase<IResource>. Once created in the “WebService” property getters, these WCF proxy objects are stored in the private field _proxyContainer (of  type ProxyContainer, implemented as a nested class) of the PSI class for any further access.

For the purpose of our solution, it is irrelevant how the WCF proxy objects (like the Resource object returned by the ResourceWebService property of the PSI class) know, what configuration they should use and how they can access the WCF endpoint. Probably I write about sometimes later in an other post.

The internal PJClientCallableContext class (Microsoft.ProjectServer namespace, Microsoft.ProjectServer assembly) contains a static read-only property called PJContext. It returns an instance of the PJContext class (Microsoft.Office.Project.PWA namespace, Microsoft.Office.Project.Server.PWA assembly) using the generic private static RetrieveValue method. If the process runs in the HTTP context (HttpContext.Current is not null) the RetrieveValue method invokes the GetContext(bool isWebServiceCall, bool ignoreCachedContext) method of the PJContext class. Otherwise (without HTTP context), the lazy-initialized value of the _pjcontext field will be returned by RetrieveValue method. The lazy-initialization of this field can be found in the Initialize method of the PJClientCallableContext class, the static GetObjectModelContext method of the PJContext class is invoked using the SPWeb object instance passed to the Initialize method as parameter. This is the same SPWeb object, that you passed to the constructor of the PSContext object (or it is the RootWeb of the site if you used the PSContext constructor with the SPSite object; or the RootWeb of the site corresponding to the URL if you used the PSContext constructor with the Uri object), as in the PSContext(SPWeb web) constructor an instance of the PJClientCallableContext class is created using the web instance as parameter.

Side note: On the other hand, using the PSContext constructors with parameters in a HTTP context may result you do no get the expected outcome, but it is beyond our scope again, more on that in a later post.

After this theoretical explanation let’s see some functional code.

The Code

My goal was to access the static PJContext property of the internal PJClientCallableContext class via Reflection, and expose its PSI property to my code as an extension method.

The solution described below requires adding the following assembly references:

  • Microsoft.Project.dll
  • Microsoft.Office.Project.Server.Library.dll
  • Microsoft.Office.Project.Server.PWA.dll
  • Microsoft.Office.Project.Schema.dll
  • Microsoft.Office.Project.Server.Administration.dll

The extension method is rather simple, and requires this using directive to work:

using Microsoft.Office.Project.PWA;

  1. public static PSI GetPSI(this PSContext dummyContext)
  2. {
  3.     PSI result = null;
  4.     Assembly psAssembly = typeof(PSContext).Assembly;
  5.  
  6.     // get internal type
  7.     Type type_PJClientCallableContext = psAssembly.GetType("Microsoft.ProjectServer.PJClientCallableContext");
  8.     PropertyInfo pi_PJContext = type_PJClientCallableContext.GetProperty("PJContext");
  9.     PJContext pjContext = pi_PJContext.GetValue(null) as PJContext;
  10.  
  11.     if (pjContext != null)
  12.     {
  13.         result = pjContext.PSI;
  14.     }
  15.  
  16.     return result;
  17. }

Note: As you can see, in the code above we don’t use the PSContext object passed as parameter to the extension method at all. It is only to enable attaching the functionality to a PSContext instance via the extension method, and so enforcing the creation of a PSContext instance first. If you have an application without HTTP context (like a console application), and you call this code without creating a PSContext instance first, like:

PSI psi = Extensions.GetPSI(null);

an InvalidOperationException will be thrown by the RetrieveValue method of the PJClientCallableContext class:

You must wrap your server OM code in a PJClientCallableContext when outside of an HttpContext.

In the case of a web application you don’t have such problem, as the PJContext instance is created using the current HTTP context.

Below I illustrate the usage of the extension method, by querying the delegation information of an enterprise resource in Project Server. To achieve that, I invoke the ReadDelegations method of the Resource class. See the available values of the DelegationFilter enumeration in the documentation.

The code requires the following using directives:

using Microsoft.Project;
using Microsoft.Office.Project.PWA;
using schema = Microsoft.Office.Project.Server.Schema;
using lib = Microsoft.Office.Project.Server.Library;

  1. using (PSContext projectContext = PSContext.GetContext(new Uri(pwaUrl)))
  2. {
  3.     PSI psi = projectContext.GetPSI();
  4.                     
  5.     lib.UserDelegationConsts.DelegationFilter filter = lib.UserDelegationConsts.DelegationFilter.All;
  6.  
  7.     // replace the GUID with an ID of a resource in your environment
  8.     // whose delegation information you would like to display
  9.     Guid resId = Guid.Parse("087ade95-281e-e411-9568-005056b45654");
  10.  
  11.     using (schema.UserDelegationDataSet delegationDS = psi.ResourceWebService.ReadDelegations(filter, resId))
  12.     {
  13.         foreach (schema.UserDelegationDataSet.ResourceDelegationsRow resDelegation in delegationDS.ResourceDelegations)
  14.         {
  15.             Console.WriteLine("Resource is substituted by '{0}' from '{1}' to '{2}'", resDelegation.DELEGATE_NAME, resDelegation.DELEGATION_START, resDelegation.DELEGATION_FINISH);
  16.         }
  17.     }
  18. }

Using the approach above you don’t have to bother with the configuration of the PSI proxy you need in your application, and still access such features in your server side code, that are available in PSI.

Note: There is a major restriction using this method. Although the PSI web services are available from a client computer, or from a remote server as well, you can not access remote servers using the code above. You can access only the PSI resources exposed by the local farm.



Security Issue with Connection Manager in SQL Server Integration Services Solutions

$
0
0

Today I had to import some data from a password protected Access file into an MS SQL database using SSIS. I am working with SQL Server Data Tools in Visual Studio 2013. Might be that the issue I am writing about cannot be reproduced by the Visual Studio 2015 version of the tool.

I’ve started to create a new connection via Connection Manager. As I typed in the password – as one expect it – black dots were displayed instead of the real characters of the password.

image

It is the same if you switch from the Connection page to the All page of the dialog, where you can see and change all properties of your connection.

image

However, if you click the OK button to persist the changes in the connection, the password are revealed under the Data connection properties:

image

I think it should be a bug in the product.

BTW, after you click the OK button in this dialog as well, the password is displayed again masked (this time by asterisk characters instead of the black dots) in the property grid of the connection:

image


Setting the Display Order of the Enterprise Custom Fields on the Project Details Page of Project Server

$
0
0

The out-of-the-box version of the Project Details page of Project Server contains two web parts. On the top, there is a web part with the title “Basic Info” (implemented in class Microsoft.Office.Project.PWA.WebParts.ProjectFieldPart, assembly Microsoft.Office.Project.Server.PWA) that displays the core information about the project, like its name and description as well as start and finish date and the name of the project owner.

There is another web part at the bottom of the page. It is the “Details” web part (implemented in class Microsoft.Office.Project.PWA.WebParts.ProjectCustomFieldsPart, assembly Microsoft.Office.Project.Server.PWA), and it displays the enterprise custom fields defined for the Project entity type.

In the “Basic Info” web part you can select, which project fields should be displayed, as well as their order via the web part properties, as displayed on the screenshot below:

image

image

However you don’t have such control on the fields displayed by the “Details” web part, it simply displays all of the project-related enterprise custom fields.

In this post I show you how to control the display order of the fields in the “Details” web part via jQuery, illustrating the possibilities by using two real-world problem as example.

Problem 1

Assume you have defined (among others) the following custom fields for the Project entity:

  • AField
  • AnotherField
  • CompanyComment
  • CompanyName
  • DivisionComment
  • DivisionName
  • JustOneMoreField
  • LastField

What can you do, if you have a business requirement that dictates the following order for these fields?

  • AField
  • AnotherField
  • DivisionName
  • DivisionComment
  • CompanyName
  • CompanyComment
  • JustOneMoreField
  • LastField
  • The fields highlighted with bold should be in the specified order. The other enterprise fields (including the ones created later) will be displayed before or after the highlighted block and sorted alphabetically.

    You have the option to remove the “Details” web part from the page, and include all of the fields you need in the “Basic Info” web part in the required order. This solution seems to be simple at first sight, but it has the drawback, that you lose the original page structure with the two web parts, and what even worse, you should add any new enterprise custom field to the “Basic Info” web part manually in the future.

    Alternatively, you can use jQuery to achieve the required results within the “Details” web part.

    The changeFieldOrder function below demonstrates how to accomplish this goal. This function invokes the getTRForField function to find the table row for the specified field. It looks up then the row that belongs to the field specified in the first member of the string array (fieldOrder) it receives as parameter, then looks up the other rows one after the other as well, and places them after the previous one.

    Optionally, you can define further field order sets and call the changeFieldOrder function repeatedly using these field sets as parameter.

    1. $(document).ready(startScript);
    2.  
    3. function getTRForField(parent, fieldName) {
    4.     var result = parent.find("h3.ms-accentText").filter(function () { return $(this).text() === fieldName; }).closest("tr").first();
    5.     return result;
    6. }
    7.  
    8. function changeFieldOrder(fieldOrder) {
    9.     var wp = $("div[name='WPClass:ProjectFieldPart']").last();
    10.     var tbody = wp.find("tbody").first();
    11.     for (i = 1; i < fieldOrder.length; i++) {
    12.         var firstTR = getTRForField(tbody, fieldOrder[i – 1]);
    13.         var secondTR = getTRForField(tbody, fieldOrder[i]);
    14.         firstTR.after(secondTR);
    15.     }
    16. }
    17.  
    18. function startScript() {
    19.  
    20.     var fieldOrder1 = ["DivisionName", "DivisionComment", "CompanyName", "CompanyComment"];
    21.     // you can define further field orders if you need
    22.     // var fieldOrder2 = ["CustomField2", "CustomField1"];
    23.  
    24.     changeFieldOrder(fieldOrder1);
    25.     // changeFieldOrder(fieldOrder2);
    26.  
    27. }

    Assuming this script is saved in the file CustomFieldOrder.js in the Site Assets library of the PWA site, and the jquery-1.9.1.min.js can be found in that library as well, you can add a Script Editor web part to the Project Details page, and include these two lines in the web part to inject the field-sorting functionality into the page:

    /PWA/SiteAssets/jquery-1.9.1.min.js
    /PWA/SiteAssets/CustomFieldOrder.js

    Problem 2

    Assume you would like to display the fields in this order:

    • DivisionName
    • DivisionComment
    CompanyName
  • CompanyComment
  • AField
  • AnotherField
  • JustOneMoreField
  • LastField
  • The fields highlighted with bold should be at the very top of the field list and in the specified order. The other enterprise fields (including the ones created later) will be displayed after the highlighted block and sorted alphabetically.

    Again, you have the option to remove the “Details” web part from the page, and include all of the fields you need in the “Basic Info” web part in the required order, but in this case you have the same drawbacks, as in the first case above.

    Instead of this, you can use the setFieldOrder function that invokes the same getTRForField function we saw in the example above. Then it looks up the row that belongs to the field specified in the first member of the string array (fieldOrder) it receives as parameter, put it at the first row of the table, then looks up the other rows one after the other as well, and places them after the previous one.

    1. function getTRForField(parent, fieldName) {
    2.     var result = parent.find("h3.ms-accentText").filter(function () { return $(this).text() === fieldName; }).closest("tr").first();
    3.     return result;
    4. }
    5.  
    6. function setFieldOrder(fieldOrder) {
    7.     var wp = $("div[name='WPClass:ProjectFieldPart']").last();
    8.     var tbody = wp.find("tbody").first();
    9.     var trLast;
    10.     for (i = 0; i < fieldOrder.length; i++) {
    11.         var tr = getTRForField(tbody, fieldOrder[i]);
    12.         if (!trLast) {
    13.             tr.prependTo(tbody);
    14.             trLast = tr;
    15.         }
    16.         else {
    17.             trLast.after(tr);
    18.             trLast = tr;
    19.         }
    20.     }
    21. }
    22.  
    23. function startScript() {
    24.     var fieldOrder = ["DivisionName", "DivisionComment", "CompanyName", "CompanyComment"];
    25.     setFieldOrder(fieldOrder);
    26. }

    You can combine the usage of the changeFieldOrder and setFieldOrder functions if you wish.

    In the next blog post I plan to stay with the same topic, how the enterprise custom fields get displayed in the “Details” web part, but instead of setting display order, I show you how to hide / display them based on conditions, like the group membership of the current user.


Displaying Enterprise Custom Fields Conditionally on the Project Details Page of Project Server

$
0
0

In my recent blog entry I’ve illustrated how to change the out-of –the-box display order of the enterprise custom fields in the “Details” web part on the Project Details page of Project Server. In this post I go one step further and show, how to display / hide the fields based on conditions. As in the previous case, I use jQuery in this case either to achieve the goal.

For the sake of example, let’s assume you have a few custom fields (in this case “Field1” and “Field2”) that should be displayed only for the members of a special SharePoint group called “Admins”.

The script we need in this case is displayed in the code snippet below:

  1. var adminGroup = "Admins";
  2.  
  3. function isCurrentUserMemberOfGroup(groupName, OnComplete) {
  4.  
  5.     var clientContext = new SP.ClientContext.get_current();
  6.     var currentUser = clientContext.get_web().get_currentUser();
  7.  
  8.     var userGroups = currentUser.get_groups();
  9.     clientContext.load(userGroups);
  10.  
  11.     clientContext.executeQueryAsync(OnSuccess, OnFailure);
  12.  
  13.     function OnSuccess(sender, args) {
  14.         var isMember = false;
  15.         var groupsEnumerator = userGroups.getEnumerator();
  16.         while (groupsEnumerator.moveNext()) {
  17.             var group = groupsEnumerator.get_current();
  18.             if (group.get_title() == groupName) {
  19.                 isMember = true;
  20.                 break;
  21.             }
  22.         }
  23.  
  24.         OnComplete(isMember);
  25.     }
  26.  
  27.     function OnFailure(sender, args) {
  28.         OnComplete(false);
  29.     }
  30. }
  31.  
  32. $(document).ready(startScript);
  33.  
  34. function setFieldVisibility(fields, visible) {
  35.     // hide status / twitter related fields
  36.     $(".ms-accentText").each(function (index) {
  37.         if ($.inArray($(this).text(), fields) != -1) {
  38.             $(this).closest('tr').toggle(visible);
  39.         }
  40.     });
  41. }
  42.  
  43. function startScript() {
  44.     var fieldsToHide = ["Field1", "Field2"];
  45.  
  46.     // hide the fields at page startup
  47.     setFieldVisibility(fieldsToHide, false);
  48.     isCurrentUserMemberOfGroup(adminGroup, function (isCurrentUserInGroup) {
  49.         console.log("Current user is member of group '" + adminGroup + "': " + isCurrentUserInGroup);
  50.         setFieldVisibility(fieldsToHide, isCurrentUserInGroup);
  51.     });
  52. }

We use the setFieldVisibility function to hide / display the fields. On the page load we hide the fields in the first step. To verify if the current user belongs to the group and to perform a custom action after the check I use the isCurrentUserMemberOfGroup function borrowed from Stack Overflow. Finally, we set the visibility of the field in the callback function based on the group membership of the current user.

Assuming this script is saved in the file DisplayFieldsForGroup.js in the Site Assets library of the PWA site, and the jquery-1.9.1.min.js can be found in that library as well, you can add a Script Editor web part to the Project Details page, and include these two lines in the web part to inject the functionality into the page:

/PWA/SiteAssets/jquery-1.9.1.min.js
/PWA/SiteAssets/emDisplayFieldsForGroup/em.js


Creating Project Server Enterprise Resources via REST

$
0
0

Recently I’ve got a question on this blog about how to create Project Server enterprise resources using the REST interface. Although I consider this task to be a rather common requirement, there are really very few (if any) practical information about that on the web.

In this post I show you how to perform this operation using C# and JavaScript as well.

In the C# example I assume you have the following using statement in your code:

using System.Net;

Furthermore, it is assumed you have a string field called baseUrl in your class that contains the URL of your PWA site.

private readonly string baseUrl = "http://YourProjectServer/PWA&quot;;

I’m using some helper methods, a few of them are borrowed from the code of this SharePoint StackExchange forum thread.

First we need to get a form digest value we will use in our later POST requests. This task is performed by the GetFormDigestValue method.

  1. private string GetFormDigestValue()
  2. {
  3.     string digest = null;
  4.  
  5.     HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(this.baseUrl + "/_api/contextinfo");
  6.     request.Method = "POST";
  7.     request.ContentType = "application/json;odata=verbose";
  8.     request.UseDefaultCredentials = true;
  9.     var postBody = string.Empty;
  10.     request.ContentLength = postBody.Length;
  11.  
  12.     byte[] postData = Encoding.ASCII.GetBytes(postBody);
  13.  
  14.     using (Stream requestStream = request.GetRequestStream())
  15.     {
  16.         requestStream.Write(postData, 0, postData.Length);
  17.         requestStream.Close();
  18.  
  19.         HttpWebResponse response = (HttpWebResponse)request.GetResponse();
  20.         using (Stream responseStream = response.GetResponseStream())
  21.         {
  22.             var encoding = ASCIIEncoding.ASCII;
  23.             using (var reader = new StreamReader(response.GetResponseStream(), encoding))
  24.             {
  25.                 // parse the ContextInfo response
  26.                 var resultXml = XDocument.Parse(reader.ReadToEnd());
  27.  
  28.                 // get the form digest value
  29.                 var d = from e in resultXml.Descendants()
  30.                         where e.Name == XName.Get("FormDigestValue", "http://schemas.microsoft.com/ado/2007/08/dataservices&quot;)
  31.                         select e;
  32.                 digest = d.First().Value;
  33.             }
  34.         }
  35.     }
  36.  
  37.     return digest;
  38. }

The POST requests will be prepared by the PreparePostRequest method before execution:

  1. private HttpWebRequest PreparePostRequest(string requestPath, string formDigestValue, string postBody)
  2. {
  3.     HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(this.baseUrl + requestPath);
  4.     request.Method = "POST";
  5.     request.ContentType = "application/json;odata=verbose";
  6.     request.Accept = "application/json;odata=verbose";
  7.     request.Headers.Add("X-RequestDigest", formDigestValue);
  8.     request.UseDefaultCredentials = true;
  9.     request.ContentLength = postBody.Length;
  10.  
  11.     byte[] postData = Encoding.ASCII.GetBytes(postBody);
  12.  
  13.     System.IO.Stream requestStream = request.GetRequestStream();
  14.     requestStream.Write(postData, 0, postData.Length);
  15.     requestStream.Close();
  16.  
  17.     return request;
  18. }

The ExecuteAuthorizedPostRequest gets the form digest and prepares the request using the other two methods, finally sends the request and reads the response returned by the server.

  1. private HttpWebResponse ExecuteAuthorizedPostRequest(string requestPath, string postBody)
  2. {
  3.     string formDigestValue = GetFormDigestValue();
  4.     HttpWebRequest request = PreparePostRequest(requestPath, formDigestValue, postBody);
  5.  
  6.     HttpWebResponse response = (HttpWebResponse)request.GetResponse();
  7.  
  8.     return response;
  9. }

Using the methods above, we can concentrate on the actual “mission”, creating a new enterprise resource. In the code below I’ve included three different versions (the first two of them are commented out).

The first one creates an enterprise resource by setting its Name property only. By default, the new resources are created having EnterpriseResourceType.Work as their ResourceType property.

In the second example we create an enterprise resource by setting its Name property and its ResourceType property to EnterpriseResourceType.Material.

In the third case we create an enterprise resource by setting its Name property and its CostCenter property.

  1. public void CreateEnterpriseResource()
  2. {
  3.     string requestPath = "/_api/ProjectServer/EnterpriseResources";
  4.     // creating a new resource setting only its name
  5.     //string postBody = "{ '__metadata': { 'type': 'PS.EnterpriseResource' }, 'Name':'New Resource' }";
  6.     // creating a new resource setting its name and type ('Material' in this case)
  7.     //string postBody = string.Format("{{ '__metadata': {{ 'type': 'PS.EnterpriseResource' }}, 'Name':'New Material Resource', 'ResourceType': '{0}' }}", (int)EnterpriseResourceType.Material);
  8.     // creating a new resource setting its name and cost center
  9.     string postBody = string.Format("{{ '__metadata': {{ 'type': 'PS.EnterpriseResource' }}, 'Name':'New CC Resource', 'CostCenter': '{0}' }}", "Your Cost Center");
  10.     
  11.     var resp = ExecuteAuthorizedPostRequest(requestPath, postBody);
  12. }

Note, that the type of the object we use in the examples is PS.EnterpriseResource and not PS.EnterpriseResourceCreationInformation one would use to create a new resource via the client object model. Since the EnterpriseResourceCreationInformation object has only a limited set of properties provided by the EnterpriseResource object, we can set a lot more properties on creating the resource, than we could using the client object model. For example, in the last example above we set the cost center of the enterprise resource when creating it. It would not be possible via the EnterpriseResourceCreationInformation object.

If you need to create the resources from JavaScript on a web page instead of C#, it is possible as well.

First, we define a String.format helper function:

  1. String.format = (function () {
  2.     // The string containing the format items (e.g. "{0}")
  3.     // will and always has to be the first argument.
  4.     var theString = arguments[0];
  5.  
  6.     // start with the second argument (i = 1)
  7.     for (var i = 1; i < arguments.length; i++) {
  8.         // "gm" = RegEx options for Global search (more than one instance)
  9.         // and for Multiline search
  10.         var regEx = new RegExp("\\{" + (i – 1) + "\\}", "gm");
  11.         theString = theString.replace(regEx, arguments[i]);
  12.     }
  13.  
  14.     return theString;
  15. });

The JavaScript equivalent (using jQuery) our three examples we saw above in C# the code:

  1. var serverUrl = String.format("{0}//{1}", window.location.protocol, window.location.host);
  2. var pwaServerRelUrl = "/PWA";
  3.  
  4. function startScript() {
  5.     $.ajax({
  6.         url: serverUrl + pwaServerRelUrl + "/_api/ProjectServer/EnterpriseResources",
  7.         type: "POST",
  8.         contentType: "application/json;odata=verbose",
  9.         //data: JSON.stringify(item),
  10.         // creating a new resource setting only its name
  11.         //data: "{ '__metadata': { 'type': 'PS.EnterpriseResource' }, 'Name':'New Resource JS' }",
  12.         // creating a new resource setting its name and type ('Material' in this case)
  13.         //data: "{ '__metadata': { 'type': 'PS.EnterpriseResource' }, 'Name':'New Material Resource JS', 'ResourceType': '2' }",
  14.         // creating a new resource setting its name and cost center
  15.         data: "{ '__metadata': { 'type': 'PS.EnterpriseResource' }, 'Name':'New CC Resource JS', 'CostCenter': 'Your Cost Center' }",
  16.         headers: {
  17.             "Accept": "application/json;odata=verbose",
  18.             "X-RequestDigest": $("#__REQUESTDIGEST").val()
  19.         },
  20.         complete: function (result) {
  21.             var response = JSON.parse(result.responseText);
  22.             if (response.error) {
  23.                 console.log(String.format("Error: {0}\n{1}", response.error.code, response.error.message.value));
  24.             }
  25.             else {
  26.                 console.log(String.format("Resource created with ID: {0}", response.d.Id));
  27.             }
  28.         }
  29.     });
  30. }
  31.  
  32. $(document).ready(startScript);

Assuming this script is saved in the file CreateResource.js in the Site Assets library of the PWA site, and the jquery-1.9.1.min.js can be found in that library as well, you can add a Script Editor web part to your page, and include these two lines in the web part to inject the resource creation functionality into the page:

/PWA/SiteAssets/jquery-1.9.1.min.js
/PWA/SiteAssets/CreateResource.js

Of course, you typically don’t want such automatic resource creation on page load in a real-world scenario. Instead that, you should probably add some text fields for the resource properties (like name, cost center) and a button a page, to submit the data and create the resources using the parameters the user entered in the text fields. The plain goal of the example above is to show how the request for creating a new enterprise resource looks like, and how to send it to the server.


Issues with the PSContext Object

$
0
0

A few weeks ago I wrote a post about how to access the PSI methods via the PSContext object, that represent the Project Server context in the server side object model. As I wrote in the side note in my post, there are some issues with this object. In the current post I explain what I mean on that exactly. The content of the post relates to the 2016 May CU of Project Server, older and newer CUs might behave differently.

The PSContext class has no public constructor. There are however three overloads of the static GetContext method you can use to get the context: one with an SPSite parameter, one with an SPWeb parameter, and one with a Uri parameter. These methods call the corresponding private constructors (one with an SPSite parameter, one with an SPWeb parameter, and one with a Uri parameter either) of the PSContext class. The private constructor having a Uri parameter creates an SPSite instance based on the Uri and then calls the constructor having an SPSite parameter. The constructor having an SPSite parameter takes the root web of the SPSite  instance, and calls the constructor having the SPWeb parameter.

A potential issue I found is, that the PSContext constructor with the SPSite parameter type stores the SPSite parameter in a private field and dispose it when the PSContext object gets disposed. Although I have not yet faced with its side effects, you should be aware of this behavior to avoid surprises.

  1. using (SPSite site = new SPSite(pwaUrl))
  2. {                
  3.     using (PSContext projectContext = PSContext.GetContext(site))
  4.     {
  5.         Console.WriteLine("Project count: {0}", projectContext.Projects.Count());
  6.     }
  7.  
  8.     // the site is already disposed at this point
  9.     // but you may want to use it further
  10.     Console.WriteLine("Site url: {0}", site.Url);
  11. }

I think the site should be stored in the private field and be disposed in the case of the PSContext constructor with the Uri parameter type (as we create a new SPSite instance in this case), and not in the constructor with the SPSite parameter when we get the SPSite instance from the external code.

But a more serious issue that caused me some headache already is the next one:

In my recent post I described, that the private static RetrieveValue method of the PJClientCallableContext class (that is an important part of the context construction) behaves differently based on the condition, whether or not the process runs with or without a HTTP context. Without HTTP context, the SPWeb object passed to the PSContext constructor is used to create the context. Otherwise, if we have an HTTP context, the SPWeb object (or the Uri, or the SPSite, depending on which constructor you invoked) passed to the PSContext constructor is simply ignored. Let’s see what it means for the developers.

As long as you use the PSContext object from a console application, or from an application without an HTTP context, like a timer job or Windows service, it’s OK, as far as I see. You might be however surprised if you think the parameters of the GetContext methods have any significance if the PSContext object is used in a web application. Based on my experience they do not have any. Of course, the value of the Uri parameter, if you use this overload, should point to a SharePoint site, to enable the creation of the SPSite instance. But beyond that, the parameter is simply ignored.

If the web page belongs to a PWA instance, then this PWA instance will be used as the context.

If you happen to have multiple PWA instances on the same server, you can not access the other one via the PSContext object, even if you pass a parameter (Uri, SPSite or SPWeb) to the GetContext method that points to the other PWA instance.

You can even pass a parameter (Uri, SPSite or SPWeb) to the GetContext method that points to a SharePoint site without PWA instance, still the PWA instance of the page will be used.

It means on the other side, that you can not use the PSContext object in a web page without PWA instance (at least, unless you try to fake the HTTP context as described here). If you try it, you receive an HTTP 403 error (similar to the error discussed in this post):

A first chance exception of type ‘System.UnauthorizedAccessException’ occurred in mscorlib.dll

Additional information: Attempted to perform an unauthorized operation.

   at Microsoft.ProjectServer.PJClientCallableContext.get_PJContext()
   at Microsoft.ProjectServer.PJClientCallable.HandleDelegation()
   at Microsoft.ProjectServer.PJClientCallable.CallPSITag[TResult](UInt32 ulsID, String caller, Func`2 body, Action`2 onError)
   at Microsoft.ProjectServer.ProjectCollection.<>c__DisplayClass17.<>c__DisplayClass25.<.ctor>b__8()
   at System.Lazy`1.CreateValue()
   at System.Lazy`1.LazyInitValue()
   at Microsoft.ProjectServer.ProjectCollection.<>c__DisplayClass17.<.ctor>b__7()
   at System.Lazy`1.CreateValue()
   at System.Lazy`1.LazyInitValue()
   at Microsoft.ProjectServer.ProjectCollection.GetEnumerator()
   at System.Linq.Enumerable.Count[TSource](IEnumerable`1 source)
   at Custom.YourTest.Page_Load(Object sender, EventArgs e)

I think a better implementation of the PSContext object would be to have a static Current getter property (similar to the SPContext class) that we could use only if there is a HTTP context, otherwise it would return null or throw an Exception. The static GetContext method should have been reserved for usage from processes without HTTP context. Invoking these methods from a process having HTTP context should throw an exception.


Viewing all 206 articles
Browse latest View live