Tools & Technologies for Building Solutions with Microsoft Project: Update on mpFx Part I

I have been working at Microsoft nearly five months already.  It is hard to believe how time flies!   Yesterday a colleague made the comment “Your blog…what happened?  Since you joined MS…no more blog?“  I didn’t intend to go dark, but I have been nonstop busy for five months.  I thought I would take time today to start writing again.

Prior to joining Microsoft, I was busily working on “mpFx”, a PSI encapsulation project to enable IT developers getting started using the PSI quickly.  That project has not stopped, but I haven’t been writing about it.  In fact, the core mpFx libraries have become the centerpiece of many of my internal projects here at Microsoft. 

Today I am going to do part I of an update on where mpFx stands, what scenarios it currently enables, and my future plans for the project.

Introduction to mpFx

Microsoft Project Fx (mpFx) version 1.0 is a class library that wraps the web services exposed by PSI in an effort to ease development of PSI applications, increase developer productivity, and reduce the amount of rework required to perform similar programming tasks across multiple projects. Beyond the 1.0 class library, mpFx is a collection of tools and technologies designed to increase developer productivity and ensure that Microsoft’s investment in Microsoft Project and PSI is embraced by developers tasked with designing solutions around Microsoft Project.

Solution Components

Version 1.0 of mpFx consists of two class libraries and a Windows Presentation Framework application for testing:

  • Class Library: CodePlex.MicrosoftProject.mpFx – This class library houses the PSI encapsulation

  • Class Library: CodePlex.MicrosoftProject.mpFx.WinForms – This class library contains a series of WinForms dialogs I created for performing common functions such as checking in a project, selecting custom fields, or editing project properties.  The class library is a “common controls” library for PSI.  Another class library is forthcoming that will provide these same common controls in WPF

  • WPF Application: Mops Browser – This Windows Presentation Foundation application is a test harness for the entire suite.

Architecture

The PSI is implemented as series of web services each housing related methods and types.  The CodePlex.MicrosoftProject.mpFx class library exposes PSI data and functionality through an object model, thus obscuring the underlying complexity of rigging and calling the web services directly.   The class library also provides logging, object factories, and other services to the developer.  While not appropriate for certain development scenarios, the object model pattern provides a concise and easily understood strategy for consuming PSI data and functionality. 

Example: Standard PSI Usage Pattern

