MpFx Walkthrough: Creating the ProjectServer Object & Enumerating Project Information

I am taking a little break from my current project.  Whew. Only such much you can cram into your brain about SharePoint in one unbroken period of time.

Today’s topic starts at the beginning of mpFx:  How to create a ProjectServer object.  I will also demonstrate enumerating project information.  The code sample and an update to the core mpFx library are available on Code Gallery.  Here is the updated mpFx core library and here is the walkthrough.

The following references are required to build and execute:

  • Mcs.Epm.MicrosoftProject.mpFx
  • System
  • System.Core
  • System.Data
  • System.Xml

NOTE: mpFx 1.0 PREVIEW is intended to be a learning aide.  There are problems with parts of the code related to resource acquisition and clean up, performance, and generally some design decisions I made well before I had a full understanding of what the PSI was up to.  I am slowly purging those areas that need purging…

NOTE II: Parts of the PREVIEW edition implement IDisposable where it isn’t really a benefit.  DataSets implement IDispose as part of MarshalByValueComponent, but the source of IDisposable in this case is IComponent!    It is recommended that you call Dispose on any object the implements it, but in this case I can’t see any reason to.  No resources are freed as part of the call, as far as I can tell.

Okay, on to the sample!

Walkthrough

Let’s begin with the sample:

    1: using System;
    2: using System.IO;
    3: using Mcs.Epm.MicrosoftProject.mpFx;
    4: using Mcs.Epm.MicrosoftProject.mpFx.ProjectsWebService;
    5:  
    6: namespace CreateProjectServerObject
    7: {
    8:     class Program
    9:     {
   10:         static void Main()
   11:         {
   12:             /*  There are four constructors for the ProjectServer object.  This demo describes 
   13:              *  the constructor most commonly used in my projects: 
   14:              *  
   15:              *  ProjectServer(string projectServerUrl, DataStoreEnum store, ILog log)
   16:              *  
   17:              *  The constructor takes three parameters:
   18:              *  
   19:              *  1.) projectServerUrl: The full path to the Project Server PWA site
   20:              *  2.) DataStorEnum: The store on which operations will be performed
   21:              *  3.) ILog: An object implementing the ILog interface, which 
   22:              *      performs logging operations.
   23:              *      
   24:              *  At the time of writing, a single class implements ILog and implements
   25:              *  logging to the file system.  Other classes might implement ILog and 
   26:              *  persist events to the Event Log or a database.            
   27:              */
   28:  
   29:             string logDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "MpFx");
   30:  
   31:             try
   32:             {
   33:                 using (Log log = new Log(logDirectory, "Demo", LogPeriod.Hourly, true))
   34:                 using (ProjectServer projectServer = new ProjectServer("https://projectserver/pwa", DataStoreEnum.WorkingStore, log))
   35:                 {
   36:                     ProjectCollection projects = projectServer.Projects;
   37:  
   38:                     foreach (EnterpriseProject project in projects)
   39:                     {
   40:                         string projectInformation = string.Format("Project Name: {0} - Start Date: {1}",
   41:                                                                   project.Name,
   42:                                                                   EnterpriseProject.StandardInfo(project).PROJ_INFO_START_DATE);
   43:                                                 
   44:                         Console.WriteLine(projectInformation);                     
   45:                     }
   46:                 }
   47:             }
   48:             catch (MpFxException exception)
   49:             {
   50:                 Console.Write(Errors.ProcessMpFxException(exception));
   51:             }
   52:             catch (Exception exception)
   53:             {
   54:                 Console.Write(exception.Message);
   55:             }
   56:  
   57:             Console.WriteLine("Projects enumerated  Press any key to close.");
   58:             Console.ReadKey();
   59:         }
   60:     }
   61: }

