TechEd Preview – Part 3 – Project Server 2007 Provisioning Project Specific Workspaces automatically

First day of TechEd today and despite the 3hr time difference from Seattle I still woke up early.  This posting will be part of the subject of the chalk talk I am delivering with Boris Scholl tomorrow (6/4) morning.  If any of you remember the Solution Accelerator for Six Sigma this is part inspired by some custom code that was in that solution – and is designed to allow a Project Manager to select a workspace type from a list in a custom project field – and then when she publishes that type of site will be provisioned from a pre-built template.

The first part of this is to create some templates for new workspaces.  This is covered in the SDK, but briefly you must base any new template on an unlinked site, then you save as a template, then from the template gallery save to a file (*.stp) and then you use stsadm –o addtemplate to bring the template back in so it can be used for provisioning (and a final IIS reset).  You will also need to use the command stsadm –o enumtemplates to get the internal name (something like _GLOBAL_#2) for each of your templates for later use.

The next step is creating a lookup table to hold the template details – and it should look something like this:-

image

 

I am using the description field to hold the internal name of the template as that is the one used when requesting a site to be provisioned.  You could resolve this in code somewhere – it just seemed easier to me to put it here. You also need to create a Project Level custom field that references this lookup table – and in my example I use the “NotYet” value to mean don’t create a site for me.  The names of the field/table aren’t important – but you will need to know the Guid associated with them.  Also you will need to turn off automatic site creation – as we will be controlling this via an event handler.  I am using Red/Green/Blue for the visual recognition for the demo – obviously ISO 9001, Six Sigma, Administration m- would be more useful types of templates to use.

Now the event handler.  I’ve thrown a few comments in and also it logs to the application event log the different actions it can take.  It could certainly do with more (read some) exception handling… but it does the job.  Based on the custom field requested it will get the template name from the lookup table description.  If a site already exists it will stop – and if the requested site is already in use it will stop.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Net;
using Microsoft.Office.Project.Server.Events;
using PSLibrary = Microsoft.Office.Project.Server.Library;


namespace TechEdEventHandler
{
    public class MyPublishedEventHandler : ProjectEventReceiver
    {
        private const string URLPREFIX = "http://";
        private const string LOGINWINDOWSWEBSERVICE = "_vti_bin/PSI/LoginWindows.asmx";
        private const string PROJECTWEBSERVICE = "_vti_bin/PSI/Project.asmx";
        private const string WSSINTEROPWEBSERVICE = "_vti_bin/PSI/WSSInterop.asmx";
        private const string LOOKUPTABLEWEBSERVICE = "_vti_bin/PSI/LookupTable.asmx";
        private string baseUrl = "http://brismithwfe/cal/";
        private const int EVENTID = 9191;
        
        private string webTemplateName;
        private Guid projectGuid;
        private Guid codeValue;
        const int PROJECT_CUSTOM_FIELDS_ENTITY_TYPE = 32;
        // We need the Guids for the lookup table and custom field - better practice would be to use a settings file
        private Guid workspaceTemplateLUTGuid = new Guid("b0fc9d01-b716-43d3-aa74-335a3297214b");
        private Guid workspaceTemplateCFGuid = new Guid("98c9444e-1fa2-4f12-ab2b-4cb7e1818738");

        private static WebSvcLoginWindows.LoginWindows loginWindows =
            new WebSvcLoginWindows.LoginWindows();
        private static WebSvcProject.Project project =
            new WebSvcProject.Project();
        private static WebSvcWssInterop.WssInterop wssInterop =
            new WebSvcWssInterop.WssInterop();
        private static WebSvcLookupTable.LookupTable lookupTable =
            new WebSvcLookupTable.LookupTable();

        public override void OnPublished(Microsoft.Office.Project.Server.Library.PSContextInfo contextInfo, ProjectPostPublishEventArgs e)
        {
            string eventName = "Auto Workspace Creation - OnPublished";
            
            loginWindows.Url = baseUrl + LOGINWINDOWSWEBSERVICE;
            loginWindows.Credentials = CredentialCache.DefaultCredentials;
            project.Url = baseUrl + PROJECTWEBSERVICE;
            project.Credentials = CredentialCache.DefaultCredentials;
            lookupTable.Url = baseUrl + LOOKUPTABLEWEBSERVICE;
            lookupTable.Credentials = CredentialCache.DefaultCredentials;
            wssInterop.Url = baseUrl + WSSINTEROPWEBSERVICE;
            wssInterop.Credentials = CredentialCache.DefaultCredentials;

            projectGuid = e.ProjectGuid;

            WebSvcWssInterop.WssServersDataSet dsWssServersDataSet = 
                new WebSvcWssInterop.WssServersDataSet();

            WebSvcWssInterop.WssSettingsDataSet dsWssSettingsDataSet =
                new WebSvcWssInterop.WssSettingsDataSet();

            WebSvcWssInterop.ProjectWSSInfoDataSet dsProjectWSSInfoDataSet =
                new WebSvcWssInterop.ProjectWSSInfoDataSet();

            // If rowcount is greater than zero then a site already exists for the project - so exit
            dsProjectWSSInfoDataSet = wssInterop.ReadWssData(projectGuid);
            int rowCount = dsProjectWSSInfoDataSet.ProjWssInfo.Rows.Count;
            if (rowCount == 0)
            {

                WebSvcProject.ProjectDataSet dsProject =
                    new WebSvcProject.ProjectDataSet();
                // We are getting the project dataset - but only for the entity type 32 - custom fields
                dsProject = project.ReadProjectEntities(projectGuid, PROJECT_CUSTOM_FIELDS_ENTITY_TYPE, WebSvcProject.DataStoreEnum.WorkingStore);
                // The we look for the workspace template field - and get the code value
                foreach (WebSvcProject.ProjectDataSet.ProjectCustomFieldsRow rowProjectCF in dsProject.ProjectCustomFields)
                    if (rowProjectCF.MD_PROP_UID == workspaceTemplateCFGuid)
                    {
                        codeValue = rowProjectCF.CODE_VALUE;
                    }
                // Using the Guid for the lookup table we search for the code value 
                Guid[] ltUidList = new Guid[] { workspaceTemplateLUTGuid };
                WebSvcLookupTable.LookupTableDataSet dsLookupTable =
                    new WebSvcLookupTable.LookupTableDataSet();
                dsLookupTable = lookupTable.ReadLookupTablesByUids(ltUidList, false, 1033);
                // the code value gets us to the description which we use as the webtemplatename
                foreach (WebSvcLookupTable.LookupTableDataSet.LookupTableTreesRow rowLookupTable in dsLookupTable.LookupTableTrees)
                    if (rowLookupTable.LT_STRUCT_UID == codeValue)
                    {
                        webTemplateName = rowLookupTable.LT_VALUE_DESC;
                    }
                dsWssServersDataSet = wssInterop.ReadWssServerInfo();
                dsWssSettingsDataSet = wssInterop.ReadWssSettings();

                Guid wssServerUid = dsWssSettingsDataSet.WssAdmin[0].WADMIN_CURRENT_STS_SERVER_UID;
                string wssWebFullUrl = dsWssServersDataSet.WssServers.FindByWSTS_SERVER_UID(wssServerUid).WSS_SERVER_URL
                    + "/" + dsWssSettingsDataSet.WssAdmin[0].WADMIN_DEFAULT_SITE_COLLECTION
                    + "/" + e.ProjectName.ToString();

                int webTemplateLcid = 1033;
                // If site exists then we cannot create it for this project...
                bool SiteExists = wssInterop.WSSWebExists(wssWebFullUrl);
                if (!SiteExists)
                {
                    //NotYet means we will create it later
                    if (webTemplateName != "NotYet")
                    {   
                        // This is where we create a site if we need one
                        wssInterop.CreateWssSite(projectGuid, wssServerUid, wssWebFullUrl, webTemplateLcid, webTemplateName);
                        
                        string msg = string.Format("{0}: Site {1} created for Project {2}", eventName, wssWebFullUrl, e.ProjectName);
                        WriteEvent(msg, EventLogEntryType.SuccessAudit , EVENTID);
                    }

                    else
                    {
                        string msg = string.Format("{0}: No site required at this time for project {1}", eventName, e.ProjectName);
                        WriteEvent(msg, EventLogEntryType.Information, EVENTID);
                    }


                }
                else
                {
                    string msg = string.Format("{0}: Site {1) already exists", eventName, wssWebFullUrl);
                    WriteEvent(msg, EventLogEntryType.Error, EVENTID);
                }

            }
            else
            {
                string msg = string.Format("{0}: Workspace already created", eventName);
                WriteEvent(msg, EventLogEntryType.Information, EVENTID);
            }
            
        }
        
    private void WriteEvent(string msg, EventLogEntryType logEntryType, int eventId)
        {
            EventLog myLog = new EventLog();
            myLog.Source = "Project Event Handler";

            string message = msg;
            myLog.WriteEntry(msg, logEntryType, eventId);
        }
    }
}

Enjoy!