Second Life of a Hungarian SharePoint Geek

June 25, 2017

Copying Hierarchical Lookup Table Entries via the Managed Object Model

After I’ve described, how to copy flat lookup tables via the Project Server managed object model, this time I will go a step forward, and show how you can copy lookup tables with hierarchy, like the RBS (resource breakdown structure) table.

The complexity of the task (comparing to the flat lookup tables) comes to the fact, that child entries are bound to their respective parent entries not via the IDs (like having a property called ParentId) but simply via the FullValue property. See the properties of the LookupEntry class in the documentation. For example (assuming the separator character used in the code mask is the period “.”), the parent entry of a child entry having its FullValue property like “Division.Subdivision.SubSubdivision” is the entry having a FullValue property like “Division.Subdivision”. Furthermore, the parent entry should be already included in the lookup table, as we inserts its child items, but it seems to be fulfilled by the standard Project Server behavior, as it returns entries in the correct order (parent entries first, their child entries next) for a simply request.

As in the case of the flat tables, we should copy the target entries one by one, by adding new LookupEntryCreationInformation instances to the existing Entries property (of type LooupEntryCollection) of the target lookup table.

Just to make our life a bit harder, in contrast to the LookupEntry class the LookupEntryCreationInformation class has a property ParentId, but no FullValue property at all. It has, however a Value property that you should to set to the value of the child entry, without the joined values of the parent entries. You should set the ParentId property to the value of the Id of the parent entry only if there is a parent entry, otherwise you mustn’t set this property (for example, to null). You can append the LookupEntryCreationInformation instance to the target LooupEntryCollection instance via Add method.

If you would like to get the Id of the parent entry, it would be nice to split the last tag from the FullValue of the current LookupEntry instance to first get the full value of the parent entry (like by splitting SubSubdvision from “Division.Subdivision.SubSubdivision” we would get “Division.Subdivision”, the FullValue of the parent entry), and make a query for the LookupEntry having the same value in the collection of already appended entries afterwards, like this:

parentId = ltTargetEntries.First(e => e.FullValue == parentFullValue).Id;

If you try that, you get the very same exception, that you receive if you try to access a property that you have not explicitly or implicitly requested in the client object model:

An unhandled exception of type ‘Microsoft.SharePoint.Client.PropertyOrFieldNotInitializedException’ occurred in Microsoft.SharePoint.Client.Runtime.dll
Additional information: The property or field ‘FullValue’ has not been initialized. It has not been requested or the request has not been executed. It may need to be explicitly requested.

You could request the entire entry collection including the FullValue property of the entries after each update, but it would not be very efficient. Instead of this, we create a dictionary object of type Dictionary<Guid, string> to store a local mapping of the Id – FullValue pairs, and use this mapping to look up the parent entries.