We will skip the details on logging for this post.  To begin, let’s look at one of the four constructors implemented on ProjectServer (I chose the one I most commonly use).  In the sample above, the constructor is called on line 34.  Here is the constructor source:

    1: public ProjectServer(string projectServerUrl, DataStoreEnum store, ILog log)
    2: {            
    3:     try
    4:     {
    5:         Log = log;
    6:  
    7:         if (!Utilities.IsValidUrl(projectServerUrl))
    8:         {
    9:             throw new ArgumentException(LibraryResources.InvalidProjectServerUrl);
   10:         }
   11:  
   12:         Settings = new ProjectServerSettings();
   13:         WebServices = new WebServices(this);
   14:         Site = new Uri(projectServerUrl);
   15:         Store = store;
   16:  
   17:         NetworkCredential = CredentialCache.DefaultNetworkCredentials;
   18:         AuthenticationType = AuthenticationType.Windows;
   19:  
   20:         WebServices.LoginWindows = new LoginWindows();
   21:  
   22:         WebServices.LoginWindows.Url = WebServices.AppendPath(projectServerUrl, ServicePaths.WindowsLoginService);
   23:         WebServices.LoginWindows.UseDefaultCredentials = true;
   24:  
   25:         WriteLogEntry(LogArea.Constructor, 
   26:                       LogEntryType.Information, 
   27:                       string.Format(LibraryResources.LogConstructor, AuthenticationType, projectServerUrl));
   28:  
   29:         WebServices.LoginWindows.Login();
   30:  
   31:         Settings.ListSeparator = WebServices.Projects.ReadServerListSeparator();
   32:  
   33:     }
   34:     catch (SoapException exception)
   35:     {
   36:         throw MpFxException.Create(exception, Log, LogArea.Constructor, LogEntryType.Error);
   37:     }
   38:     catch (ArgumentException exception)
   39:     {
   40:         throw MpFxException.Create(exception, Log, LogArea.Constructor, LogEntryType.Error);
   41:     }
   42:     catch (Exception exception)
   43:     {
   44:         throw MpFxException.Create(exception, Log, LogArea.Constructor, LogEntryType.Error);
   45:     }
   46: }

Here are a few elaborations and considerations:

  • Lines 12-15:  mpFx attempts to build an object model from the disparate web services and data of the PSI.  From the ProjectServer object, you can directly access the underlying web services, by accessing the WebServices property (projectServer.WebServices.[webServiceName]) or you can access the encapsulation objects and use the helper methods I have created.  For example, to create a project you can populate a ProjectDataSet object, directly access the project web service, call QueueCreateProject, wait on the job, and deal with the potential exceptions yourself, or you can call projectServer.Projects.Create, which looks like this:
    1: /// <summary>
    2: /// Create a project in Project Server.
    3: /// </summary>
    4: /// <param name="project">Project</param>
    5: /// <param name="validateOnly">Indicates whether the operation should validate only or perform the creation</param>
    6: /// <param name="wait">Indicates whether the call should wait on the queued job.</param>
    7: /// <returns>Queue job GUID</returns>
    8: public Guid Create(ProjectDataSet project, bool validateOnly, bool wait)
    9: {
   10:    try
   11:    {
   12:        Guid jobGuid = Guid.NewGuid();
   13:  
   14:        Parent.WebServices.Projects.QueueCreateProject(jobGuid, project, validateOnly);                
   15:  
   16:        if (wait)
   17:        {
   18:            string errorMessage;
   19:  
   20:            Parent.Queue.WaitOnJobStatus(jobGuid,
   21:                                         JobState.Success,
   22:                                         Parent.Settings.QueueStatusRetryCount,
   23:                                         Parent.Settings.QueueStatusSleepDuration,
   24:                                         out errorMessage);
   25:  
   26:            MpFxException.ThrowIfError(errorMessage, Parent.Log, LogArea.CreateProject, LogEntryType.Error);                    
   27:            
   28:        }
   29:  
   30:        return jobGuid;
   31:  
   32:    }
   33:    catch (SoapException exception)
   34:    {
   35:        throw MpFxException.Create(exception, Parent.Log, LogArea.CreateProject, LogEntryType.Error);
   36:    }
   37:    catch (Exception exception)
   38:    {
   39:        throw MpFxException.Create(exception, Parent.Log, LogArea.CreateProject, LogEntryType.Error);
   40:    }
   41: }
  • The particular constructor we used in the sample uses Windows authentication when communicating with the PSI.  NOTE: Forms authentication has NOT been tested with mpFx 1.0 PREVIEW

