I have already done one post on integrating with project server. Since then I have had to get much more involved with it and have discovered it's a bit of a dark art to say the least. Hopefully, this post will serve as a useful resource to anyone else who needs to do the same and stop them from having to go through all of the pain that I went through.
First off, the documentation is there if you search hard enough for it. At the end of this post is a list of resources that proved to be useful when I was carrying out this development - most of them are links to MSDN. The SDK is pretty useful as well although I did struggle to get the main (projtool) project running at first. Basically there is a small bug (in my opinion) in it, which means that if you select to login using windows authentication then it just uses your default credentials. You should be able to specify the (windows) credentials you want to use. This was necessary for me as the project server I was developing against was one of those fabrikam instances, which therefore had users configured in the fabrikam domain and wouldn't let me add new users from my domain (i.e. me). Once I changed the code to use credentials of a fabrikam user (administrator) I got it working. Anyway, once you get into and start reading through the examples and other documentation you will realise that there isn't much to integrating with project server after all. You basically have to call web methods on a number of different web services (collectively known as PSI), these return (and take as parameters) datasets generally, which you need to manipulate and pass back to project server. Most of the main actions are carried out by calling sensibly named web methods, there are just a few gotcha's that you need to be aware of, the ones that got me I will try and cover in this post.
The web services
The list below is a list of the web services that I have used together with a brief explanation of what they are used for, or at least what I used them for. It is not a complete list, just the list of the ones I have made use of:
- LoginWindows: This web service is used to login to project server. That statement is a bit misleading as you still have to provide the same credentials that you use with this web service to all of the others. This web service does give you a handy way of checking the credentials are correct. You can also use it to logoff again. There is a corresponding web service that is used for forms authentication called LoginForms
- Project: The Project web service is used for dealing with things at a project level. For example, creating projects and checking them in and out.
- CustomFields:You can use the CustomFields web service for creating and searching for custom fields. You may need to do this to get a custom fields guid for setting the value of a custom field on a task or resource for example.
- Resource: If you need to do anything with the enterprise resource pool you will need to call methods on this web service.
- LookupTable: To get a list of values from a lookup table you will use the LookupTable web service.
- QueueSystem: A number of of the web methods that make up the PSI are asynchronous. These asynchronous methods take a job Guid which can then be used in conjunction with methods on the QueueSystem web service to determine (amongst other things) whether or not the job has completed.
PSI
The class below shows the structure of the class that I used to encapsulate the calls to the PSI web services. Creating an instance logs into project server with the LoginWindows web service (using credentials stored in web.config) and creates instances of all the other web services (well the proxies created by Visual Studio). The class implements the IDisposable interface; disposing of an instance of PSI will log off and dispose of all of the web service proxies created in the constructor. The constructor creates a new (session) guid which is used in conjunction with the session description passed into the constructor, which is required by some of the web methods.
public class PSI : IDisposable
{
#region Constructor
private Guid sessionId;
private String sessionDescr;
private LoginWindowsWebSvc.LoginWindows loginSvc;
private ProjectWebSvc.Project projectSvc;
private CustomFieldsWebSvc.CustomFields custFieldsSvc;
private ResourceWebSvc.Resource resourceSvc;
private LookupTableWebSvc.LookupTable lookupTableSvc;
private QueueSystemWebSvc.QueueSystem queueSvc;
private SharePointListWebSvc.Lists listSvc;
private WebClient webClient;
public PSI(String sessionDescr)
{
this.sessionId = Guid.NewGuid();
this.sessionDescr = sessionDescr;
// Currently this uses forms authentication, although this could easily be changed to use web
// based authentication
loginSvc = new LoginWindowsWebSvc.LoginWindows();
CredentialCache credentialCache = new CredentialCache();
credentialCache.Add(
new Uri(Settings.Default.PSI_DataAccess_BaseUri), "NTLM",
new NetworkCredential(
Settings.Default.PSI_DataAccess_Username,
Settings.Default.PSI_DataAccess_Password,
Settings.Default.PSI_DataAccess_Domain));
loginSvc.Credentials = credentialCache;
Login();
projectSvc = new ProjectWebSvc.Project();
projectSvc.Credentials = credentialCache;
custFieldsSvc = new CustomFields();
custFieldsSvc.Credentials = credentialCache;
resourceSvc = new ResourceWebSvc.Resource();
resourceSvc.Credentials = credentialCache;
lookupTableSvc = new LookupTableWebSvc.LookupTable();
lookupTableSvc.Credentials = credentialCache;
queueSvc = new QueueSystemWebSvc.QueueSystem();
queueSvc.Credentials = credentialCache;
listSvc = new SharePointListWebSvc.Lists();
listSvc.Credentials = credentialCache;
webClient = new WebClient();
webClient.Credentials = credentialCache;
}
/// <summary>
/// Logs in to project server (assumes the credentials will already have been set.
/// </summary>
private void Login()
{
try
{
if (!loginSvc.Login())
throw new PSIException("Failed to login to project server");
}
catch (SoapException ex)
{
throw new PSIException(InterpretException(ex), ex);
}
catch (WebException ex)
{
throw new PSIException(InterpretException(ex), ex);
}
}
#endregion
// Main functionality
#region IDisposable
~PSI()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public virtual void Dispose(bool disposing)
{
if (disposing)
{
Logoff();
loginSvc.Dispose();
projectSvc.Dispose();
custFieldsSvc.Dispose();
resourceSvc.Dispose();
lookupTableSvc.Dispose();
queueSvc.Dispose();
listSvc.Dispose();
webClient.Dispose();
}
}
/// <summary>
/// Logs off from the project server.
/// </summary>
private void Logoff()
{
try
{
// The idea is that this method will just get called when the object is disposed, therefore
// if an exception occurs, we will just swallow it - if an exception occurs it is likely that
// we will be disposoing of the object due to a previous exception.
loginSvc.Logoff();
}
catch (SoapException)
{ }
catch (WebException)
{ }
}
#endregion
}
Implementing the IDisposable pattern in this way, I think makes for quite a nice programming construct when used in conjunction with the using statement:
using (PSI psi = new PSI("Create new Project")
{
psi.CreateProject("Project Name");
}
Exception Handling
The PSI web methods have a tendancy to throw exceptions which have varying amounts of useful information encapsulated in them. The code below (which is used in the Login() method above) is taken from the many MSDN examples which shows how to extract the data encapsulated. I warn you now though, the text produced may be useful for debugging or auditing purposes, but you probably wouldn't want to risk showing it to a user.
/// <summary>
/// An attempt at making the exceptions that are thrown by project server, more useful.
/// </summary>
/// <param name="ex">The exception that project server threw.</param>
/// <returns>A string representation of the error that occured.</returns>
private String InterpretException(SoapException ex)
{
StringBuilder errMess = new StringBuilder();
PSClientError error = new PSClientError(ex);
errMess.AppendLine("The following project server error(s) occcured:");
foreach (PSErrorInfo errorInfo in error.GetAllErrors())
{
for (int i = 0; i < errorInfo.ErrorAttributes.Length; i++)
{
errMess.Append(errorInfo.ErrorAttributeNames()[i]);
errMess.Append(" : ");
errMess.AppendLine(errorInfo.ErrorAttributes[i]);
}
}
return errMess.ToString();
}
/// <summary>
/// An attempt at making the exceptions that are thrown by project server, more useful.
/// </summary>
/// <param name="ex">The exception that project server threw.</param>
/// <returns>A string representation of the error that occured.</returns>
private string InterpretException(WebException ex)
{
StringBuilder errMess = new StringBuilder();
errMess.AppendLine(ex.Message);
errMess.AppendLine("Log on, or check the Project Server Queuing Service");
return errMess.ToString();
}
Waiting for jobs to complete
As mentioned in the web services section a number of the PSI web methods are asynchronous, but by making use of the QueueSystem web service, you can determine when they have finished (successfully or otherwise) and therefore (if you wish) create synchronous equivalents. The method below takes a job guid as a parameter (the same one that would have been passed to an asynchronous web method) and uses that to determine when the job has completed.
/// <summary>
/// Waits for a job to pass through the queue - inspired by
/// http://msdn.microsoft.com/en-us/library/websvcresource.resource.readresourceassignments.aspx
/// </summary>
/// <param name="jobId">The job Guid.</param>
private void WaitForQueue(Guid jobId)
{
string xmlError = string.Empty;
JobState jobState = GetJobCompletionState(jobId, out xmlError);
if (jobState != JobState.Success)
{
do
{
if (jobState == QueueSystemWebSvc.JobState.Unknown ||
jobState == QueueSystemWebSvc.JobState.Failed ||
jobState == QueueSystemWebSvc.JobState.FailedNotBlocking ||
jobState == QueueSystemWebSvc.JobState.CorrelationBlocked ||
jobState == QueueSystemWebSvc.JobState.Canceled)
{
throw new PSIException("Queue request failed " + jobState + " for Job ID " + jobId + ".\r\n" + xmlError);
}
else
{
Thread.Sleep(QUEUE_WAIT_TIME * 1000);
}
}
while ((jobState = GetJobCompletionState(jobId, out xmlError)) != JobState.Success);
}
}
/// <summary>
/// Gets the job completion state.
/// </summary>
/// <param name="jobId">The Guid of the job to get the state for.</param>
/// <param name="xmlError">An out parameter to collect any error data.</param>
/// <returns>The job state.</returns>
private JobState GetJobCompletionState(Guid jobId, out String xmlError)
{
try
{
return queueSvc.GetJobCompletionState(jobId, out xmlError);
}
catch (SoapException ex)
{
throw new PSIException(InterpretException(ex), ex);
}
catch (WebException ex)
{
throw new PSIException(InterpretException(ex), ex);
}
}
Creating a Project
To create a new project, you simply need to create a new project dataset and a new project row for the new project. The example below sets a handful of the fields that are at a project level but, as with all of the following examples, I just set the ones that I needed to for my purposes.
/// <summary>
/// Creates a new project.
/// </summary>
/// <param name="projectName">The name to give the project.</param>
/// <param name="startDate">The project start date.</param>
/// <returns>The Guid assigned to the new project.</returns>
public Guid CreateProject(String projectName,
Nullable<DateTime> startDate)
{
using (ProjectDataSet projectDS = new ProjectDataSet())
{
ProjectDataSet.ProjectRow projectRow = projectDS.Project.NewProjectRow();
projectRow.PROJ_UID = Guid.NewGuid();
projectRow.PROJ_NAME = projectName;
if (startDate.HasValue)
projectRow.PROJ_INFO_START_DATE = startDate.Value;
projectDS.Project.AddProjectRow(projectRow);
Guid jobId = Guid.NewGuid();
CreateProject(jobId, projectDS);
return projectRow.PROJ_UID;
}
}
/// <summary>
/// Creates a new project in project server.
/// </summary>
/// <param name="jobId">The Guid to give the queued create job.</param>
/// <param name="projectDS">The project dataset.</param>
private void CreateProject(Guid jobId, ProjectDataSet projectDS)
{
try
{
projectSvc.QueueCreateProject(jobId, projectDS, false);
}
catch (SoapException ex)
{
throw new PSIException(InterpretException(ex), ex);
}
catch (WebException ex)
{
throw new PSIException(InterpretException(ex), ex);
}
}
This method could be extended to add tasks or publish the project as well. The QueueCreateProject() method is asynchronous so if you wanted to do this, you would need to wait for the job to finish first.
Reading a Project
Reading a project from project server is pretty trivial. Simply call the ReadProject() web method with the Guid of the project you want to read. This method also takes a second parameter which is the store to read the project from (for example the published store). The method will return a project dataset which represents the project.
#region Read Project
/// <summary>
/// Gets Project data from project server
/// </summary>
/// <param name="store">The store to get the project from</param>
/// <returns>ProjectDataset containing project data</returns>
public ProjectDataSet GetProjectData(Guid projId, ProjectWebSvc.DataStoreEnum store)
{
try
{
return projectSvc.ReadProject(projId, store);
}
catch (SoapException ex)
{
throw new PSIException(InterpretException(ex), ex);
}
catch (WebException ex)
{
throw new PSIException(InterpretException(ex), ex);
}
}
Check In/Out
Before you can update a project you will need to check the project out. Once you have finished, you will need to check it in again. As with most things, this is again straight forward and just a matter of calling a couple of web methods and passing the Guid of the project you wish to check in/out to them. The check out method also takes an additional parameter (force) which allows you to force a check in. This is used to check in a project not checked out to the current user (for example if another user has left the project checked out by mistake), the user calling check in will require sufficient permissions to force the check in however.
/// <summary>
/// Checks a project out.
/// </summary>
/// <param name="projectId">The guid for the project that needs checking out.</param>
public void CheckOutProject(Guid projectId)
{
try
{
projectSvc.CheckOutProject(projectId, sessionId, sessionDescr);
}
catch (SoapException ex)
{
throw new PSIException(InterpretException(ex), ex);
}
catch (WebException ex)
{
throw new PSIException(InterpretException(ex), ex);
}
}
/// <summary>
/// Checks a project in.
/// </summary>
/// <param name="projectId">The guid for the project that needs checking in.</param>
/// <param name="force">Forces checkin if project is checked out the another session id</param>
public void CheckInProject(Guid projectId, bool force)
{
try
{
Guid jobId = Guid.NewGuid();
projectSvc.QueueCheckInProject(jobId, projectId, force, sessionId, sessionDescr);
WaitForQueue(jobId);
}
catch (SoapException ex)
{
throw new PSIException(InterpretException(ex), ex);
}
catch (WebException ex)
{
throw new PSIException(InterpretException(ex), ex);
}
}
Update Project
Updating a project isn't much different from creating a project. You pass in a project dataset containing the data for the project to update and call the QueueUpdateProject() web method. There is a GOTCHA with this web method though. You can only update existing items. For example, you can't add additional tasks to the project dataset - if you tried to do this then you would get an error
/// <summary>
/// Updates a project. Something you wont find in the documentation very easily is that you can
/// only update existing items with this web method - for example you can't add or removed resources.
/// Now who thought that was a good idea?
/// </summary>
/// <param name="projectDS">A dataset containing the project to update.</param>
public void UpdateProject(ProjectDataSet projectDS)
{
try
{
Guid jobId = Guid.NewGuid();
projectSvc.QueueUpdateProject(jobId, sessionId, projectDS, false);
WaitForQueue(jobId);
}
catch (SoapException ex)
{
throw new PSIException(InterpretException(ex), ex);
}
catch (WebException ex)
{
throw new PSIException(InterpretException(ex), ex);
}
}
Publish Project
Publishing a project makes it available to be viewed in project server. It is possible to create a workspace for the project by passing in a valid url for the workspace but it's optional. The creation of the workspace is a little temprimental however. The workspace url should be of the form <base URI><workspace name>. The base URI must be exactly right, including case otherwise the creation of the workspace will silently fail. For example, during development I used http://portal.fabrikam.com/pwa/ rather than http://portal.fabrikam.com/PWA/ which meant a workspace was not created. When things do work properly, it takes a little bit of time for the workspace to be created even after the publish job has been removed from the queue.
/// <summary>
/// Publishes a project and creates the default windows sharepoint services workspace.
/// </summary>
/// <param name="projectId">The Id of the project to publish.</param>
/// <param name="wssUrl">A valid URL for the workspace (null will mean no workspace is created).</param>
public void PublishProject(Guid projectId, string wssUrl)
{
try
{
Guid jobId = Guid.NewGuid();
projectSvc.QueuePublish(jobId, projectId, false, wssUrl);
WaitForQueue(jobId);
}
catch (SoapException ex)
{
throw new PSIException(InterpretException(ex), ex);
}
catch (WebException ex)
{
throw new PSIException(InterpretException(ex), ex);
}
}
Add Project Tasks
Adding tasks to a project takes a number of steps. First of all, the project will need to be checked out, task rows will need to be added to a project dataset and then the QueueAddToProject web method will need to be called, passing in the dataset containing the new tasks. Again, there are lots of fields that can be set on the task, but I only explictly set the ones I'm interested in, the most important of these for me, were the parent guid and outline level, in order to create the task, sub task project structure.
/// <summary>
/// Creates a task row and adds it to the tasks dataset.
/// </summary>
/// <param name="projectId">The Id of the project the task is for.</param>
/// <param name="tasks">The dataset containing new tasks.</param>
/// <param name="description">The task description/name.</param>
/// <param name="level">The level of the task.</param>
/// <param name="parentTaskId">The Guid of the parent task.</param>
/// <param name="duration">The task duration (in Project duration time units).</param>
/// <returns>The Guid of the newly created task.</returns>
private Guid CreateTask(Guid projectId,
ProjectDataSet tasks,
String description,
Int32 level,
Nullable<Guid> parentTaskId,
Int32 duration,
Nullable<decimal> cost)
{
ProjectDataSet.TaskRow task = tasks.Task.NewTaskRow();
task.PROJ_UID = projectId;
task.TASK_UID = Guid.NewGuid();
task.TASK_NAME = description;
if (parentTaskId.HasValue)
task.TASK_PARENT_UID = parentTaskId.Value;
task.TASK_OUTLINE_LEVEL = level;
task.TASK_DUR_FMT = (Int32)Task.DurationFormat.Day;
task.TASK_DUR = duration;
if (cost.HasValue)
task.TASK_COST = (double)cost.Value;
tasks.Task.AddTaskRow(task);
return task.TASK_UID;
}
/// <summary>
/// Adds entities to project.
/// </summary>
/// <param name="projectId">The guid of the project to add entities to.</param>
/// <param name="tasks">A project dataset containing new entities.</param>
public void AddToProject(Guid projectId, ProjectDataSet projectDS)
{
try
{
Guid jobId = Guid.NewGuid();
projectSvc.QueueAddToProject(jobId, sessionId, projectDS, false);
WaitForQueue(jobId);
}
catch (SoapException ex)
{
throw new PSIException(InterpretException(ex), ex);
}
catch (WebException ex)
{
throw new PSIException(InterpretException(ex), ex);
}
}
When adding tasks, it is likely that you will need to add task dependencies - i.e one task shouldn't start until another one has finished. This is just a matter of populating another data table in the project dataset, prior to calling QueueAddToProject().
/// <summary>
/// Creates a dependency so that the success task is dependent upon the predecessor task.
/// </summary>
/// <param name="projectId">The Guid of the project the dependency relates to.</param>
/// <param name="predecessorId">The Guid of the predecessor task.</param>
/// <param name="successorId">The Guid of the success task.</param>
/// <param name="projectDS">The project dataset.</param>
/// <returns>The Guid of the newly created dependency.</returns>
private Guid CreateTaskDependency(Guid projectId,
Guid predecessorId,
Guid successorId,
ProjectDataSet projectDS)
{
ProjectDataSet.DependencyRow dependency = projectDS.Dependency.NewDependencyRow();
dependency.PROJ_UID = projectId;
dependency.LINK_UID = Guid.NewGuid();
dependency.LINK_PRED_UID = predecessorId;
dependency.LINK_SUCC_UID = successorId;
dependency.LINK_TYPE = 1; // Predecessor must finish before successor can start
dependency.LINK_LAG_FMT = (Int32)Task.DurationFormat.Hour;
dependency.LINK_LAG = 0;
projectDS.Dependency.AddDependencyRow(dependency);
return dependency.LINK_UID;
}
When adding tasks to a project you can also add resources to those projects. Rather than adding resources you actually need to create resource assignments. If you want to create an enterprise resource assignment then you will need to find the guid of the enterprise resource, add the resource to the project team and then create the assignment. Adding resources to the project team requires a call to an additional web method - QueueUpdateProjectTeam(). QueueUpdateProjectTeam() will need to be called prior to QueueAddToProject() and the job must have completed which is why WaitForQueue() is called.
/// <summary>
/// Adds the enterprise resources to the project team and creates and assignment for them in the
/// project dataset. We must wait for the resources to be added to the team (which this method does)
/// before we can create the assignments - the problem with this is that if anything goes wrong
/// when creating / updating the project, the resources will still have been added to the project.
/// </summary>
/// <param name="projectId">The Guid of the project.</param>
/// <param name="taskId">The Guid of the task to assign the resources to.</param>
/// <param name="projectDS">The project dataset.</param>
/// <param name="resrcs">The list of resources.</param>
private void AddEnterpriseResources(Guid projectId,
Guid taskId,
ProjectDataSet projectDS,
IEnumerable<IEntResource> resrcs)
{
using (ProjectTeamDataSet teamDS = new ProjectTeamDataSet())
{
AddEnterpriseResources(projectId, taskId, projectDS, teamDS, resrcs);
if (teamDS.ProjectTeam.Count > 0)
UpdateProjectTeam(projectId, teamDS);
}
}
/// <summary>
/// Adds the enterprise resources to the team dataset and creates an assignment for them in the
/// project dataset.
/// </summary>
/// <param name="projectId">The Guid of the project the resources will be added to.</param>
/// <param name="taskId">The Guid of the task the resources will be assigned to.</param>
/// <param name="projectDS">The project dataset.</param>
/// <param name="teamDS">The team dataset.</param>
/// <param name="resrcs">The enterprise resources to add.</param>
private void AddEnterpriseResources(Guid projectId,
Guid taskId,
ProjectDataSet projectDS,
ProjectTeamDataSet teamDS,
IEnumerable<IEntResource> resrcs)
{
foreach (IEntResource resrc in resrcs)
{
Guid resrcId = GetEnterpriseResourceUID(resrc.CustFieldName, resrc.CustFieldValue);
CreateProjectTeamRow(projectId, resrcId, teamDS);
CreateTaskRsrcAssignment(projectId, projectDS, taskId, resrcId);
}
}
/// <summary>
/// Gets the Guid of the enterprise resource with the custom field matching the specified
/// value.
/// </summary>
/// <param name="customFieldName">The name of the custom field.</param>
/// <param name="customFieldValue">The value of the custom field.</param>
/// <returns>The resources Guid.</returns>
private Guid GetEnterpriseResourceUID(String customFieldName, String customFieldValue)
{
using (ResourceDataSet resourceDS =
GetEnterpriseResources(
new Filter.FieldOperator(
Filter.FieldOperationType.Equal, Filter.MatchType.SingleValue,
GetCustomFieldUID(customFieldName, new Guid(EntityCollection.Entities.ResourceEntity.UniqueId)),
Filter.PropertyTypeEnum.TextValue, customFieldValue)))
{
if (resourceDS.Resources == null || resourceDS.Resources.Count == 0)
throw new PSIException("Unknown resource with " + customFieldName + " of " + customFieldValue);
if (resourceDS.Resources.Count > 1)
throw new PSIException("Project server has been configured with multiple enterprise resources " +
"with the " + customFieldName + " of " + customFieldValue);
return resourceDS.Resources[0].RES_UID;
}
}
/// <summary>
/// Creates a project team row.
/// </summary>
/// <param name="projectId">The Guid of the project the team applies to.</param>
/// <param name="resrcId">The Guid of the enterprise resource to add to the team.</param>
/// <param name="teamDS">The team data set.</param>
private void CreateProjectTeamRow(Guid projectId, Guid resrcId, ProjectTeamDataSet teamDS)
{
ProjectTeamDataSet.ProjectTeamRow teamRow = teamDS.ProjectTeam.NewProjectTeamRow();
teamRow.PROJ_UID = projectId;
teamRow.RES_UID = resrcId;
teamRow.NEW_RES_UID = resrcId;
teamRow.RES_IS_ENTERPRISE_RESOURCE = true;
teamDS.ProjectTeam.AddProjectTeamRow(teamRow);
}
/// <summary>
/// Updates the project team, and waits until the update is complete (assumes there is a project
/// team to update).
/// </summary>
/// <param name="projectId">The Id of the project the team is for.</param>
/// <param name="teamDS">The project team.</param>
private void UpdateProjectTeam(Guid projectId, ProjectTeamDataSet teamDS)
{
try
{
Guid jobId = Guid.NewGuid();
projectSvc.QueueUpdateProjectTeam(jobId, sessionId, projectId, teamDS);
WaitForQueue(jobId);
}
catch (SoapException ex)
{
throw new PSIException(InterpretException(ex), ex);
}
catch (WebException ex)
{
throw new PSIException(InterpretException(ex), ex);
}
}
/// <summary>
/// Creates a task, resource assignment and adds it to the project dataset.
/// </summary>
/// <param name="projectId">The Id of the project the assignment is for.</param>
/// <param name="projectDS">The dataset to add the assignment to.</param>
/// <param name="taskId">The Id of the task.</param>
/// <param name="resrcId">The Id of the resource.</param>
/// <returns>The Guid of the new assignment.</returns>
private Guid CreateTaskRsrcAssignment(Guid projectId, ProjectDataSet projectDS, Guid taskId, Guid resrcId)
{
ProjectDataSet.AssignmentRow assignment = projectDS.Assignment.NewAssignmentRow();
assignment.PROJ_UID = projectId;
assignment.ASSN_UID = Guid.NewGuid();
assignment.TASK_UID = taskId;
assignment.RES_UID = resrcId;
projectDS.Assignment.AddAssignmentRow(assignment);
return assignment.ASSN_UID;
}
If you want to create a local resource assignment then you will either need to create a new local resource and add it to the project and then create the assignment, or find the local resources guid and then create the assignment. There is example code below to do all of these steps - creating the assignment is the same for local and enterprise resources:
/// <summary>
/// Searches the project dataset for a resource with the specified name.
/// </summary>
/// <param name="projectDS">The project dataset.</param>
/// <param name="resourceNm">The name of the resource to search for.</param>
/// <returns>The guid of the resource if it is found.</returns>
private Guid? FindLocalResource(ProjectDataSet projectDS, String resourceNm)
{
Guid? resrcId = null;
foreach (ProjectDataSet.ProjectResourceRow resource in projectDS.ProjectResource)
{
if (resource.RES_NAME == resourceNm)
{
resrcId = resource.RES_UID;
break;
}
}
return resrcId;
}
/// <summary>
/// Creates a local (project) resource and adds it to the project dataset.
/// </summary>
/// <param name="projectId">The Id of the project the resource is for.</param>
/// <param name="projectDS">The dataset to add the resource to.</param>
/// <param name="resourceNm">The name to be given to the resource.</param>
/// <returns>The guid for the new resource.</returns>
private Guid CreateLocalResource(Guid projectId,
ProjectDataSet projectDS,
String resourceNm)
{
ProjectDataSet.ProjectResourceRow localRsrc = projectDS.ProjectResource.NewProjectResourceRow();
localRsrc.PROJ_UID = projectId;
localRsrc.RES_UID = Guid.NewGuid();
localRsrc.RES_NAME = resourceNm;
projectDS.ProjectResource.AddProjectResourceRow(localRsrc);
return localRsrc.RES_UID;
}
There is a big GOTCHA here. The PSI has a number of limitations which can be a little restrictive. One of the limitations is that it will NOT let you create cost resources. Work and Material resources are fine, but you can NOT create cost resources. The way I got round this limitation was to create Material resources with a quantity of 1.
Enterprise Resources
Below is example code for reading enterprise resources, both by resource GUID and unique custom field - in this example, the custom field value will come from a lookup table (the name of which must be specified). It should be reasonably easy to see how this code could be changed to read resources by a custom field whose value does not come from a lookup table.
/// <summary>
/// Reads a single resource, using its resource guid.
/// </summary>
/// <param name="resourceUID">The guid of the resource to read.</param>
/// <returns>A data set containing the resource.</returns>
public ResourceDataSet GetEnterpriseResource(Guid resourceUID)
{
using (ResourceDataSet filterDS = new ResourceDataSet())
{
Filter.FieldOperator filter = new Filter.FieldOperator(
Filter.FieldOperationType.Equal, filterDS.Resources.RES_UIDColumn.ColumnName, resourceUID);
return GetEnterpriseResources(filter);
}
}
/// <summary>
/// Reads all resources from the enterprise resource pool.
/// </summary>
/// <returns>A resource dataset.</returns>
public ResourceDataSet GetEnterpriseResources()
{
return GetEnterpriseResources(null, null, null);
}
/// <summary>
/// Reads all resources from the enterprise resource pool. It optionally filters the results based
/// on a "lookup table" custom field value.
/// </summary>
/// <param name="customFieldNm">The optional custom field to filter the resources on.</param>
/// <param name="lookupTableNm">The lookup table from which the custom field value will come.</param>
/// <param name="customFieldValue">The value of the optional custom field.</param>
/// <returns>A dataset of resources.</returns>
public ResourceDataSet GetEnterpriseResources(String customFieldNm,
String lookupTableNm,
String customFieldValue)
{
Filter.FieldOperator filter = null;
if (!String.IsNullOrEmpty(customFieldNm))
filter = CreateResrcCustLookupFldFilter(customFieldNm, lookupTableNm, customFieldValue);
return GetEnterpriseResources(filter);
}
/// <summary>
/// Creates the filter used to find an enterprise field with a particular custom field set to a value
/// from a lookup table.
/// </summary>
/// <param name="customFieldNm">The name of the custom field.</param>
/// <param name="lookupTableNm">The lookup table the value has come from.</param>
/// <param name="customFieldValue">The value.</param>
/// <returns>A filter.</returns>
private Filter.FieldOperator CreateResrcCustLookupFldFilter(String customFieldNm,
String lookupTableNm,
String customFieldValue)
{
using (LookupTableDataSet lookupTable = GetLookupTable(lookupTableNm))
{
return new Filter.FieldOperator(
Filter.FieldOperationType.Equal, Filter.MatchType.SingleValue,
GetCustomFieldUID(customFieldNm, new Guid(EntityCollection.Entities.ResourceEntity.UniqueId)),
Filter.PropertyTypeEnum.CodeValue, GetItemUIDFromLookupTable(lookupTable, customFieldValue));
}
}
/// <summary>
/// Gets the guid for an item in a lookup table. This code assumes that the lookup table data set will
/// contain just one lookup table.
/// </summary>
/// <param name="lookupTables">The lookup tables dataset.</param>
/// <param name="value">The (text) value of the item to find the UID for.</param>
/// <returns>The Guid of the item in the lookup table.</returns>
private Guid GetItemUIDFromLookupTable(LookupTableDataSet lookupTables, String value)
{
LookupTableDataSet.LookupTableTreesDataTable lookupTableTrees = lookupTables.LookupTableTrees;
using (DataView lookupItems = new DataView(lookupTableTrees))
{
lookupItems.RowFilter =
lookupTableTrees.LT_VALUE_TEXTColumn.ColumnName + " = '" + value + "'";
if (lookupItems.Count == 0)
throw new PSIException("Lookup table does not contain an entry with the specified value [" +
value + "]");
if (lookupItems.Count > 1)
throw new PSIException("Lookup table contains multiple entries with the specified value [" +
value + "]");
return (Guid)lookupItems[0][lookupTableTrees.LT_STRUCT_UIDColumn.ColumnName];
}
}
/// <summary>
/// Gets a dataset of all enterprise resources, optionally filtered.
/// </summary>
/// <param name="filterCriteria">An optional filter used to filter the resources returned.</param>
/// <returns>A dataset of enterprise resources.</returns>
private ResourceDataSet GetEnterpriseResources(Filter.FieldOperator filterCriteria)
{
using (ResourceDataSet filterDS = new ResourceDataSet())
{
String tableName = filterDS.Resources.TableName;
String uidColumn = filterDS.Resources.RES_UIDColumn.ColumnName;
String nameColumn = filterDS.Resources.RES_NAMEColumn.ColumnName;
Filter filter = new Filter();
filter.FilterTableName = tableName;
filter.Fields.Add(new Filter.Field(tableName, uidColumn));
filter.Fields.Add(new Filter.Field(nameColumn));
if (filterCriteria != null)
filter.Criteria = filterCriteria;
return ReadResources(filter.GetXml());
}
}
/// <summary>
/// Reads the enterprise resources from the project server.
/// </summary>
/// <param name="filter">A filter to limit the results.</param>
/// <returns>A resource dataset.</returns>
private ResourceDataSet ReadResources(String filter)
{
try
{
return resourceSvc.ReadResources(filter, false);
}
catch (SoapException ex)
{
throw new PSIException(InterpretException(ex), ex);
}
catch (WebException ex)
{
throw new PSIException(InterpretException(ex), ex);
}
}
As well as reading enterprise resources I also needed to update the costs of enterprise resources. Cost type resources can not have a cost set on them (costs are set at the task assignment level) but we did need to record a default cost for these resources, so we specified a custom field to store this.
/// <summary>
/// Updates the cost rates of all the resources.
/// </summary>
/// <param name="resourceCosts">The resources to update together with their new costs.</param>
/// <param name="defaultCostCustFldNm">You are not able to add a cost to cost type resources, so
/// by specifying a custom field (which is assumed to be a number type) the cost can be stored in
/// their instead.</param>
public void UpdateResourceCosts(IList<IResourceCost> resourceCosts, String defaultCostCustFldNm)
{
Guid? defaultCostGuid = null;
if (!String.IsNullOrEmpty(defaultCostCustFldNm))
defaultCostGuid = GetCustomFieldUID(defaultCostCustFldNm, new Guid(EntityCollection.Entities.ResourceEntity.UniqueId));
// It's unfortunate having to read in all of the resources like this, but there is no (obvious)
// way of filtering the resources and have all the tables in the dataset populated - we need the
// resource custom fields to be populated
using (ResourceDataSet resourceDS = ReadResources(null))
{
UpdateResources(UpdateResourceCosts(resourceCosts, resourceDS, defaultCostGuid), resourceDS);
}
}
/// <summary>
/// Updates the sot rates of all the resources.
/// </summary>
/// <param name="resourceCosts">The resources to update together with their new costs.</param>
/// <param name="resourceDS">A dataset containing all of the resources to be updated (and possibly more).</param>
/// <param name="defaultCostGuid">An optional custom field guid which will be used to store the costs of cost type
/// resources.</param>
/// <returns>An array of all resources that are updated (only resources whose cost has changed will
/// be updated).</returns>
private Guid[] UpdateResourceCosts(IList<IResourceCost> resourceCosts,
ResourceDataSet resourceDS,
Guid? defaultCostGuid)
{
List<Guid> updatedRsrcGuids = new List<Guid>();
foreach (IResourceCost resourceCost in resourceCosts)
{
if (UpdateResourceCost(resourceCost, resourceDS.Resources.FindByRES_UID(resourceCost.ResourceUID),
resourceDS.ResourceCustomFields, resourceDS.ResourceRates, defaultCostGuid))
{
updatedRsrcGuids.Add(resourceCost.ResourceUID);
}
}
return updatedRsrcGuids.ToArray();
}
/// <summary>
/// Updates a single cost.
/// </summary>
/// <param name="resourceCost">The resource to update together with its new costs.</param>
/// <param name="resource">The corresponding resource row read from project server.</param>
/// <param name="customFields">All the resource custom fields (required to be able to update the cost
/// custom field for cost type resources).</param>
/// <param name="rates">All of the resource rates.</param>
/// <param name="defaultCostGuid">An optional guid of the custom field that will be used to store the
/// cost of cost type resources.</param>
/// <returns>true if the resource is updated (i.e. the cost has changed).</returns>
private bool UpdateResourceCost(IResourceCost resourceCost,
ResourceDataSet.ResourcesRow resource,
ResourceDataSet.ResourceCustomFieldsDataTable customFields,
ResourceDataSet.ResourceRatesDataTable rates,
Guid? defaultCostGuid)
{
bool updated = false;
if (IsCostType((PSIL.Resource.Type)resource.RES_TYPE))
{
updated = UpdateResourceCost(resourceCost, resource, customFields, defaultCostGuid.Value);
}
else if (IsWorkOrMaterialType((PSIL.Resource.Type)resource.RES_TYPE))
{
updated = UpdateResourceCost(resourceCost, resource, rates);
}
return updated;
}
/// <summary>
/// Checks whether the type is one of the cost types.
/// </summary>
/// <param name="type">The type to check.</param>
/// <returns>true if it is one of the cost types.</returns>
public bool IsCostType(PSIL.Resource.Type type)
{
return type == PSIL.Resource.Type.CostResources ||
type == PSIL.Resource.Type.BudgetCostResource ||
type == PSIL.Resource.Type.GenericBudgetCostResource ||
type == PSIL.Resource.Type.GenericCostResources;
}
/// <summary>
/// Checks whether the type is one of the work or material types.
/// </summary>
/// <param name="type">The type to check.</param>
/// <returns>true if it is one of the work or material types.</returns>
public bool IsWorkOrMaterialType(PSIL.Resource.Type type)
{
return IsWorkType(type) ||
IsMaterialType(type);
}
/// <summary>
/// Checks whether type is one of the work types.
/// </summary>
/// <param name="type">The type to check.</param>
/// <returns>true if it is one of the work types.</returns>
public bool IsWorkType(PSIL.Resource.Type type)
{
return type == PSIL.Resource.Type.WorkResource ||
type == PSIL.Resource.Type.BudgetWorkResource ||
type == PSIL.Resource.Type.GenericBudgetWorkResource ||
type == PSIL.Resource.Type.GenericWorkResource;
}
/// <summary>
/// Checks whether the type is one of the material types.
/// </summary>
/// <param name="type">The type to check.</param>
/// <returns>true if it is one of the material types.</returns>
public bool IsMaterialType(PSIL.Resource.Type type)
{
return type == PSIL.Resource.Type.MaterialResource ||
type == PSIL.Resource.Type.BudgetMaterialResource ||
type == PSIL.Resource.Type.GenericBudgetMaterialResource ||
type == PSIL.Resource.Type.GenericMaterialResource;
}
/// <summary>
/// Updates a cost type resource.
/// </summary>
/// <param name="resourceCost">The resource to update together with the new cost.</param>
/// <param name="resource">The corresponding resource read from project server.</param>
/// <param name="customFields">All the resource custom fields (required to be able to update the cost
/// custom field for cost type resources).</param>
/// <param name="defaultCostGuid">An optional guid of the custom field that will be used to store the
/// cost of cost type resources.</param>
/// <returns>true if the resource is updated ( if the cost has changed).</returns>
private bool UpdateResourceCost(IResourceCost resourceCost,
ResourceDataSet.ResourcesRow resource,
ResourceDataSet.ResourceCustomFieldsDataTable customFields,
Guid defaultCostGuid)
{
List<ResourceDataSet.ResourceCustomFieldsRow> customFieldsLst =
new List<ResourceDataSet.ResourceCustomFieldsRow>(resource.GetResourceCustomFieldsRows());
ResourceDataSet.ResourceCustomFieldsRow customField =
customFieldsLst.Find(delegate(ResourceDataSet.ResourceCustomFieldsRow currField)
{
return currField.MD_PROP_UID == defaultCostGuid;
});
bool updated = false;
if ((updated = (customField == null)))
{
ResourceDataSet.ResourceCustomFieldsRow newCustomField = customFields.NewResourceCustomFieldsRow();
newCustomField.CUSTOM_FIELD_UID = Guid.NewGuid();
newCustomField.RES_UID = resource.RES_UID;
newCustomField.MD_PROP_UID = defaultCostGuid;
newCustomField.FIELD_TYPE_ENUM = (byte)CustomField.Type.NUMBER;
newCustomField.NUM_VALUE = resourceCost.Cost ?? 0.0M;
customFields.AddResourceCustomFieldsRow(newCustomField);
}
else if ((updated = (customField.IsNUM_VALUENull() || customField.NUM_VALUE != (resourceCost.Cost ?? 0.0M))))
{
customField.NUM_VALUE = resourceCost.Cost ?? 0.0M;
}
return updated;
}
/// <summary>
/// Updates the costs of a work or material resource.
/// </summary>
/// <param name="resourceCost">The resource to update together with its new costs.</param>
/// <param name="resource">The corresponding resource row read from project server.</param>
/// <param name="rates">All of the resource rates.</param>
/// <returns>true if the resource is updated (i.e. one of the costs have changed).</returns>
private bool UpdateResourceCost(IResourceCost resourceCost,
ResourceDataSet.ResourcesRow resource,
ResourceDataSet.ResourceRatesDataTable rates)
{
bool updated = false;
List<ResourceDataSet.ResourceRatesRow> list = new List<ResourceDataSet.ResourceRatesRow>(resource.GetResourceRatesRows());
updated |= UpdateResourceCost(rates, resource.RES_UID, 0, resourceCost.CostRateA ?? 0.0);
updated |= UpdateResourceCost(rates, resource.RES_UID, 1, resourceCost.CostRateB ?? 0.0);
updated |= UpdateResourceCost(rates, resource.RES_UID, 2, resourceCost.CostRateC ?? 0.0);
updated |= UpdateResourceCost(rates, resource.RES_UID, 3, resourceCost.CostRateD ?? 0.0);
updated |= UpdateResourceCost(rates, resource.RES_UID, 4, resourceCost.CostRateE ?? 0.0);
return updated;
}
/// <summary>
/// Updates a resource cost - it adds a new resource rate with an effective date of now.
/// </summary>
/// <param name="rates">All of the resource rates.</param>
/// <param name="resourceGuid">The Guid identifying the resource that the new rate is for.</param>
/// <param name="rateTableId">The rate table (0, 1, 2, 3 or 4 corresponding to A, B, C, D or E).</param>
/// <param name="cost">The new cost.</param>
/// <returns>true if the resource is updated (i.e. a new rate is added).</returns>
private bool UpdateResourceCost(ResourceDataSet.ResourceRatesDataTable rates,
Guid resourceGuid,
int rateTableId,
double cost)
{
bool updated = false;
double? stdRate = GetStdRate(rates, resourceGuid, rateTableId);
if ((updated = (!stdRate.HasValue || stdRate.Value != cost)))
{
ResourceDataSet.ResourceRatesRow newRate = rates.NewResourceRatesRow();
newRate.RES_UID = resourceGuid;
newRate.RES_RATE_TABLE = rateTableId;
newRate.RES_RATE_EFFECTIVE_DATE = DateTime.Now;
newRate.RES_STD_RATE = cost;
rates.AddResourceRatesRow(newRate);
}
return updated;
}
/// <summary>
/// Gets the rate for the latest effective date.
/// </summary>
/// <param name="rates">All of the rates.</param>
/// <param name="rateTableId">The rate table we are interested in.</param>
/// <returns>The lates standard rate (or null if there isn't one).</returns>
private double? GetStdRate(ResourceDataSet.ResourceRatesDataTable rates,
Guid resourceGuid,
int rateTableId)
{
using (DataView rateItems = new DataView(rates))
{
rateItems.RowFilter =
rates.RES_UIDColumn.ColumnName +" = CONVERT('" + resourceGuid + "', 'System.Guid') AND " +
rates.RES_RATE_EFFECTIVE_DATEColumn.ColumnName + " = MAX(RES_RATE_EFFECTIVE_DATE) AND " +
rates.RES_RATE_TABLEColumn.ColumnName + " = " + rateTableId;
return rateItems.Count == 0 ? default(double?) : (double)rateItems[0][rates.RES_STD_RATEColumn.ColumnName];
}
}
To actually write the update away, you will need to check out the resource:
/// <summary>
/// Checks out the resources that have been updated - these are identified by the guid array - updates
/// them and checks them back in.
/// </summary>
/// <param name="updatedRsrcsGuids">The guids of the resources to update.</param>
/// <param name="resourceDS">The enterprise resources.</param>
private void UpdateResources(Guid[] updatedRsrcsGuids, ResourceDataSet resourceDS)
{
CheckOutResources(updatedRsrcsGuids);
try
{
UpdateResources(resourceDS);
}
finally
{
CheckInResources(updatedRsrcsGuids, false);
}
}
/// <summary>
/// Checks out all of the resources.
/// </summary>
/// <param name="resourceUIDs">The guids of the resources to check out.</param>
private void CheckOutResources(Guid[] resourceUIDs)
{
try
{
resourceSvc.CheckOutResources(resourceUIDs);
}
catch (SoapException ex)
{
throw new PSIException(InterpretException(ex), ex);
}
catch (WebException ex)
{
throw new PSIException(InterpretException(ex), ex);
}
}
/// <summary>
/// Checks in the resources.
/// </summary>
/// <param name="resourceUIDs">The guids of the resources to check in.</param>
/// <param name="force">A flag to determine whether or not to force the checkin.</param>
private void CheckInResources(Guid[] resourceUIDs, bool force)
{
try
{
resourceSvc.CheckInResources(resourceUIDs, force);
}
catch (SoapException ex)
{
throw new PSIException(InterpretException(ex), ex);
}
catch (WebException ex)
{
throw new PSIException(InterpretException(ex), ex);
}
}
/// <summary>
/// Updates the enterprise resources in the dataset.
/// </summary>
/// <param name="resourceDS">The enterprise resources.</param>
private ResourceDataSet UpdateResources(ResourceDataSet resourceDS)
{
try
{
return resourceSvc.UpdateResources(resourceDS, false, false);
}
catch (SoapException ex)
{
throw new PSIException(InterpretException(ex), ex);
}
catch (WebException ex)
{
throw new PSIException(InterpretException(ex), ex);
}
}
Custom Fields
When adding new resources to a project, if you have any compulsory custom fields set up then you will need to add custom field assignments to the project dataset, for the new resources. The code below shows how to find the guid of a custom field and then effectively set the value of a string type custom field on a resource:
/// <summary>
/// Gets the guid of the resource custom field.
/// </summary>
/// <param name="customFieldNm">The name of the custom field.</param>
/// <returns>The custom field guid.</returns>
public Guid GetResourceCustomFieldUID(String customFieldNm)
{
return GetCustomFieldUID(customFieldNm, new Guid(EntityCollection.Entities.ResourceEntity.UniqueId));
}
/// <summary>
/// Finds the Guid of a custom field.
/// </summary>
/// <param name="fieldName">The name of the custom field.</param>
/// <param name="entityType">The type of custom field (Resource, Project or Task).</param>
/// <returns>The custom fields Guid.</returns>
private Guid GetCustomFieldUID(String fieldName, Guid entityType)
{
return GetCustomField(fieldName, entityType).MD_PROP_UID;
}
/// <summary>
/// Finds the custom field.
/// </summary>
/// <param name="fieldName">The name of the custom field.</param>
/// <param name="entityType">The type of custom field (Resource, Project or Task).</param>
/// <returns>The custom field row.</returns>
private CustomFieldDataSet.CustomFieldsRow GetCustomField(String fieldName, Guid entityType)
{
using (CustomFieldDataSet filterDS = new CustomFieldDataSet())
{
String tableName = filterDS.CustomFields.TableName;
String uidColumn = filterDS.CustomFields.MD_PROP_UIDColumn.ColumnName;
String secUidColumn = filterDS.CustomFields.MD_PROP_ID_SECONDARYColumn.ColumnName;
String nameColumn = filterDS.CustomFields.MD_PROP_NAMEColumn.ColumnName;
String entityUidColumn = filterDS.CustomFields.MD_ENT_TYPE_UIDColumn.ColumnName;
Filter filter = new Filter();
filter.FilterTableName = tableName;
filter.Fields.Add(new Filter.Field(tableName, uidColumn));
filter.Fields.Add(new Filter.Field(secUidColumn));
filter.Fields.Add(new Filter.Field(nameColumn));
filter.Fields.Add(new Filter.Field(entityUidColumn));
filter.Criteria =
new Filter.LogicalOperator(
Filter.LogicalOperationType.And,
new Filter.FieldOperator(Filter.FieldOperationType.Equal, entityUidColumn, entityType),
new Filter.FieldOperator(Filter.FieldOperationType.Equal, nameColumn, fieldName));
using (CustomFieldDataSet customFieldDS = ReadCustomFields(filter.GetXml()))
{
if (customFieldDS.CustomFields == null || customFieldDS.CustomFields.Count == 0)
throw new PSIException("Unknown custom field [" + fieldName + "]");
if (customFieldDS.CustomFields.Count > 1)
throw new PSIException("Project Server has been configured with multiple custom fields with " +
"the name [" + fieldName + "]");
return customFieldDS.CustomFields[0];
}
}
}
/// <summary>
/// Reads the custom fields from the project server.
/// </summary>
/// <param name="filter">A filter to limit the results.</param>
private CustomFieldDataSet ReadCustomFields(String filter)
{
try
{
return custFieldsSvc.ReadCustomFields(filter, false);
}
catch (SoapException ex)
{
throw new PSIException(InterpretException(ex), ex);
}
catch (WebException ex)
{
throw new PSIException(InterpretException(ex), ex);
}
}
/// <summary>
/// Creates a resource custom field assignment. In otherwords, sets the custom value for the resource (etc).
/// </summary>
/// <param name="projectId">The Guid of the project.</param>
/// <param name="resourceId">The Guid of the resource.</param>
/// <param name="customFldId">The Guid of the custom field.</param>
/// <param name="projectDS">The project dataset.</param>
/// <param name="value">The value to set for the custom field.</param>
/// <returns>The Guid of the instance of the custom field.</returns>
private Guid CreateResourceCustomField(Guid projectId,
Guid resourceId,
Guid customFldId,
ProjectDataSet projectDS,
String value)
{
ProjectDataSet.ProjectResourceCustomFieldsRow rsrcCustFld = projectDS.ProjectResourceCustomFields.NewProjectResourceCustomFieldsRow();
rsrcCustFld.PROJ_UID = projectId;
rsrcCustFld.RES_UID = resourceId;
rsrcCustFld.CUSTOM_FIELD_UID = Guid.NewGuid();
rsrcCustFld.MD_PROP_UID = customFldId;
rsrcCustFld.FIELD_TYPE_ENUM = (byte)PSDataType.STRING;
rsrcCustFld.TEXT_VALUE = value;
projectDS.ProjectResourceCustomFields.AddProjectResourceCustomFieldsRow(rsrcCustFld);
return rsrcCustFld.MD_PROP_UID;
}
Lookups
The only other thing that I needed to do which I haven't covered so far, was to read lookup tables so that I could assign a value from one to a custom field. The following method does this:
/// <summary>
/// Finds the lookup table.
/// </summary>
/// <param name="tableName">The name of the lookup table to find.</param>
/// <returns>The lookup table.</returns>
private LookupTableDataSet GetLookupTable(String lookupTableName)
{
using (LookupTableDataSet filterDS = new LookupTableDataSet())
{
String tableName = filterDS.LookupTables.TableName;
String uidColumn = filterDS.LookupTables.LT_UIDColumn.ColumnName;
String nameColumn = filterDS.LookupTables.LT_NAMEColumn.ColumnName;
Filter filter = new Filter();
filter.FilterTableName = tableName;
filter.Fields.Add(new Filter.Field(tableName, uidColumn));
filter.Fields.Add(new Filter.Field(tableName, nameColumn));
filter.Criteria =
new Filter.FieldOperator(
Filter.FieldOperationType.Equal, "LT_NAME", new String[] { lookupTableName });
using (LookupTableDataSet lookupTableDS = ReadLookupTables(filter.GetXml()))
{
if (lookupTableDS.LookupTables == null || lookupTableDS.LookupTables.Count == 0)
throw new PSIException("Unknown lookup table [" + tableName + "]");
if (lookupTableDS.LookupTables.Count > 1)
throw new PSIException("Project server has been configured with multiple lookup tables with " +
"the name [" + tableName + "]");
// Because there is no (obvious) way to filter the results of ReadLookupTables to just return
// a single table *AND* have the lookup table trees populated as well, we now need to use
// ReadLookupTablesByUID to re-read the lookup table but this time the table trees will be populated.
// The only other way to do this would be to change the code above, not to specify a filter - this
// would mean that the table trees would be populated, but it would return *all* lookup tables
// which could (would?) ccause its own performance problem.
return ReadLookupTablesByUID(new Guid[] { lookupTableDS.LookupTables[0].LT_UID });
}
}
}
/// <summary>
/// Reads the lookup tables from the project server.
/// </summary>
/// <param name="uidList">A list of uids used to identify the tables to read.</param>
/// <returns>A look up table data set.</returns>
private LookupTableDataSet ReadLookupTablesByUID(Guid[] uidList)
{
try
{
return lookupTableSvc.ReadLookupTablesByUids(uidList, false, 0);
}
catch (SoapException ex)
{
throw new PSIException(InterpretException(ex), ex);
}
catch (WebException ex)
{
throw new PSIException(InterpretException(ex), ex);
}
}
References
Below are a number of links to various resources that I have found useful during this development. They aren't in any particular order, although the first one is probably a good place to start as it lists the things you can't do with the PSI.
http://msdn.microsoft.com/
http://msdn.microsoft.com/...
http://www.thecodecage.com/...
http://www.epmfaq.com/...
http://msdn.microsoft.com/...
http://www.mombu.com/...
http://msdn.microsoft.com/...
http://www.eggheadcafe.com/...
http://msdn.microsoft.com/...
http://books.google.co.uk/...
http://www.microsoft.com/...