Quantcast
Channel: RLV Blog
Viewing all articles
Browse latest Browse all 79

Working with enterprise custom fields in Project Online

$
0
0

I was tasked with with writing a utility for setting Enterprise Custom Fields on User Resources in Microsoft Project Online. The fields got their values from Lookup Tables. Since is was Project Online, the client side object model, CSOM, had to be used. Without previous experience of MS Project, this proved to be tricky.

Here I present a C# class that should help anyone in the same situation to get started. It is a simple console application that show various ways to interact with MS Project, and in particular how to extract available custom field values and and set on user resources. Below are some interesting snippets of the code showing how to work with Project and CSOM. The complete class can do more and is available at the bottom as well as on GitHub.

 

Project Online is a (very) customized SharePoint site collection. We can use same techniques when working with it as with any SharePoint Online site. As always, we need to begin with getting a context. In this case we request a ProjectContext which is just a normal SharePoint context object with additional properties:

var username = "user@mytenant.onmicrosoft.com";
var password = "12345";
var site = "https://mytenant.sharepoint.com/sites/PROJ";

var securePassword = new SecureString();
foreach (var ch in password.ToCharArray())
{
    securePassword.AppendChar(ch);
}

context = new ProjectContext(site);
context.Credentials = new SharePointOnlineCredentials(username, securePassword);

 

As always in CSOM, we need to explicitly load all resources we need to access:

context.Load(context.Web);
context.ExecuteQuery();
Console.WriteLine("   Connected to '" + context.Web.Title + "'");

 

Listing enterprise custom fields and lookup table entries is simply a matter of iterating over the respective properties:

CustomFieldCollection customFields = context.CustomFields;
LookupTableCollection lookupTables = context.LookupTables;
context.Load(customFields);
context.Load(lookupTables);
context.ExecuteQuery();

Console.WriteLine("Custom Fields:");
foreach (CustomField cf in customFields)
{
    Console.WriteLine(cf.Name + " {" + cf.Id + "}  (" + entry.InternalName + ")");
}

Console.WriteLine("Lookup Tables");
foreach (LookupTable lt in lookupTables)
{
    Console.WriteLine(lt.Name + " {" + lt.Id + "}");
    context.Load(lt.Entries);
    context.ExecuteQuery();
    foreach (LookupEntry entry in lt.Entries)
    {
        Console.WriteLine("    " + entry.FullValue + " {" + entry.Id + "}  (" + entry.InternalName + ")");
    }
}

 

A user resources is not the same as the user object. It seems that we can’t trust that all users emails are synced to Project Online. To get around this, I load user resource using the login name instead of just the email, by simply appending the claims prefix to the email:

var claimsPrefix = "i:0#.f|membership|";
var loginName = claimsPrefix + userEmail;
User user = context.Web.SiteUsers.GetByLoginName(loginName);
EnterpriseResource userRes = context.EnterpriseResources.GetByUser(user);
context.Load(userRes);
context.ExecuteQuery();
Console.WriteLine("Got user resource: " + userRes.Name);

 

Now we can get the custom fields from the user resource. Note that fieldValue is a string array, because it can be multi valued.

var fieldValues = userRes.FieldValues;
if (fieldValues.Count == 0)
{
    Console.WriteLine("User has no custom fields");
}
foreach (var fieldValue in fieldValues)
{
    Console.WriteLine(fieldValue.Key + " = " + fieldValue.Value);

    foreach (var value in (string[])fieldValue.Value)
    {
        Console.WriteLine("\t{" + value + "}");
    }
}

 

To set a custom field with the value of a lookup table we need to understand the internal structure:

  • Each value in both the custom fields and lookup tables have a GUID, an internal name and a display name (also called simply name or full value). (The internal name is actually a concatenation of the GUID and a word like “custom” or “entry”.)
  • A custom field may be bound to a lookup table. We use the internal name of the lookup table entries to set such custom fields.
  • Since these are custom fields, the compiler is not aware of them. In order to set such a field we need to access it using an [indexer] together with the internal name.
  • Since field values can be multi-valued, we need to set it using a string array.
  • Finally, to persist the changes we need to update the EnterpriseResources collection, because this is where the resources are stored.

Now that we know all this, it is actually quite easy to set the field! Assuming that we already have the internal names of the custom field and lookup table entry we wish to set it to:

userRes[customFieldInternalName] = new string[] { lookupTableEntryInternalName };
context.EnterpriseResources.Update();
context.ExecuteQuery();

 

Putting all of this together, I made a class that can be used to read and write custom fields, and also shows how to list a bunch of information from Project sites and its users. Use it as a template to make your own solution. You need to modify it to use your own login information and GUID:s before you can use it. Again, the full Visual Studio solution can be downloaded from GitHub.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.ProjectServer.Client;
using Microsoft.SharePoint.Client;
using Microsoft.SharePoint.Client.UserProfiles;
using System.Security;