This method assumes the target lookup table already exists, and both of the source and target tables have the same depth / code mask and the period character “.” as separator:

  1. private void CopyHierarchicalLookupTableValues(string sourcePwa, string sourceTable, string targetPwa, string targetTable)
  2. {
  3.     var separator = '.';
  4.  
  5.     LookupEntryCollection ltSourceEntries = null;
  6.     using (var pcSource = new ProjectContext(sourcePwa))
  7.     {
  8.         pcSource.Load(pcSource.LookupTables, lts => lts.Where(lt => lt.Name == sourceTable).Include(lt => lt.Entries.Include(e => e.FullValue, e => e.Id, e => e.SortIndex)));
  9.         pcSource.ExecuteQuery();
  10.  
  11.         if (pcSource.LookupTables.Any())
  12.         {
  13.             ltSourceEntries = pcSource.LookupTables.First().Entries;
  14.         }
  15.         else
  16.         {
  17.             Console.WriteLine("Source table '{0}' not found on PWA '{1}'", sourceTable, sourcePwa);
  18.         }
  19.     }
  20.  
  21.     if (ltSourceEntries != null)
  22.     {
  23.         using (var pcTarget = new ProjectContext(targetPwa))
  24.         {
  25.             pcTarget.Load(pcTarget.LookupTables, lts => lts.Where(lt => lt.Name == targetTable).Include(lt => lt.Name));
  26.             pcTarget.ExecuteQuery();
  27.  
  28.             // target table exist
  29.             if (pcTarget.LookupTables.Any())
  30.             {
  31.                 var ltTargetEntries = pcTarget.LookupTables.First().Entries;
  32.                 var localIdToFullValueMap = new Dictionary<Guid, string>();
  33.  
  34.                 // we cannot assign the FullValue property the value that includes the separator characters
  35.                 // to avoid LookupTableItemContainsSeparator = 11051 error
  36.                 // we should  split the value at separator characters and assign the last item to the Value property and if there is a parent item
  37.                 // set the ParentId property as well, see later
  38.                 // https://msdn.microsoft.com/en-us/library/office/ms508961.aspx
  39.                 ltSourceEntries.ToList().ForEach(lte =>
  40.                 {
  41.                     var value = lte.FullValue;
  42.                     Console.WriteLine("FullValue: '{0}'", value);
  43.                     Guid? parentId = null;
  44.                     var parentFullValue = string.Empty;
  45.  
  46.                     var lastIndexOfSeparator = value.LastIndexOf(separator);
  47.                     if (lastIndexOfSeparator > -1)
  48.                     {
  49.                         parentFullValue = value.Substring(0, lastIndexOfSeparator);
  50.                         value = value.Substring(lastIndexOfSeparator + 1);
  51.                         Console.WriteLine("value: '{0}'", value);
  52.                         Console.WriteLine("parentFullValue: '{0}'", parentFullValue);
  53.  
  54.                         // parent should have been already appended to avoid the error:
  55.                         // An unhandled exception of type 'Microsoft.SharePoint.Client.PropertyOrFieldNotInitializedException' occurred in Microsoft.SharePoint.Client.Runtime.dll
  56.                         // Additional information: The property or field 'FullValue' has not been initialized. It has not been requested or the request has not been executed. It may need to be explicitly requested.
  57.                         //parentId = ltTargetEntries.First(e => e.FullValue == parentFullValue).Id;
  58.                         parentId = localIdToFullValueMap.First(e => e.Value == parentFullValue).Key;
  59.                         Console.WriteLine("parentId: '{0}'", parentId);
  60.  
  61.                     }
  62.  
  63.                     // instead creating a new ID, you can copy the existing ID
  64.                     // it works only if you copy the entries to another PWA instance,
  65.                     // and only if there wasn't already an entry with the same ID
  66.                     var id = Guid.NewGuid(); // lte.Id;
  67.  
  68.                     var leci = new LookupEntryCreationInformation
  69.                     {
  70.                         Id = id,
  71.                         Value = new LookupEntryValue { TextValue = value },
  72.                         SortIndex = lte.SortIndex
  73.                     };
  74.  
  75.                     Console.WriteLine("leci Id: '{0}', Value: '{1}'", leci.Id, leci.Value.TextValue);
  76.                     var fullValue = value;
  77.  
  78.                     // we should set the ParentId property only if the entry has really a parent
  79.                     // setting the ParentId property to null is not OK
  80.                     if (parentId.HasValue)
  81.                     {
  82.                         leci.ParentId = parentId.Value;
  83.                         fullValue = parentFullValue + separator + value;
  84.                     }
  85.  
  86.  
  87.                     localIdToFullValueMap.Add(leci.Id, fullValue);
  88.  
  89.                     ltTargetEntries.Add(leci);
  90.                     // if there are a lot of entries, it might be advisable to update and execute query after each of the entries
  91.                     // to avoid "The request uses too many resources" error message
  92.                     // https://pholpar.wordpress.com/2015/07/19/how-to-avoid-the-request-uses-too-many-resources-when-using-the-client-object-model-via-automated-batching-of-commands/
  93.                     // pcTarget.LookupTables.Update();
  94.                     // pcTarget.ExecuteQuery();
  95.                 });
  96.  
  97.                 pcTarget.LookupTables.Update();
  98.                 pcTarget.ExecuteQuery();
  99.             }
  100.             else
  101.             {
  102.                 Console.WriteLine("Target table '{0}' not found on PWA '{1}'", targetTable, targetPwa);
  103.             }
  104.         }
  105.     }
  106. }

The following call copies the lookup table RBS from one PWA instance to another one:

