In the recent days I’m busy with Project Online and its CSOM API (if you would like to avoid some traps, see my first experiences with the Project 2013 SDK here). One of the tasks includes manipulating calendar exceptions for enterprise resources, like creating, listing and deleting calendar exceptions for our resources (I mean colleagues) as their holiday requests get approved / rejected.
I should note, that editing calendar exceptions seems to be not available on the Project Server user interface, only using the desktop client Project Professional. As far as I see (using Fiddler), Project Professional still uses the Project Server Interface (PSI) web services, although it is officially not the way we have to choose when working against Project Online:
“To develop applications for Project Online, you must use the Microsoft.ProjectServer.Client namespace instead of the PSI.” (source: MSDN)
I made some quick checks, and technically (with some minor hacks) it still seems to be possible to use PSI with Project Online, but let’s try to keep with CSOM this time, at least as long as it covers our needs.
Before delving deeper into code details, you can find some useful links about authentication against O365 in my former post, that I suggest to read as we apply a similar technique in this case.
I’ve created a new console application (C#, Target framework: .NET framework 4, Platform target: x86), and added some assembly references as shown below. To have Microsoft.IdentityModel, you should download the Windows Identity Foundation SDK 3.5.
I borrowed the helper classes Office365ClaimsHelper and WcfClientContracts from this post of Sundararajan Narasiman. The link to the source code of those classes can be found at the bottom of that post. You should alter the namespace of the classes to match the one of your application.
I’ve created an internal static ProjectOnlineTest class to wrap the functionality, with the following namespaces imported:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using Microsoft.ProjectServer.Client;
The most important values are declared first, you should edit them to match your own site and user credentials.
- private static readonly string projName;
- private static readonly string connectUserName;
- private static readonly string connectPwd;
- private static readonly string pwaPath;
- private static readonly string projDomain;
- static ProjectOnlineTest()
- {
- // update the values here
- projName = "YourProjSite";
- connectUserName = "john.smith";
- connectPwd = "password";
- projDomain= string.Format("{0}.onmicrosoft.com", projName);
- pwaPath = string.Format("https://{0}.sharepoint.com/sites/pwa", projName);
- }
I added some helper properties and a method to make my code a bit simpler to read and modify. Note the format of the Project Online user name in the GetFullUserName method.
- private static ProjectContext ProjectContext
- {
- get
- {
- var pc = new ProjectContext(pwaPath);
- var claimsHelper = ClaimsHelper;
- pc.ExecutingWebRequest += claimsHelper.clientContext_ExecutingWebRequest;
- return pc;
- }
- }
- private static Office365ClaimsHelper ClaimsHelper
- {
- get
- {
- return new Office365ClaimsHelper(new Uri(pwaPath),
- string.Format("{0}@{1}", connectUserName, projDomain),
- connectPwd);
- }
- }
- private static string GetFullUserName(string userName)
- {
- return string.Format("i:0#.f|membership|{0}@{1}", userName, projDomain);
- }
The CreateCalendarException method creates a new calendar exception for the resource (user). The key is to use the CalendarExceptionCreationInformation class and calling the Update method of the EnterpriseResourceCollection class. Credits go for the latter one again to Jim Corbin, the member of the Microsoft Project SDK team:
“When you make a change to one of the properties, and then save the change, Project Server automatically checks out the resource, make the change, and checks the resource back in — similar to the process of getting an enterprise resource in Project Pro, editing it, and then saving.” (source: MSDN Forum thread)
- internal static void CreateCalendarException(string userName, string exceptionName, DateTime exceptionStart, DateTime exceptionFinish)
- {
- using (var projContext = ProjectContext)
- {
- var ers = projContext.EnterpriseResources;
- var usrs = projContext.Web.SiteUsers;
- var usr = string.IsNullOrEmpty(userName) ? projContext.Web.CurrentUser : usrs.GetByLoginName(GetFullUserName(userName));
- var er = ers.GetByUser(usr);
- projContext.Load(ers);
- projContext.Load(usrs);
- projContext.Load(usr);
- projContext.Load(er);
- projContext.ExecuteQuery();
- var rcExs = er.ResourceCalendarExceptions;
- CalendarExceptionCreationInformation cexCI = new CalendarExceptionCreationInformation
- {
- Name = exceptionName,
- Start = exceptionStart,
- Finish = exceptionFinish
- };
- rcExs.Add(cexCI);
- ers.Update();
- projContext.ExecuteQuery();
- }
- }
Note: The Associate resource with a user account option for your resource should be checked (or if no user name is specified, your authenticated user should have a resource mapped to) to successfully lookup an enterprise resource by the user (e.g. GetByUser method). Otherwise, you should iterate through the resources and find the one with the specified name, or use a Guid if you know one.
The DeleteCalendarException method deletes the calendar exceptions of the selected user (resource) with the specified name.
- internal static void DeleteCalendarException(string userName, string exceptionName)
- {
- using (var projContext = ProjectContext)
- {
- var ers = projContext.EnterpriseResources;
- var usrs = projContext.Web.SiteUsers;
- var usr = string.IsNullOrEmpty(userName) ? projContext.Web.CurrentUser : usrs.GetByLoginName(GetFullUserName(userName));
- var er = ers.GetByUser(usr);
- var rcExs = er.ResourceCalendarExceptions;
- projContext.Load(ers);
- projContext.Load(usrs);
- projContext.Load(usr);
- projContext.Load(er);
- projContext.Load(rcExs);
- projContext.ExecuteQuery();
- foreach (CalendarException ce in rcExs)
- {
- rcExs.Remove(ce);
- }
- ers.Update();
- projContext.ExecuteQuery();
- }
- }
The DumpCalendarExceptions method simply dumps the existing resources and their calendar exceptions to the console.
- internal static void DumpCalendarExceptions()
- {
- using (var projContext = ProjectContext)
- {
- var ers = projContext.EnterpriseResources;
- projContext.Load(ers);
- projContext.ExecuteQuery();
- Console.WriteLine("\nResource ID : Resource name");
- foreach (EnterpriseResource res in ers)
- {
- Console.WriteLine("\n\t{0}\n\t{1}", res.Id, res.Name);
- var rcExs = res.ResourceCalendarExceptions;
- projContext.Load(rcExs);
- projContext.ExecuteQuery();
- foreach (CalendarException ce in rcExs)
- {
- Console.WriteLine("\n\t{0}\n\t{1}\t{2}\t{3}", ce.Id, ce.Name, ce.Start, ce.Finish);
- }
- }
- }
- }
And here is a sample usage of the methods above:
- static void Main(string[] args)
- {
- ProjectOnlineTest.DeleteCalendarException("project.user", "Test exception X");
- ProjectOnlineTest.CreateCalendarException("another.user", "Test exception A", new DateTime(2013, 3, 5), new DateTime(2013, 3, 15));
- ProjectOnlineTest.DumpCalendarExceptions();
- Console.WriteLine("Press a key to exit: ");
- Console.ReadKey(false);
- }
I hope this sample makes it easier to work with Project Online using CSOM.
Leave a comment