Microsoft Project Server 2013:如何以编程方式更改特定时间段的资源分配
5.00/5 (3投票s)
结合多个 MS Project Server API 以实现复杂目标
引言
假设我们在 MS Project Server 2013 中有一个项目,该项目使用了一些物料资源。例如,它可以是某个建筑物的详细施工计划,类似这样:
| Monday | 星期二 | 星期三 | |
| 钉子 | 150 | 300 | 200 |
| 木板 | 20 | 50 | 40 |
| Bricks | 300 | 500 | 350 |
现在,假设我们想通过编程方式将周二的砖块数量从 500 更改为 550。为什么?这可能是因为最初使用的建造者报告不准确,而建造者刚刚上传了更正后的 Excel 文档或纯文本文件。我们不想每次发生这种情况时都手动编辑 Project 文件。
因此,我们将不得不使用某个 API 来操作 Project Server 上的数据。
MS Project Server API 概述
我们可以使用几个 API。每个 API 都有其优点和缺点。
PSI (或 Project Server Interface) 是一个强大的工具,几乎可以在 Project Server 上完成所有事情:创建项目、删除项目、检入、检出、发布、添加或删除任务或资源,以及许多其他不同的事情。
PSI 使用的简单示例
SvcProject.ProjectDataSet projectDs = projectClient.ReadProjectList();
foreach (SvcProject.ProjectDataSet.ProjectRow projectRow in projectDs.Project)
{
if (projectRow.PROJ_NAME == projectName)
projectId = projectRow.PROJ_UID;
}
就我们的任务而言,PSI 确实有一个编辑资源分配的方法(称为 Statusing.UpdateStatus)。但不幸的是,它有一些限制:它只能一次性更改整个任务的分配。在我们的例子中,这意味着砖块数量将从 450 更改为任务的每一天,而不仅仅是周二。周一和周三的旧值将丢失,这是不可接受的。
CSOM (或 Client-side object model) 是 Project Server 2013 推荐的 API。它包含 5 个不同的 API,用于不同的目的:Microsoft .NET CSOM、Microsoft Silverlight CSOM、Windows Phone 8 CSOM、JavaScript 对象模型 (JSOM) 以及一个支持 REST 接口的 OData 服务。
我没有深入研究 Silverlight CSOM、Windows Phone CSOM 和 JSOM,因为我的目标是创建一个 .Net 控制台应用程序,所以这三个接口将在本文中不作介绍。
Microsoft .NET CSOM 在管理 Project Server 方面几乎具有与 PSI 相同的可能性。简单示例:
var eptList = projContext.LoadQuery(
projContext.EnterpriseProjectTypes.Where(
ept => ept.Name == eptName));
projContext.ExecuteQuery();
projectId = eptList.First().Id;
.Net CSOM 具有编辑资源分配的方法(它们包含在 StatusAssignment 类中),但它们与前面描述的 PSI 方法存在相同的限制:它们只能更改整个任务的分配,而不能更改特定时间段的分配。
OData (或 Open Data Protocol) 使用 REST (基于 HTTP) API 进行报告。此 API 提供的数据是只读的,因此根本无法使用 OData 在 Project Server 上更改任何内容。
OData 使用的简单示例
http://ServerName/ProjectServerName/_api/ProjectData/Projects
Microsoft.Office.Interop.MSProject.dll 是 Office 系列互操作程序集 的一部分,它提供了 MS Project 客户端应用程序可编程性的 API。Interop 程序集使用 MS Project 进程(因此,MS Project 必须在后台运行),并且在 MS Project Server 管理方面并不强大。但它可以执行用户使用 MS Project 可以做的任何事情,包括编辑特定日期的资源分配。因此,我们别无选择,只能将其用于我们的任务。
Project Interop 程序集使用的简单示例
ApplicationClass app = new ApplicationClass();
app.FileOpen(currentProject, false, Missing.Value, Missing.Value, Missing.Value, Missing.Value,
Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value,
PjPoolOpen.pjDoNotOpenPool, Missing.Value, Missing.Value, Missing.Value, Missing.Value);
foreach (Task task in wp.app.ActiveProject.Tasks)
Console.WriteLine(task.Name);
app.FileCloseEx(PjSaveType.pjDoNotSave, false, !app.ActiveProject.ReadOnly);
应用程序结构
正如我之前提到的,Interop 程序集无法执行特定于服务器的任务。因此,应用程序的不同部分将追求不同的目标并使用不同的 API。
1. 强制签入我们将要处理的项目。此步骤对于应用程序能够编辑项目是必需的。此部分将使用 PSI API。
2. 启动 MS Project 客户端应用程序并将其连接到 Project Server。连接将使用 MS Project 命令行参数建立;MS Project 将使用 System.Diagnostics.Process 程序集启动。
3. 在服务器上打开必要的项目并进行所需的更改。此步骤将使用 Microsoft.Office.Interop.MSProject.dll 程序集。
4. 保存、发布并关闭项目,然后关闭 MS Project 客户端应用程序。此步骤也将使用 Microsoft.Office.Interop.MSProject.dll 程序集。
步骤 1:强制签入
为了让 PSI 代码正常工作,我们将需要 ProjectServerServices.dll 程序集。 MS Project SDK 包含源代码、.cmd 文件和用于编译此程序集的详细手册。
SDK 还包含几个解释如何配置 Project Server 端点的示例。我们将需要两个端点:Project 和 QueueSystem。
private const string ENDPOINT_PROJECT = "basicHttp_Project";
private const string ENDPOINT_QUEUESYSTEM = "basicHttp_QueueSystem";
private static SvcProject.ProjectClient projectClient;
private static SvcQueueSystem.QueueSystemClient queueSystemClient;
public bool ForseCheckInProject(string projectName)
{
ConfigClientEndpoints(ENDPOINT_PROJECT);
ConfigClientEndpoints(ENDPOINT_QUEUESYSTEM);
...
}
// Use the endpoints defined in app.config to configure the client.
public static void ConfigClientEndpoints(string endpt)
{
if (endpt == ENDPOINT_PROJECT)
projectClient = new SvcProject.ProjectClient(endpt);
else if (endpt == ENDPOINT_QUEUESYSTEM)
queueSystemClient = new SvcQueueSystem.QueueSystemClient(endpt);
}
Project 端点将用于强制签入项目。
SvcProject.ProjectDataSet projectDs = projectClient.ReadProjectList();
foreach (SvcProject.ProjectDataSet.ProjectRow projectRow in projectDs.Project)
{
if (projectRow.PROJ_NAME == projectName)
projectId = projectRow.PROJ_UID;
}
if (projectId != Guid.Empty)
{
projectClient.QueueCheckInProject(jobId, projectId, true, sessionId, SESSION_DESC);
WaitForQueue(queueSystemClient, jobId);
return true;
}
QueueSystem 将用于等待签入操作完成。
static private void WaitForQueue(SvcQueueSystem.QueueSystemClient q, Guid jobId)
{
SvcQueueSystem.JobState jobState;
const int QUEUE_WAIT_TIME = 2; // two seconds
bool jobDone = false;
string xmlError = string.Empty;
int wait = 0;
// Wait for the project to get through the queue.
// Get the estimated wait time in seconds.
wait = q.GetJobWaitTime(jobId);
// Wait for it.
Thread.Sleep(wait * 1000);
// Wait until it is finished.
do
{
// Get the job state.
jobState = q.GetJobCompletionState(out xmlError, jobId);
if (jobState == SvcQueueSystem.JobState.Success)
{
jobDone = true;
}
else
{
if (jobState == SvcQueueSystem.JobState.Unknown)
{
jobDone = true;
Console.WriteLine("Project was already checked in, operation aborted");
}
else if (jobState == SvcQueueSystem.JobState.Failed
|| jobState == SvcQueueSystem.JobState.FailedNotBlocking
|| jobState == SvcQueueSystem.JobState.CorrelationBlocked
|| jobState == SvcQueueSystem.JobState.Canceled)
{
// If the job failed, error out.
throw (new ApplicationException("Queue request failed \"" + jobState + "\" Job ID: " + jobId +
".\r\n" + xmlError));
}
else
{
Console.WriteLine("Job State: " + jobState + " Job ID: " + jobId);
Thread.Sleep(QUEUE_WAIT_TIME*1000);
}
}
} while (!jobDone);
}
步骤 2:启动 MS Project 客户端应用程序并将其连接到 Project Server
在此之前,请确保手动使用 MS Project 连接到它时,MS Project Server 不会要求登录名和密码。这可以通过 Project Server 安全设置完成,而这些设置超出了本文的范围。
string paramList = " /s " + serverURL;
var startInfo = new ProcessStartInfo("C:\\Program Files\\Microsoft Office\\Office15\\WINPROJ.EXE", paramList);
startInfo.WindowStyle = ProcessWindowStyle.Hidden;
startInfo.UseShellExecute = false;
Process.Start(startInfo);
User32.dll 中的 FindWindow 和 FindWindowEx 方法由应用程序使用,以检查 MS Project 应用程序是否已完全加载。
WINPROJ.EXE 的位置从 Windows 注册表中提取。
步骤 3:打开项目并进行所需的更改
在调试过程中,我经常在此步骤遇到“应用程序繁忙”错误。因此,此步骤的每个小部分都包含在 try/catch 块中,并且该块被放入一个 While 循环中,每次尝试后都有一个小超时。如果 MS Project 繁忙,我们将等待必要的时间。
打开项目
ApplicationClass app = new ApplicationClass();
string currentProject = "<>\\" + projName;
app.FileOpen(currentProject, false, Missing.Value, Missing.Value, Missing.Value, Missing.Value,
Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value,
PjPoolOpen.pjDoNotOpenPool, Missing.Value, Missing.Value, Missing.Value, Missing.Value);
编辑项目/资源分配。此示例使用 1 天的时间段;它可以更改为任何其他时间段长度。
//Loop through all the tasks and resources to match what we are looking for
foreach (Task task in wp.app.ActiveProject.Tasks)
{
if (task.Name == taskName)
foreach (Assignment asgt in task.Assignments)
{
if (asgt.ResourceName == resourceName)
{
TimeScaleValues TSV = asgt.TimeScaleData(updateDate, updateDate.AddDays(1),
PjAssignmentTimescaledData.pjAssignmentTimescaledWork,
PjTimescaleUnit.pjTimescaleDays, 1);
TSV[1].Value = newValue;
}
}
}
步骤 4:保存并关闭所有内容
在实际代码中,此步骤中的每个命令都包含在其自己的 try/catch 块中。
app.FileSave();
app.PublishAllInformation();
app.FileCloseEx(PjSaveType.pjDoNotSave, false, !app.ActiveProject.ReadOnly);
app.Quit(PjSaveType.pjDoNotSave);
app = null;
结论
此应用程序是一个粗略的原型,在性能和安全性方面存在许多潜在问题。但我相信它展示了如何通过组合多个 API 来实现 MS Project Server 上的一些复杂目标。
致谢
我以 Project Server 2007 Test Data Population Tool 的源代码为基础,而 此 MSDN 论坛帖子 提供了一些有价值的见解。我非常感谢作者的出色工作。