CopyHierarchicalLookupTableValues("http://YourProjectServer/PWA&quot;, "RBS", "http://AnotherProjectServer/PWA&quot;, "RBS");

The notes I made for the flat lookup tables apply for the hierarchical case as well:

If your lookup table has not a lot of entries, you can probably copy them in a single batch, using a single call to the ExecuteQuery method. Otherwise, if thee batch size exceeds the 2 MB limit, you might have an exception like “The request uses too many resources”. In this case I suggest you to invoke the ExecuteQuery method for each entry, or create an ExecuteQueryBatch method, as described in this post.

Theoretically, you could copy the entries with their ID, but technically it is not always an option. For example, if you would like to copy the entries within the same PWA instance, you can’t have two entries sharing the same IDs. Based on my experience, if you have already an entry with the same ID, and you would like to copy it into another lookup table, although no exception is thrown, the entry won’t be copied.

And one last additional note yet: Of course, you can copy not only hierarchical lookup tables, but flat lookup tables as well with this script.

June 20, 2017

Copying Flat Lookup Table Entries via the Managed Object Model

Assume you have in Project Server a flat lookup table (I mean a lookup table having a single level, without any hierarchy between the entries), and you would like to copy the entries to another (already existing!) lookup table, that may exist on the same or on another server / PWA instance. You can do the via the managed object model of Project Server, as demonstrated by the code below:

  1. private void CopyLookupTableValues(string sourcePwa, string sourceTable, string targetPwa, string targetTable)
  2. {
  3.     LookupEntryCollection ltSourceEntries = null;
  4.     using (var pcSource = new ProjectContext(sourcePwa))
  5.     {
  6.         pcSource.Load(pcSource.LookupTables, lts => lts.Where(lt => lt.Name == sourceTable).Include(lt => lt.Masks, lt => lt.Entries.Include(e => e.FullValue, e => e.Id, e => e.SortIndex)));
  7.         pcSource.ExecuteQuery();
  8.  
  9.         if (pcSource.LookupTables.Any())
  10.         {
  11.             ltSourceEntries = pcSource.LookupTables.First().Entries;
  12.         }
  13.         else
  14.         {
  15.             Console.WriteLine("Source table '{0}' not found on PWA '{1}'", sourceTable, sourcePwa);
  16.         }
  17.     }
  18.  
  19.     if (ltSourceEntries != null)
  20.     {
  21.         using (var pcTarget = new ProjectContext(targetPwa))
  22.         {
  23.             pcTarget.Load(pcTarget.LookupTables, lts => lts.Where(lt => lt.Name == targetTable).Include(lt => lt.Name));
  24.             pcTarget.ExecuteQuery();
  25.  
  26.             // target table exist
  27.             if (pcTarget.LookupTables.Any())
  28.             {
  29.                 var ltTargetEntries = pcTarget.LookupTables.First().Entries;
  30.  
  31.                 ltSourceEntries.ToList().ForEach(lte => {
  32.                     ltTargetEntries.Add(new LookupEntryCreationInformation
  33.                         {
  34.                             // instead creating a new ID, you can copy the existing ID
  35.                             // it works only if you copy the entries to another PWA instance,
  36.                             // and only if there wasn't already an entry with the same ID
  37.                             Id = Guid.NewGuid(), // lte.Id,
  38.                             Value = new LookupEntryValue { TextValue = lte.FullValue },
  39.                             SortIndex = lte.SortIndex
  40.                         });
  41.                     // if you have a lot of entries, it might be better to execute the query for each entries
  42.                     // to avoid 'The request uses too many resources' error
  43.                     // pcTarget.LookupTables.Update();
  44.                     // pcTarget.ExecuteQuery();
  45.                 });
  46.  
  47.                 pcTarget.LookupTables.Update();
  48.                 pcTarget.ExecuteQuery();
  49.             }
  50.             else
  51.             {
  52.                 Console.WriteLine("Target table '{0}' not found on PWA '{1}'", targetTable, targetPwa);
  53.             }
  54.         }
  55.     }
  56. }

The following call copies the lookup table Divisions from one PWA instance to another one:

CopyLookupTableValues("http://YourProjectServer/PWA&quot;, "Divisions", "http://AnotherProjectServer/PWA&quot;, "Divisions");

If your lookup table has not a lot of entries, you can probably copy them in a single batch, using a single call to the ExecuteQuery method. Otherwise, if thee batch size exceeds the 2 MB limit, you might have an exception like “The request uses too many resources”. In this case I suggest you to invoke the ExecuteQuery method for each entry, or create an ExecuteQueryBatch method, as described in this post.

Theoretically, you could copy the entries with their ID, but technically it is not always an option. For example, if you would like to copy the entries within the same PWA instance, you can’t have two entries sharing the same IDs. Based on my experience, if you have already an entry with the same ID, and you would like to copy it into another lookup table, although no exception is thrown, the entry won’t be copied.

The sample above works only for flat (non-hierarchical) lookup tables. You can copy hierarchical lookup tables (like RBS – resource breakdown structure) as well, but it requires a bit more coding, as I show you in the next post.

You can find further sample codes to manipulate Project Server enterprise custom fields and lookup table via the client object model in this older post.

August 20, 2016

Issues with the PSContext Object

Filed under: Bugs, PS 2013 — Tags: , — Peter Holpar @ 22:43

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.

July 26, 2016

Creating Project Server Enterprise Resources via REST

Filed under: Enterprise Resources, JavaScript, PS 2013, REST — Tags: , , , — Peter Holpar @ 21:27

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.

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

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

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

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.

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

Filed under: PS 2013, PSI, Reflection — Tags: , , — Peter Holpar @ 21:21

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.

May 29, 2016

Project Publishing Failed due to Deleted SharePoint User

Filed under: Bugs, Event receivers, PowerShell, PS 2013 — Tags: , , , — Peter Holpar @ 05:53

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.

Project Publishing Failed due to Scheduling Conflict

Filed under: PS 2013 — Tags: — Peter Holpar @ 05:43

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.

May 16, 2016

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

Filed under: PS 2013 — Tags: — Peter Holpar @ 15:57

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!

Older Posts »

Create a free website or blog at WordPress.com.