Here is a sample from the SDK which creates a project and waits for the queued project creation task is complete:

    1: namespace Microsoft.SDK.Project.Samples.QueueCreateProject
    2: {
    3:     class Program
    4:     {
    5:         [STAThread]
    6:         static void Main(string[] args)
    7:         {
    8:             try
    9:             {
   10:                 const string PROJECT_SERVER_URI = "https://ServerName/ProjectServerName/";
   11:                 const string PROJECT_SERVICE_PATH = "_vti_bin/psi/project.asmx";
   12:                 const string QUEUESYSTEM_SERVICE_PATH = "_vti_bin/psi/queuesystem.asmx";
   13:  
   14:                 Guid jobId;
   15:  
   16:                 // Set up the Web service objects
   17:                 ProjectWebSvc.Project projectSvc = new ProjectWebSvc.Project();
   18:  
   19:                 ProjectWebSvc.ProjectDataSet projectDs = new ProjectWebSvc.ProjectDataSet();
   20:  
   21:                 projectSvc.Url = PROJECT_SERVER_URI + PROJECT_SERVICE_PATH;
   22:                 projectSvc.Credentials = CredentialCache.DefaultCredentials;
   23:  
   24:                 QueueSystemWebSvc.QueueSystem q = new QueueSystemWebSvc.QueueSystem();
   25:                 q.Url = PROJECT_SERVER_URI + QUEUESYSTEM_SERVICE_PATH;
   26:                 q.UseDefaultCredentials = true;
   27:  
   28:                 projectDs = new ProjectWebSvc.ProjectDataSet();
   29:  
   30:                 // Create the project
   31:                 ProjectWebSvc.ProjectDataSet.ProjectRow projectRow = projectDs.Project.NewProjectRow();
   32:                 projectRow.PROJ_UID = Guid.NewGuid();
   33:                 projectRow.PROJ_NAME = "Its a wonderful project at " +
   34:                    DateTime.Now.ToShortDateString().Replace("/", "") + " " +
   35:                    DateTime.Now.ToShortTimeString().Replace(":", "");
   36:                 projectRow.PROJ_TYPE = (int)PSLibrary.Project.ProjectType.Project;
   37:                 projectDs.Project.AddProjectRow(projectRow);
   38:  
   39:                 // Add some tasks        
   40:                 jobId = Guid.NewGuid();
   41:                 projectSvc.QueueCreateProject(jobId, projectDs, false);
   42:                 WaitForQueue(q, jobId);
   43:  
   44:             }
   45:             catch (SoapException ex)
   46:             {
   47:                 PSLibrary.PSClientError error = new PSLibrary.PSClientError(ex);
   48:                 PSLibrary.PSErrorInfo[] errors = error.GetAllErrors();
   49:                 string errMess = "==============================\r\nError: \r\n";
   50:                 for (int i = 0; i < errors.Length; i++)
   51:                 {
   52:                     errMess += "\n" + ex.Message.ToString() + "\r\n";
   53:                     errMess += "".PadRight(30, '=') + "\r\nPSCLientError Output:\r\n \r\n";
   54:                     errMess += errors[i].ErrId.ToString() + "\n";
   55:  
   56:                     for (int j = 0; j < errors[i].ErrorAttributes.Length; j++)
   57:                     {
   58:                         errMess += "\r\n\t" + errors[i].ErrorAttributeNames()[j] + ": " + errors[i].ErrorAttributes[j];
   59:                     }
   60:                     errMess += "\r\n".PadRight(30, '=');
   61:                 }
   62:                 Console.ForegroundColor = ConsoleColor.Red;
   63:                 Console.WriteLine(errMess);
   64:             }
   65:             catch (WebException ex)
   66:             {
   67:                 string errMess = ex.Message.ToString() +
   68:                    "\n\nLog on, or check the Project Server Queuing Service";
   69:                 Console.ForegroundColor = ConsoleColor.Red;
   70:                 Console.WriteLine("Error: " + errMess);
   71:             }
   72:             catch (Exception ex)
   73:             {
   74:                 Console.ForegroundColor = ConsoleColor.Red;
   75:                 Console.WriteLine("Error: " + ex.Message);
   76:             }
   77:             finally
   78:             {
   79:                 Console.ResetColor();
   80:                 Console.WriteLine("\r\n\r\nPress any key...");
   81:                 Console.ReadKey();
   82:             }
   83:         }
   84:         static private void WaitForQueue(QueueSystemWebSvc.QueueSystem q, Guid jobId)
   85:         {
   86:             QueueSystemWebSvc.JobState jobState;
   87:             const int QUEUE_WAIT_TIME = 2; // two seconds
   88:             bool jobDone = false;
   89:             string xmlError = string.Empty;
   90:             int wait = 0;
   91:  
   92:             //Wait for the project to get through the queue
   93:             // - Get the estimated wait time in seconds
   94:             wait = q.GetJobWaitTime(jobId);
   95:  
   96:             // - Wait for it
   97:             Thread.Sleep(wait * 1000);
   98:             // - Wait until it is done.
   99:  
  100:             do
  101:             {
  102:                 // - Get the job state
  103:                 jobState = q.GetJobCompletionState(jobId, out xmlError);
  104:  
  105:                 if (jobState == QueueSystemWebSvc.JobState.Success)
  106:                 {
  107:                     jobDone = true;
  108:                 }
  109:                 else
  110:                 {
  111:                     if (jobState == QueueSystemWebSvc.JobState.Unknown
  112:                     || jobState == QueueSystemWebSvc.JobState.Failed
  113:                     || jobState == QueueSystemWebSvc.JobState.FailedNotBlocking
  114:                     || jobState == QueueSystemWebSvc.JobState.CorrelationBlocked
  115:                     || jobState == QueueSystemWebSvc.JobState.Canceled)
  116:                     {
  117:                         // If the job failed, error out
  118:                         throw (new ApplicationException("Queue request failed \"" + jobState + "\" Job ID: " + jobId + ".\r\n" + xmlError));
  119:                     }
  120:                     else
  121:                     {
  122:                         Console.WriteLine("Job State: " + jobState + " Job ID: " + jobId);
  123:                         Thread.Sleep(QUEUE_WAIT_TIME * 1000);
  124:                     }
  125:                 }
  126:             }
  127:             while (!jobDone);
  128:         }
  129:     }
  130: }

