This is a long time coming, prompted by a question on the forums http://social.technet.microsoft.com/Forums/en-US/project2010custprog/thread/94af5d03-2606-4899-90aa-d17dddc3829b and a promise I made in my blog from 2007 http://blogs.msdn.com/b/brismith/archive/2007/12/06/setting-custom-field-values-using-the-psi.aspx.
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 http://msdn.microsoft.com/en-us/library/ms455600(office.12).aspx. 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 http://blogs.msdn.com/b/brismith/archive/2010/08/12/project-server-flag-fields-why-can-t-i-make-them-required.aspx).
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; try { WebSvcProject.ProjectDataSet dsProject = new WebSvcProject.ProjectDataSet(); WebSvcProject.ProjectDataSet.ProjectRow projectRow = dsProject.Project.NewProjectRow(); Guid projectGuid = Guid.NewGuid(); projectRow.PROJ_UID = projectGuid; projectRow.PROJ_NAME = this.txtProjectName.Text; projectRow.PROJ_TYPE = Convert.ToInt32(PSLibrary.Project.ProjectType.Project); dsProject.Project.AddProjectRow(projectRow); //Adding a row for my first Project CF - ProjCost WebSvcProject.ProjectDataSet.ProjectCustomFieldsRow cfRowCost = dsProject.ProjectCustomFields.NewProjectCustomFieldsRow(); 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 = dsProject.ProjectCustomFields.NewProjectCustomFieldsRow(); 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 = dsProject.ProjectCustomFields.NewProjectCustomFieldsRow(); 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 dsProject.ProjectCustomFields.AddProjectCustomFieldsRow(cfRowCost); dsProject.ProjectCustomFields.AddProjectCustomFieldsRow(cfRowDate); dsProject.ProjectCustomFields.AddProjectCustomFieldsRow(cfRowText); //Now for the Task custom fields. First I will add a Task WebSvcProject.ProjectDataSet.TaskRow taskRow = dsProject.Task.NewTaskRow(); Guid taskGuid = Guid.NewGuid(); taskRow.PROJ_UID = projectGuid; taskRow.TASK_UID = taskGuid; taskRow.TASK_NAME = "My Task"; dsProject.Task.AddTaskRow(taskRow); // And add some custom fields to my task // First a text field not based on a lookup table WebSvcProject.ProjectDataSet.TaskCustomFieldsRow cfRowTaskText = dsProject.TaskCustomFields.NewTaskCustomFieldsRow(); 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 = dsProject.TaskCustomFields.NewTaskCustomFieldsRow(); 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 = dsProject.TaskCustomFields.NewTaskCustomFieldsRow(); 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 = dsProject.TaskCustomFields.NewTaskCustomFieldsRow(); 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 dsProject.TaskCustomFields.AddTaskCustomFieldsRow(cfRowTaskText); dsProject.TaskCustomFields.AddTaskCustomFieldsRow(cfRowTaskFlag); dsProject.TaskCustomFields.AddTaskCustomFieldsRow(cfRowTaskNumber); dsProject.TaskCustomFields.AddTaskCustomFieldsRow(cfRowTaskDuration); 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. System.Threading.Thread.Sleep(3000); 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; else 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, MessageBoxIcon.Error); } catch (WebException ex) { string message = ex.Message.ToString() + "\n\nLog 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; } else { 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!