Project Server: Adding Custom Fields to Projects and Tasks using the PSI

This is a long time coming, prompted by a question on the forums and a promise I made in my blog from 2007

This was created targeting  Project Server 2007, but there should be no issues using the exact same approach with Project Server 2010.

I have built this around the SDK LoginDemo sample, and just added to the btnCreateProject_Click method.  The sample just creates a project – see  I have added a task, and both Project and Task level custom fields.  Across the project and task levels I have used each type of custom field to show which values need setting.  This is just to create a project – obviously adding or changing them later is very similar.

In this example I created 3 project level custom fields – cost, date and a text one based on a lookup table.  For the task I created 4 custom fields – flag, number, duration and text.  I made them all required just to validate my code – except the Flag which will always have a value and therefore does not have the option to set to required (see

For simplicity (and laziness) I have hard coded many of the GUIDs I am using.  For the Custom Fields I copied these from the URLs when in the edit screen for custom fields.  I got the lookup table value GUID in the ‘View Source’ page when editing the lookup table.  Obviously you could also get these from the database.  In a real situation you would use the Custom Field and Lookup Table web services.  Obviously your GUIDs will be different from mine.

private void btnCreateProject_Click(object sender, EventArgs e)
            string projectCreatedLabel = "Project created!";
            string wssUrl;
            string projectWorkspace = ResetWorkspaceUrl();
            bool created = false;

            // GUIDs for my CFs etc - in practice you would use the CF and LU PSI calls
            Guid projCostGuid = new Guid("e672bd64-c535-4486-bc64-8f4999547390");
            Guid projDateGuid = new Guid("3dafa5d4-9473-4781-9503-aafe207a71bb");
            Guid projTextGuid = new Guid("08758857-1a69-4efb-a657-27ed61d7d7c3");
            Guid taskTextGuid = new Guid("30665299-bc21-4c51-b954-220d407ba47e");
            Guid taskFlagGuid = new Guid("3658a81d-2e64-436f-afb9-970b778954b1");
            Guid taskNumberGuid = new Guid("d5c0318c-06cc-4d82-a891-7f3ea43503ab");
            Guid taskDurationGuid = new Guid("58519124-7e1d-44b8-b14c-7435408f02e7");
            Guid colourLUValueRed = new Guid("5b730bab-7212-4ffb-823d-60aef0df1fff");

            lblProjectCreated.Text = "";
            lblWorkspaceUrl.Text = projectWorkspace;
            this.Cursor = Cursors.WaitCursor;

                WebSvcProject.ProjectDataSet dsProject = 
                    new WebSvcProject.ProjectDataSet();
                WebSvcProject.ProjectDataSet.ProjectRow projectRow = 

                Guid projectGuid = Guid.NewGuid();
                projectRow.PROJ_UID = projectGuid;
                projectRow.PROJ_NAME = this.txtProjectName.Text;
                projectRow.PROJ_TYPE = 


                //Adding a row for my first Project CF - ProjCost

                WebSvcProject.ProjectDataSet.ProjectCustomFieldsRow cfRowCost = 

                cfRowCost.PROJ_UID = projectGuid;
                // The Custom_Field_UID is the unique identifier for each custom field row
                cfRowCost.CUSTOM_FIELD_UID = Guid.NewGuid();
                // The MD_PROP_UID identifies the specific custom field
                cfRowCost.MD_PROP_UID = projCostGuid;
                // Cost custom fields have their value set in NUM_VALUE
                // The value entered is decimal and 100 times the actual cost - 5000 = $50 in my case
                cfRowCost.NUM_VALUE = 5000;

               //Adding a row for my second Project CF - ProjDate

                WebSvcProject.ProjectDataSet.ProjectCustomFieldsRow cfRowDate =

                cfRowDate.PROJ_UID = projectGuid;
                cfRowDate.CUSTOM_FIELD_UID = Guid.NewGuid();
                cfRowDate.MD_PROP_UID = projDateGuid;
                // Date custom fields have their values set in DATE_VALUE as a DateTime data type
                cfRowDate.DATE_VALUE = DateTime.Parse("Aug 25, 2010 10:45:00 PM");

                //Adding a row for my third Project CF - ProjText, which is based on a Lookup Table

                WebSvcProject.ProjectDataSet.ProjectCustomFieldsRow cfRowText =

                cfRowText.PROJ_UID = projectGuid;
                cfRowText.CUSTOM_FIELD_UID = Guid.NewGuid();
                cfRowText.MD_PROP_UID = projTextGuid;
                // Custom fields based on a lookup table have the GUID that identifies the row in
                // the lookup table entered against CODE_VALUE.  See the LT_STRUCT_UID from the 
                // LookupTable web service
                cfRowText.CODE_VALUE = colourLUValueRed;

                //Adding my CFRows to the dataset


                //Now for the Task custom fields.  First I will add a Task

                WebSvcProject.ProjectDataSet.TaskRow taskRow =

                Guid taskGuid = Guid.NewGuid();

                taskRow.PROJ_UID = projectGuid;
                taskRow.TASK_UID = taskGuid;
                taskRow.TASK_NAME = "My Task";


                //  And add some custom fields to my task

                // First a text field not based on a lookup table
                WebSvcProject.ProjectDataSet.TaskCustomFieldsRow cfRowTaskText =

                cfRowTaskText.PROJ_UID = projectGuid;
                // For our Task CF rows we need the Task UID as well as the Project UID
                cfRowTaskText.TASK_UID = taskGuid;
                cfRowTaskText.CUSTOM_FIELD_UID = Guid.NewGuid();
                cfRowTaskText.MD_PROP_UID = taskTextGuid;
                // As we have no lookup table for this text field the value goes in TEXT_VALUE
                cfRowTaskText.TEXT_VALUE = "My Text Value";

                // Next a Flag field
                WebSvcProject.ProjectDataSet.TaskCustomFieldsRow cfRowTaskFlag =

                cfRowTaskFlag.PROJ_UID = projectGuid;
                cfRowTaskFlag.TASK_UID = taskGuid;
                cfRowTaskFlag.CUSTOM_FIELD_UID = Guid.NewGuid();
                cfRowTaskFlag.MD_PROP_UID = taskFlagGuid;
                // Flags are a bool, so expect true or false - they default to false
                // Also Flags cannot be made required, as they will always have a value anyway
                // They are entered against FLAG_VALUE
                cfRowTaskFlag.FLAG_VALUE = true;

                // Next a Number field
                WebSvcProject.ProjectDataSet.TaskCustomFieldsRow cfRowTaskNumber =

                cfRowTaskNumber.PROJ_UID = projectGuid;
                cfRowTaskNumber.TASK_UID = taskGuid;
                cfRowTaskNumber.CUSTOM_FIELD_UID = Guid.NewGuid();
                cfRowTaskNumber.MD_PROP_UID = taskNumberGuid;
                // Numbers are decimal - the M suffix is used as this value would normally default to double
                // They are entered against NUM_VALUE
                cfRowTaskNumber.NUM_VALUE = 25.6M;

                // Finally a duration field
                WebSvcProject.ProjectDataSet.TaskCustomFieldsRow cfRowTaskDuration =

                cfRowTaskDuration.PROJ_UID = projectGuid;
                cfRowTaskDuration.TASK_UID = taskGuid;
                cfRowTaskDuration.CUSTOM_FIELD_UID = Guid.NewGuid();
                cfRowTaskDuration.MD_PROP_UID = taskDurationGuid;
                // Durations have a format for their display which is best enumerated as below
                // They are entered against NUM_VALUE
                cfRowTaskDuration.DUR_FMT = (int)PSLibrary.Task.DurationFormat.Day; 
                // Duration is indicated in tenths of minutes. A value of 100 indicates 10 minutes
                cfRowTaskDuration.DUR_VALUE = 24000;

                // Add my custom field rows to the dataset



                Guid jobGuid = Guid.NewGuid();
                bool validateOnly = false;
                // Create and save project to the Draft db
                project.QueueCreateProject(jobGuid, dsProject, validateOnly);

                // Wait 3 seconds (more or less) for Queue job to complete.
                // Or, add a routine that checks the QueueSystem for job completion.

                WebSvcProject.ProjectRelationsDataSet dsProjectRelations =
                    new WebSvcProject.ProjectRelationsDataSet();
                jobGuid = Guid.NewGuid();

                // Set wssUrl = "" to have default WSS project workspace, or null to have no workspace.
                if (chkDefaultWorkspace.Checked)
                    wssUrl = "";
                else if (projectWorkspace == "")
                    wssUrl = null;
                    wssUrl = projectWorkspace;

                bool fullPublish = true;
                // Publishes project to the Published db
                dsProjectRelations = project.QueuePublish(jobGuid, projectGuid, fullPublish, wssUrl);
                created = true;
            catch (SoapException ex)
                string errMess = "";
                // Pass the exception to the PSClientError constructor to get 
                // all error information.
                PSLibrary.PSClientError error = new PSLibrary.PSClientError(ex);
                PSLibrary.PSErrorInfo[] errors = error.GetAllErrors();

                for (int j = 0; j < errors.Length; j++)
                    errMess += errors[j].ErrId.ToString() + "n";
                errMess += "n" + ex.Message.ToString();

                MessageBox.Show(errMess, "Error", MessageBoxButtons.OK,
            catch (WebException ex)
                string message = ex.Message.ToString() +
                    "nnLog on, or check the Project Server Queuing Service";
                MessageBox.Show(message, "Project Creation Error", 
                    MessageBoxButtons.OK, MessageBoxIcon.Error);
            this.Cursor = Cursors.Default;
            if (created)
                lblProjectCreated.ForeColor = Color.Green;
                lblWorkspaceUrl.Visible = true;
                lblWorkspaceLabel.Visible = true;
                projectCreatedLabel = "Project not created";
                lblProjectCreated.ForeColor = Color.Red;
                lblWorkspaceUrl.Visible = false;
                lblWorkspaceLabel.Visible = true;
            lblProjectCreated.Text = projectCreatedLabel;
            lblProjectCreated.Visible = true;

I hope this was worth waiting for!

Technorati Tags: ,,