Here is the same thing in mpFx:

    1: using System;
    2: using System.Web.Services.Protocols;
    3: using CodePlex.MicrosoftProject.mpFx;
    4: using CodePlex.MicrosoftProject.mpFx.ProjectsWebService;
    5:  
    6: namespace ConsoleTest
    7: {
    8:     class Program
    9:     {
   10:         static void Main(string[] args)
   11:         {
   12:             using (ProjectServer projectServer = new ProjectServer("https://epm2007demo/pwa", DataStoreEnum.WorkingStore))
   13:             {
   14:                 using (ProjectDataSet projectDataSet = EntityFactory.NewProject("Demo"))
   15:                 {
   16:                     try
   17:                     {
   18:                         projectServer.Projects.Create(projectDataSet, false, true);
   19:                     }
   20:                     catch (SoapException exception)
   21:                     {
   22:                         Console.WriteLine(Errors.ProcessMSProjectErrors(exception));
   23:                     }
   24:                     catch (Exception exception)
   25:                     {
   26:                         Console.WriteLine(exception.Message);                        
   27:                     }
   28:                 }
   29:             }
   30:         }
   31:     }
   32: }

First, it is a lot more compact and secondly it is much easier to read.  Let’s walk through what is going on under the covers. 

image The root of the object model, not surprisingly, is the ProjectServer class (see the image left).  In line 12 above, I constructed an instance of ProjectServer using one of four constructors:

  • imageThe first constructor in the image to the left is used for constructing an instance of ProjectServer using forms authentication.