namespace UserPropsDemo
{
    class UserProps
    {
        private ProjectContext context;

        // Custom Enterprise Fields
        private const string CustomFieldGuid = "618D383A-CCBD-11E5-9FC3-966EB9876117";      // must be a valid guid for a custom field in your MS Project Online
        private Dictionary<string, CustomFieldEntity> AllCustomFields = null;

        // Internal classes for keeping data
        private class LookupEntryEntity
        {
            public string Id { get; set; }
            public string InternalName { get; set; }
            public string Value { get; set; }
        }

        private class CustomFieldEntity
        {
            public string Id { get; set; }
            public string InternalName { get; set; }
            public string Name { get; set; }

            public Dictionary<string, LookupEntryEntity> LookupEntries { get; set; }
            public CustomFieldEntity()
            {
                LookupEntries = new Dictionary<string, LookupEntryEntity>();
            }
        }

        // Constructor. Will connect to Project Online with mandratory credentials.
        public UserProps(string site, string username, string password)
        {
            try
            {
                if (site == "" || username == "" || password == "")
                {
                    throw (new Exception("Must supply site, username and password!"));
                }

                Console.WriteLine("Connecting to Project Online @ " + site + "...");

                var securePassword = new SecureString();
                foreach (var ch in password.ToCharArray())
                {
                    securePassword.AppendChar(ch);
                }

                context = new ProjectContext(site);
                context.Credentials = new SharePointOnlineCredentials(username, securePassword);
                context.Load(context.Web);
                context.ExecuteQuery();
                Console.WriteLine("   Connected to '" + context.Web.Title + "'");
            }
            catch (Exception ex)
            {
                Console.WriteLine("Error: " + ex.Message);
            }
        }

        // Public facing function to set the custom enterprise resource fields
        public void SetCustomField(string userEmail, string newValue = "")
        {
            var user = GetUserResource(userEmail);
            if (user != null)
            {
                // Get all custom fields
                LoadCustomFields();

                // Set the custom enterprise resource field
                try
                {
                    Console.WriteLine("Setting custom enterprise resource property for user...");
                    newValue = newValue.ToLower();
                    var fieldInternalName = AllCustomFields[CustomFieldGuid].InternalName;
                    var entryInternalName = AllCustomFields[CustomFieldGuid].LookupEntries[newValue].InternalName;  // not that we are doing a string match here to figure out the internal name of the value in the lookup table
                    user[fieldInternalName] = new string[] { entryInternalName };   // note that it should be a string array
                    Console.WriteLine("\t" + AllCustomFields[CustomFieldGuid].Name + " >> " + AllCustomFields[CustomFieldGuid].LookupEntries[newValue].Value);
                }
                catch (Exception ex)
                {
                    Console.WriteLine("Could not set custom field " + CustomFieldGuid + " to " + newValue + ": " + ex.Message);
                }


                // Persist changes (note than the resource object is in the EnterpriseResources collection)
                try
                {
                    Console.WriteLine("\tSaving changes...");
                    context.EnterpriseResources.Update();
                    context.ExecuteQuery();
                }
                catch (Exception ex)
                {
                    Console.WriteLine("Error saving: " + ex.Message);
                }
            }
        }

        // Load all custom enterprise resource fields and package in an easy to access dictionary.
        private void LoadCustomFields()
        {
            if (AllCustomFields == null)
            {
                Console.WriteLine("Loading custom fields and lookup entries...");

                // In this example I am only using one custom field. You can add more custom field guids to this list to handle multiple fields
                var customFields = new List<CustomField> { context.CustomFields.GetByGuid(new Guid(CustomFieldGuid)) };
                foreach (var field in customFields)
                {
                    context.Load(field);
                    context.Load(field.LookupEntries);
                }
                context.ExecuteQuery();

                // Package custom fields in an easy to access format
                AllCustomFields = new Dictionary<string, CustomFieldEntity>();
                foreach (var field in customFields)
                {
                    //Console.WriteLine(field.InternalName + " = " + field.Name);
                    var cfe = new CustomFieldEntity() { Id = field.Id.ToString(), InternalName = field.InternalName, Name = field.Name };
                    foreach (var entry in field.LookupEntries)
                    {
                        //Console.WriteLine("\t" + entry.InternalName + " = " + entry.FullValue);
                        cfe.LookupEntries.Add(
                                                entry.FullValue.ToLower(),
                                                new LookupEntryEntity() { Id = entry.Id.ToString(), InternalName = entry.InternalName, Value = entry.FullValue }
                                             );
                    }
                    AllCustomFields.Add(field.Id.ToString(), cfe);
                }
            }
        }