Returning to the sample:

   36:                     ProjectCollection projects = projectServer.Projects;

The Projects property returns a ProjectCollection object. Looking at the property implementation uncovers another design decision:

    1: public ProjectCollection Projects
    2: {
    3:     get
    4:     {
    5:         if (_ProjectCollection == null)
    6:         {
    7:             _ProjectCollection = new ProjectCollection(this);
    8:         }
    9:  
   10:         return _ProjectCollection;
   11:     }
   12: }

The lazy load pattern is widely employed (with various degrees of success and cleanliness!) throughout mpFx.   Examining the internals of the next line of code further illustrates this point:

   38:                     foreach (EnterpriseProject project in projects)

Let’s take a look at the enumerator implementation.  First, the ProjectCollection implements two IEnumerables:

    1: public class ProjectCollection : IEnumerable<EnterpriseProject>, IEnumerable<Guid>

The first enumerates EnterpriseProjects (we will get to this later) and the second enumerates Guids.  Let’s take a look at the EnterpriseProject enumerator, which is the one used in the sample:

    1: public IEnumerator<EnterpriseProject> GetEnumerator()
    2: {
    3:     return ((IEnumerable<EnterpriseProject>)this).GetEnumerator();
    4: }

And:

    1: IEnumerator<EnterpriseProject> IEnumerable<EnterpriseProject>.GetEnumerator()
    2: {
    3:     if (_projectsCollection == null)
    4:     {
    5:         LoadProjectCollection();
    6:     }
    7:  
    8:     ThrowLoadCollectionLoadException();
    9:  
   10:     foreach (KeyValuePair<Guid, EnterpriseProject> pair in _projectsCollection)
   11:     {
   12:         yield return pair.Value;
   13:     }
   14: }

Note that the backing field is first checked for nullness.  If it is null, the project collection is loaded:

    1: internal void LoadProjectCollection()
    2: {
    3:     if (_projectsCollection == null)
    4:     {
    5:         _projectsCollection = new Dictionary<Guid, EnterpriseProject>();
    6:     }
    7:     else
    8:     {
    9:         _projectsCollection.Clear();
   10:     }
   11:  
   12:     using (ProjectDataSet projectDataSet = Parent.WebServices.Projects.ReadProjectStatus(Guid.Empty, Parent.Store, string.Empty, (int)Project.ProjectType.Project))
   13:     {
   14:         foreach (ProjectDataSet.ProjectRow project in projectDataSet.Project.Rows)
   15:         {
   16:             if (project.PROJ_UID != Guid.Empty)
   17:             {
   18:                 _projectsCollection.Add(project.PROJ_UID, new EnterpriseProject(this, project.PROJ_UID, project.PROJ_NAME, Parent.Store));
   19:             }
   20:         }
   21:     }
   22: }

imagePretty straightforward so far.  The ProjectCollection contains EnterpriseProjects.  Rather than build a full model of all of the ProjectDataSet.ProjectRow members, it exposes data and helper methods.  See the class diagram to the left.

The properties and methods that are currently implement reflect the learning requirements I experienced while developing mpFx in the early days.  Essentially, every time I needed to read or act on project data in a new way, I would implement a property or method appropriate to the requirement. 

Returning to the sample:

   40:                         string projectInformation = string.Format("Project Name: {0} - Start Date: {1}",
   41:                                                                   project.Name,
   42:                                                                   EnterpriseProject.StandardInfo(project).PROJ_INFO_START_DATE);
   43:                                                 

The project name is one of a few properties that wrap elements of the ProjectDataSet.ProjectRow data row class.  The helper method EnterpriseProject.StandardInfo is short hand for project.StandardInformation.Project[0].   Accessing the StandardInformation property causes a call to be made to Project Server:

    1: public ProjectDataSet StandardInformation
    2: {
    3:     get
    4:     {
    5:         return Parent.Parent.WebServices.Projects.ReadProjectEntities(ProjectGuid, (int)ProjectEntityType.Project, Parent.Parent.Store);
    6:     }
    7: }

That’s pretty much it!

More later.  Back to SharePoint.