The second constructor is used when using the local web service paths (https://server:port/shared service provider/psi/webservice.asmx), typically for impersonation.

The third constructor is used much as the second except rather then querying SharePoint for the site GUID, the site GUID is a parameter.  This is useful for achieving a small performance gain.

Finally, the forth constructor and the one used in the sample, constructs an instance of ProjectServer for Windows authentication.

All constructors specify the store (published, draft, etc) on which operations subsequently are executed.

ProjectServer implements IDisposable.

Let’s take a look at the source for the forth constructor. 

    1: /// <summary>
    2: /// Constructor for Windows authentication
    3: /// </summary>
    4: /// <param name="projectServerUrl">Project Server URL</param>
    5: /// <param name="store">The store on which subsequent operations will be executed.</param>
    6: public ProjectServer(string projectServerUrl, DataStoreEnum store)
    7: {
    8:     if (!Utilities.IsValidUrl(projectServerUrl))
    9:     {
   10:         throw new ArgumentException(LibraryResources.InvalidProjectServerUrl);
   11:     }
   12:  
   13:     Settings = new ProjectServerSettings();
   14:     WebServices = new WebServices(this);
   15:     Site = new Uri(projectServerUrl);                        
   16:     Store = store;            
   17:  
   18:     NetworkCredential = CredentialCache.DefaultNetworkCredentials;
   19:     AuthenticationType = AuthenticationType.Windows;
   20:  
   21:     WebServices.LoginWindows = new LoginWindows
   22:                                    {
   23:                                        Url = WebServices.AppendPath(projectServerUrl, ServicePaths.WindowsLoginService), 
   24:                                        UseDefaultCredentials = true
   25:                                    };
   26:  
   27:     WebServices.LoginWindows.Login();
   28:  
   29:     Settings.ListSeparator = WebServices.Projects.ReadServerListSeparator();
   30:  
   31: }

 

After inspecting URL, we jump into creating a small object ProjectServerSettings which houses default values for the session description, the number of retries to attempt on a queue wait operation, and the length of time to sleep between calls to query a job status:

    1: /// <summary>
    2: /// Houses all ProjectServer settings
    3: /// </summary>
    4: public class ProjectServerSettings
    5: {
    6:     #region Constructor
    7:  
    8:     protected internal ProjectServerSettings()
    9:     {
   10:         DefaultSessionDescription = LibraryResources.DefaultSessionDescription;
   11:         QueueStatusRetryCount = Settings.Default.QueueStatusRetryCount;
   12:         QueueStatusSleepDuration = Settings.Default.QueueStatusSleepDuration;            
   13:     }
   14:  
   15:     #endregion
   16:  
   17:     #region Public Properties
   18:  
   19:     public int QueueStatusRetryCount { get; set; }
   20:  
   21:     public int QueueStatusSleepDuration { get; set; }
   22:  
   23:     public string DefaultSessionDescription { get; set; }
   24:  
   25:     public char ListSeparator { get; set;}
   26:  
   27:     #endregion
   28: }

 

Next, a containment object, WebServices, is created that houses a ServiceProxy instance for each PSI web service encapsulated in mpFx.   Each web service is accessible as a property.  Let’s take a look at the Projects property:

    1: public ProjectProxy Projects
    2: {
    3:     get
    4:     {
    5:         if (_ProjectWebService == null)
    6:         {
    7:             _ProjectWebService = new ProjectProxy();
    8:  
    9:             if (_Parent.IsImpersonating)
   10:             {
   11:                 ConfigureService(_ProjectWebService, LocalServicePaths.ProjectService, true);
   12:             }
   13:             else
   14:             {
   15:                 ConfigureService(_ProjectWebService, ServicePaths.ProjectService, true);
   16:             }
   17:         }
   18:  
   19:         return _ProjectWebService;
   20:     }
   21:     set { _ProjectWebService = value; }
   22: }

 

Each property is lazy-loaded to improve performance.  Thusly, the first time the property is accessed, the _ProjectWebService (a proxy class) is created and initialized.  We will touch on proxy classes in a minute, but first lets look at the calls to ConfigureService on lines 11 and 15.  The parent object is inspected to see if the the current ProjectServer instance is being used in an impersonated call.  Impersonation, as the name indicates, allows for the call to execute in the context of a specific user and requires that local web service paths be used (impersonating code can only be executed on the server machine).  The local web service paths take the form https://server:port/shared service provider/psi/webservice.asmx.  In the case of our example, we are not using impersonation so we go with the remote service paths, which take the form https://server/projectwebaccess/_vti_bin/psi/webservice.asmx.  So, with that backgrounder, lets take a look at ConfigureService:

    1: /// <summary>
    2: /// Given a service, configure the URL and security properties
    3: /// </summary>
    4: /// <param name="service">The service to configure</param>
    5: /// <param name="servicePath">The service path to append to the _Parent's Site property.</param>
    6: /// <param name="checkImpersonation">True if impersonating to resolve base url to SSP or site URL</param>
    7: private void ConfigureService(HttpWebClientProtocol service,
    8:                               string servicePath,
    9:                               bool checkImpersonation)
   10: {
   11:  
   12:     service.Credentials = CredentialCache.DefaultNetworkCredentials;
   13:  
   14:     if (_Parent.IsImpersonating && checkImpersonation)
   15:     {
   16:         service.Url = AppendPath(_Parent.SPPBaseURL, servicePath);
   17:         service.UseDefaultCredentials = false;
   18:     }
   19:     else
   20:     {
   21:         service.Url = AppendPath(_Parent.Site.OriginalString, servicePath);
   22:         service.UseDefaultCredentials = true;
   23:     }
   24:  
   25:     switch (_Parent.AuthenticationType)
   26:     {
   27:         case AuthenticationType.Windows:
   28:             break;
   29:         case AuthenticationType.Forms:
   30:             service.CookieContainer = _Parent.CookieContainer;
   31:             break;
   32:         default:
   33:             throw new ArgumentOutOfRangeException(LibraryResources.InvalidAuthenticationType);
   34:     }
   35: }

Note that the inbound service parameter in line 7 is of type HttpClientProtocol, which is the base class for all PSI web services.   The method configures the web service according to the impersonation settings and the inbound service path.  For reasons lost to me at this point, the method is actually rather convoluted and needs work but rather than fix it while I blog, I think I need to study why I wrote it that way first!  This is one of the best things about blogging your own code: it is a self-analysis/code review for free!

imageAs I mentioned earlier, proxy classes are next on our list.  First, when you connect to a .NET XML web service using Visual Studio, the WSDL service description is used to a create a proxy class.  These proxies are obscured in Visual Studio’s Solution Explorer, as noted in the image left, but you can easily access the source through File Explorer.  In mpFx, the proxy is derived to support impersonation, which requires that an impersonation context be set and the GetWebRequest method must be overridden.  Here is the ProjectProxy class:

 

 

 

 

 

 

    1: using System;
    2: using System.Net;
    3: using Microsoft.Office.Project.Server.Library;
    4: using Project=CodePlex.MicrosoftProject.mpFx.ProjectsWebService.Project;
    5:  
    6: namespace CodePlex.MicrosoftProject.mpFx.Proxies
    7: {
    8:     public class ProjectProxy : Project
    9:     {
   10:         public string ContextString { get; set; }
   11:  
   12:         protected override WebRequest GetWebRequest(Uri uri)
   13:         {            
   14:             WebRequest webRequest = base.GetWebRequest(uri);
   15:  
   16:             if (ContextString != String.Empty)
   17:             {
   18:                 webRequest.UseDefaultCredentials = true;
   19:                 webRequest.Credentials = CredentialCache.DefaultNetworkCredentials;
   20:                 webRequest.Headers.Add("PjAuth", ContextString);
   21:                 webRequest.PreAuthenticate = true;
   22:             }
   23:             return webRequest;
   24:         }
   25:  
   26:         public void SetImpersonationContext(bool isWindowsUser, String userNTAccount, Guid userGuid, Guid trackingGuid, Guid siteId, String lcid)
   27:         {
   28:             ContextString = GetImpersonationContext(isWindowsUser, userNTAccount, userGuid, trackingGuid, siteId, lcid);
   29:         }
   30:  
   31:         private static String GetImpersonationContext(bool isWindowsUser, String userNTAccount, Guid userGuid, Guid trackingGuid, Guid siteId, String lcid)
   32:         {
   33:             PSContextInfo contextInfo = new PSContextInfo(isWindowsUser, userNTAccount, userGuid, trackingGuid, siteId, lcid);
   34:             String contextString = PSContextInfo.SerializeToString(contextInfo);
   35:             return contextString;
   36:         }
   37:     }
   38: }

 

In an impersonated call, the proxy’s impersonation context is set by calling the SetImpersonationContext on line 26.  In an event, this context is an inbound parameter to the event receiver.  In other situations, you must craft this context on your own.

Okay, back to our sample.  The next thing is the EntityFactory, which offers clean semantics for instantiating the most common of PSI objects.  In our sample, we are calling a simple overloaded method CreateProject:

    1: /// <summary>
    2: /// Create project row with name 
    3: /// </summary>
    4: /// <param name="name">Project Name</param>
    5: /// <returns></returns>
    6: public static ProjectDataSet NewProject(string name)
    7: {
    8:     ProjectDataSet project = NewProject();
    9:  
   10:     project.Project[0].PROJ_NAME = name;
   11:  
   12:     return project;
   13: }

As you can see, it is very simple but in the sample it makes it much more readable.

Okay, that’s it for this evening.  Stay tuned for part II where we get in a little deeper.