        // Loads a user as an enterprise resouce
        private EnterpriseResource GetUserResource(string userEmail)
        {
            try
            {
                Console.WriteLine("Loading user resource for '" + userEmail + "'");

                // Since we can't trust that email is synced to project, get user by login name instead
                string claimsPrefix = "i:0#.f|membership|";
                var loginName = claimsPrefix + userEmail;
                User user = context.Web.SiteUsers.GetByLoginName(loginName);
                EnterpriseResource res = context.EnterpriseResources.GetByUser(user);
                context.Load(res);
                context.ExecuteQuery();
                Console.WriteLine("   Got resource: " + res.Name + " {" + res.Id + "}");
                return res;
            }
            catch (Exception ex)
            {
                Console.WriteLine("Error loading user: " + ex.Message);
                return null;
            }
        }


        // --------------- Below are some other examples on how to get data from Project ---------------


        // This shows how to get all custom fields present on a user
        public void ListUserResourceCustomFields(string email)
        {
            var userRes = GetUserResource(email);

            // iterate custom fields
            Console.WriteLine("Loading custom fields...");

            var allFields = new Dictionary<string, Dictionary<string, string>>();

            var customFields = userRes.CustomFields;
            context.Load(customFields);
            context.ExecuteQuery();

            foreach (var field in customFields)
            {
                context.Load(field);
                context.Load(field.LookupEntries);
                context.ExecuteQuery();

                //Console.WriteLine("\t" +  field.Name + " {" + field.InternalName + "}");
                var entries = new Dictionary<string, string>();
                entries.Add("KEYNAME", field.Name);

                foreach (var entry in field.LookupEntries)
                {
                    //Console.WriteLine("\t  " + entry.FullValue + " {" + entry.InternalName + "}");
                    entries.Add(entry.InternalName, entry.FullValue);
                }

                allFields.Add(field.InternalName, entries);
            }

            Console.WriteLine("-----------------User Custom Fields-----------------");
            var fieldValues = userRes.FieldValues;
            if (fieldValues.Count == 0)
            {
                Console.WriteLine("User has no custom fields...");
            }
            foreach (var fieldValue in fieldValues)
            {
                Console.WriteLine(allFields[fieldValue.Key]["KEYNAME"] + " {" + fieldValue.Key + "}");

                foreach (var value in (string[])fieldValue.Value)
                {
                    Console.WriteLine("\t" + allFields[fieldValue.Key][value] + " {" + value + "}");
                }
            }
        }

        // This function shows how to list facts about the MS Project site
        public void ListContextData()
        {
            Console.WriteLine("\nListing context data...\n");

            Console.WriteLine("---------------All Projects---------------");
            context.Load(context.Projects);
            context.ExecuteQuery();

            foreach (PublishedProject proj in context.Projects)
            {
                Console.WriteLine(proj.Name);
            }

            Console.WriteLine("---------------All Site Users---------------");
            UserCollection siteUsers = context.Web.SiteUsers;
            context.Load(siteUsers);
            context.ExecuteQuery();

            var peopleManager = new PeopleManager(context);
            foreach (var user in siteUsers)
            {
                try
                {
                    PersonProperties userProfile = peopleManager.GetPropertiesFor(user.LoginName);
                    context.Load(userProfile);
                    context.ExecuteQuery();

                    Console.WriteLine(userProfile.DisplayName + " (" + userProfile.Email + ")");
                }
                catch (Exception ex)
                {
                    Console.WriteLine("Error loading user: " + user.LoginName + " --> " + ex.Message);
                }
            }


            Console.WriteLine("---------------All Site User Resources---------------");
            EnterpriseResourceCollection resources = context.EnterpriseResources;
            context.Load(resources);
            context.ExecuteQuery();

            foreach (EnterpriseResource res in resources)
            {
                Console.WriteLine(res.Name + " {" + res.Id + "}");
            }

            Console.WriteLine("---------------Custom Fields---------------");
            CustomFieldCollection customFields = context.CustomFields;
            context.Load(customFields);
            context.ExecuteQuery();
            foreach (CustomField cf in customFields)
            {
                Console.WriteLine(cf.Name + " {" + cf.Id + "}");
            }

            Console.WriteLine("---------------Lookup Tables---------------");
            LookupTableCollection lookupTables = context.LookupTables;
            context.Load(lookupTables);
            context.ExecuteQuery();
            foreach (LookupTable lt in lookupTables)
            {
                Console.WriteLine(lt.Name + " {" + lt.Id + "}");

                context.Load(lt.Entries);
                context.ExecuteQuery();
                foreach (LookupEntry entry in lt.Entries)
                {
                    Console.WriteLine("    " + entry.FullValue + " {" + entry.Id + "}");
                }
            }
        }

    }
}

 


Viewing all articles
Browse latest Browse all 79

Trending Articles