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.

Advertisements

Leave a Comment »

No comments yet.

RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Blog at WordPress.com.

%d bloggers like this: