创建 SharePoint 托管的 Project Server 加载项

在可以为Project Online (自动托管、提供程序托管和 SharePoint 托管) 创建的三种类型的应用程序中,SharePoint 托管的应用程序是创建和部署的最简单。 SharePoint 托管的应用程序不需要 OAuth 身份验证,也不使用 Azure,也不需要维护提供程序托管资源的本地站点。 Visual Studio 中的 SharePoint 2013 应用程序 模板是开发可在 Office 应用商店中发布和销售或部署到 SharePoint 上的专用应用程序目录的应用程序的便捷框架。

在 Project 中,状态是一个过程,工作组成员可以使用Project Web App中的“任务”页提交已分配任务的状态,例如每周每天在任务上工作的时间数。 分配所有者通常 (项目经理) 可以批准或拒绝状态。 状态获得批准后,Project 会重新计算计划。 QuickStatus 应用显示分配的任务,用户可以在其中快速更新完成百分比并提交所选分配的状态以供审批。 尽管 Project Web App 中的“任务”页具有更多功能,但 QuickStatus 应用就是一个提供简化界面的示例。

QuickStatus 应用是面向开发人员的示例:它不适用于生产环境。 主要目的是显示Project Online的应用开发示例,而不是创建功能齐全的状态应用。 有关状态的更好方法,请参阅 后续步骤中的建议。

有关状态的一般信息,请参阅 任务进度。 有关开发 SharePoint 和 Project Server 加载项的详细信息,请参阅 SharePoint 外接程序

为 Project Server 2013 创建应用的先决条件

若要开发可部署到 Project Server 2013 Project Online或本地安装的相对简单的应用,可以使用 Napa,它提供联机开发环境。 对于更复杂的应用、修改Project Web App功能区以及简化开发过程中的调试,可以使用 Visual Studio 2012 或 Visual Studio 2013。 例如,通过本地安装,可以手动检查 Project Server 数据库中的更改的 Drafts 数据表。 本文介绍如何使用 Visual Studio 进行应用开发。

使用 Visual Studio 开发 Project Server 应用需要满足以下条件:

  • 确保您已在本地开发计算机上安装最新的 Service Pack 和 Windows 更新。 操作系统可以是 Windows 7、Windows 8、Windows Server 2008 或 Windows Server 2012。

  • 您必须有一台安装了 SharePoint Server 2013 和 Project Server 2013 的计算机,其中计算机已配置为应用隔离和旁加载应用程序。 通过旁加载,Visual Studio 可以暂时安装应用进行调试。 可以使用 SharePoint 和 Project Server 的本地安装。 有关详细信息,请参阅 为 SharePoint 的应用程序设置本地开发环境

    注意

    对于本地安装,请在创建公司应用目录 之前 配置独立应用域。

  • 开发计算机可以是安装了适用于 Visual Studio 2012 的 Office 开发人员工具的远程计算机。 确保已安装最新版本;请参阅 Office 和 SharePoint 下载的应用程序“工具”部分。

  • 验证用于开发和测试的 Project Web App 实例是否可在浏览器中访问。

有关使用联机工具的信息,请参阅 在 Microsoft 365 上为 SharePoint 外接程序设置开发环境。 有关为使用联机工具的 Project Server 生成简单应用的演练,请参阅 EPMSource 博客系列 生成第一个 Project Server 应用

使用 Visual Studio 创建 Project Server 应用

适用于 Visual Studio 2012 的 Office 开发人员工具包括可用于 Project Server 2013 的 SharePoint 应用程序的模板。 创建应用解决方案时,该解决方案包括自定义代码的以下文件:

  • AppManifest.xml 包括应用标题、权限请求范围和其他属性的设置。 过程 1 包括使用清单Designer设置属性的步骤。

  • 页面”文件夹中的Default.aspx是应用的main页。 过程 2 演示如何为 QuickStatus 应用添加 HTML5 内容。

  • “脚本”文件夹中的App.js 是自定义 JavaScript 代码的主文件。 过程 3 介绍了 QuickStatus 应用的 JavaScript 代码。

    如果添加商业控件(如基于 jQuery 的网格或日期选取器),则可以在 Default.aspx 文件中添加对其他 JavaScript 文件的引用。

  • Content 文件夹中的App.css 是自定义 CSS3 样式的主文件。 过程 2 和过程 3 包括有关 QuickStatus 应用的级联样式表 (CSS) 样式的信息。 可以在 Default.aspx 文件中添加对其他 CSS 文件的引用。

  • “图像”文件夹中的AppIcon.png 是应用在 Office 应用商店或应用程序目录中显示的 96 x 96 图标。

若要修改Project Web App功能区,可以添加功能区自定义操作。 QuickStatus 应用示例代码部分包括修改后的Default.aspx、App.js、App.css、Elements.xml 和 AppManifest.xml 文件的完整代码。

程序 1. 在 Visual Studio 中创建应用项目

  1. 以管理员身份运行 Visual Studio 2012,然后在“开始”页上选择“ 新建项目 ”。

  2. 在“ 新建项目 ”对话框中,展开 “模板”、“ Visual C#”和 “Office/SharePoint ”节点,然后选择“ 应用”。 使用中心窗格顶部目标框架下拉列表中的默认.NET Framework 4.5,然后选择“SharePoint 2013 应用程序” (请参阅图 1) 。

  3. “名称” 字段中,键入“QuickStatus”,浏览到要保存应用的位置,然后选择“ 确定”。

    图 1. 在 Visual Studio 中创建 Project Server 应用程序

    在 Visual Studio 中创建 Project

  4. 在“ 新建 SharePoint 应用 ”对话框中,填写以下三个字段:

    • 在顶部文本框中,键入希望应用在Project Web App中显示的名称。 例如,键入“快速状态更新”。

    • 对于要用于调试的站点,请键入 Project Web App 实例的 URL。 例如,键入 https://ServerName/ProjectServerName (将 ServerNameProjectServerName 替换为自己的值) ,然后选择 “验证”。 如果一切顺利,Visual Studio 将显示 “连接成功”。 如果收到错误消息,请确保Project Web App URL 正确,并且为 Project Server 计算机配置了应用隔离和旁加载应用。 有关详细信息,请参阅 为 Project Server 2013 创建应用的先决条件 部分。

    • 在“ 希望如何托管 SharePoint 应用 ”下拉列表中,选择“ SharePoint 托管”。

    警告

    如果错误地选择了默认 提供程序托管 的项目类型,Visual Studio 会在解决方案中创建两个项目: QuickStatus 项目和 QuickStatusWeb 项目。 如果看到两个项目,请删除该解决方案,然后重新开始。

  5. 选择 “确定” 以创建 QuickStatus 解决方案、 QuickStatus 项目和默认文件。

  6. 例如,打开“清单Designer”视图 (双击 AppManifest.xml 文件) 。 在“ 常规 ”选项卡上,“ 标题 ”文本框应显示你在步骤 4 中键入的应用名称。 选择“ 权限 ”选项卡,为应用添加以下权限请求 (请参阅图 2) :

    • “权限请求” 列表的第一行的“ 作用域 ”列中,选择下拉列表中的“ 状态 ”。 在 “权限” 列中,选择“ SubmitStatus”。

    • 添加一行,其中 “范围 ”为 “多个项目 ”, “权限 ”为 “读取”。

    图 2. 为状态应用程序设置权限范围

    设置状态应用的权限范围

使用 QuickStatus 应用,Project Web App用户可以从多个项目中读取该用户的分配、更改分配完成百分比并提交更新。 此应用不需要图 2 下拉列表中显示的其他权限请求范围。 权限请求范围是应用代表用户请求的权限。 如果用户在 Project Web App 中没有这些权限,则应用不会运行。 一个应用可以有多个权限请求范围,包括其他 SharePoint 权限的权限请求范围,但只应具有应用程序功能所需的最低权限范围。 下面是与 Project Server 相关的权限请求范围:

  • 企业资源:资源管理器权限,用于读取或写入有关其他Project Web App用户的信息。

  • 多个项目:读取或写入多个项目,其中用户具有请求的权限。

  • Project Server:要求应用用户具有Project Web App的管理员权限。

  • 报告:读取Project Web App (的 ProjectData OData 服务只需要Project Web App) 的登录权限。

  • 单个项目:读取或写入用户具有所请求权限的项目。

  • 状态:提交工作分配状态的更新,例如工作时间、完成百分比和新分配。

  • 工作流:如果用户有权运行 Project Server 工作流,则应用会以提升的工作流权限运行。

有关 Project Server 2013 的权限请求范围的详细信息,请参阅 Project 2013 中面向开发人员的汇报SharePoint 2013 中的应用权限中的 Project apps 部分。

为 QuickStatus 应用创建 HTML 内容

在开始编码 HTML 内容之前,请设计 QuickStatus 应用的用户界面和用户体验 (图 3 显示了已完成页面) 的示例。 设计还可以包含与 HTML 代码交互的 JavaScript 函数的大纲。 有关常规信息,请参阅 SharePoint 2013 中应用的 UX 设计

图 3. QuickStatus 应用程序页面的设计

QuickStatus 应用页

应用在顶部显示显示名称,这是 AppManifest.xml 中 Title 元素的值。

默认情况下,页面使用 HTML5。 下面是 QuickStatus 应用在页面正文中包含的main UI 对象的标准 HTML 元素:

  • 窗体元素包含所有其他 UI 元素。

  • fieldset 元素为工作分配表创建容器和边框;子图例元素为容器提供标签。

  • table 元素包含一个描述文字,并且仅包含一个表标题。 JavaScript 函数更改表描述文字并为赋值添加行。

    注意

    为了轻松添加分页和排序,生产应用可能会使用基于 jQuery 的商业网格控件,而不是表。

    该表包括项目名称、具有检查框的任务名称、实际工时、完成百分比、剩余工时和工作分配完成日期的列。 JavaScript 函数为每个任务的完成百分比创建检查框和文本输入字段。

  • 文本框的 输入 元素设置所有选定作业的完成百分比。

  • 按钮元素提交状态更改。

  • 按钮元素刷新页面。

  • 按钮元素退出应用并返回到 Project Web App 中的“任务”页。

底部文本框和按钮元素位于 div 元素中,因此 CSS 可以轻松管理 UI 对象的位置和外观。 JavaScript 函数在页面底部添加一个段落,其中包含状态更新成功或失败的结果。

程序 2. 创建 HTML 内容

  1. 在 Visual Studio 中,打开 Default.aspx 文件。

    该文件包括两个 asp:Content 元素:具有 属性的 ContentPlaceHolderID="PlaceHolderAdditionalPageHead" 元素添加到页眉内,具有 属性的 ContentPlaceHolderID="PlaceHolderMain" 元素放置在页面 正文 元素中。

  2. <asp:Content ContentPlaceHolderID="PlaceHolderAdditionalPageHead" runat="server"> 页眉的 控件中,添加对 Project Server 计算机上的 PS.js 文件的引用。 对于测试和调试,可以使用 PS.debug.js。

      <script type="text/javascript" src="/_layouts/15/ps.debug.js"></script>
    

    应用程序基础结构在 /_layouts/15/ IIS 中使用 SharePoint 站点的虚拟目录。 物理文件为 %ProgramFiles%\Common Files\Microsoft Shared\Web Server Extensions\15\TEMPLATE\LAYOUTS\PS.debug.js

    注意

    在部署用于生产的应用之前,请从脚本引用中删除 .debug 以提高性能。

  3. <asp:Content ContentPlaceHolderID="PlaceHolderMain" runat="server"> 页面正文的 控件中,删除生成的 div 元素,然后添加 UI 对象的 HTML 代码。 table 元素仅包含标题行。 “任务名称”列包含检查框输入控件。 描述文字 元素的文本替换为 App.js 文件中 getUserInfo 函数的 onGetUserNameSuccess 回调。

    <form>
        <fieldset>
        <legend>Select assigned tasks</legend>
        <table id="assignmentsTable">
            <caption id="tableCaption">Replace caption</caption>
            <thead>
            <tr id="headerRow">
                <th>Project name</th>
                <th><input type="checkbox" id="headercheckbox" checked="checked" />Task name</th>
                <th>Actual work</th>
                <th>% complete</th>
                <th>Remaining work</th>
                <th>Due date</th>
            </tr>
            </thead>
        </table>
        </fieldset>
        <div id="inputPercentComplete" >
        Set percent complete for all selected assignments, or leave this
        <br /> field blank and set percent complete for individual assignments: 
        <input type="text" name="percentComplete" id="pctComplete" size="4"  maxlength="4" />
        </div>
        <div id="submitResult">
        <p><button id="btnSubmitUpdate" type="button" class="bottomButtons" ></button></p>
        <p id="message"></p>
        </div>
        <div id="refreshPage">
        <p><button id="btnRefresh" type="button" class="bottomButtons" >Refresh</button></p>
        </div>
        <div id="exitPage">
        <p><button id="btnExit" type="button" class="bottomButtons" >Exit</button></p>
        </div>
    </form>
    
  4. 在 App.css 文件中,为 UI 元素的位置和外观添加 CSS 代码。 有关 QuickStatus 应用的完整 CSS 代码,请参阅 QuickStatus 应用的示例代码 部分。

过程 3 添加 JavaScript 函数以读取分配和创建表行,以及更改和更新分配完成百分比。 在开发应用时,实际步骤更具迭代性,其中你交替创建一些 HTML 代码,添加和测试相关样式和 JavaScript 函数,修改或添加更多 HTML 代码,然后重复此过程。

为 QuickStatus 应用创建 JavaScript 函数

SharePoint 应用的 Visual Studio 模板包含 App.js 文件,该文件包含获取 SharePoint 客户端上下文的默认初始化代码,并演示应用程序页面的基本获取和设置操作。 SharePoint 客户端 SP.js 库的 JavaScript 命名空间是 SP。 由于 Project Server 应用使用 PS.js 库,因此应用使用 PS 命名空间获取客户端上下文并访问适用于 Project Server 的 JSOM。

QuickStatus 应用中的 JavaScript 函数包括以下内容:

  • 文档 就绪 事件处理程序在文档对象模型 (DOM) 实例化时运行。 就绪事件处理程序执行以下四个步骤:

    1. 使用 Project Server JSOM 的客户端上下文和 pwaWeb 全局变量初始化 projContext 全局变量。

    2. 调用 getUserInfo 函数以初始化 projUser 全局变量。

    3. 调用 getAssignments 函数,该函数获取用户的指定分配数据。

    4. 将单击事件处理程序绑定到表标题检查框,以及表的每一行中的检查框。 当用户选择或清除表中的任何检查框时,单击事件处理程序将管理检查框的 checked 属性。

  • 如果 getAssignments 函数成功,它将调用 onGetAssignmentsSuccess 函数。 该函数在每个分配的表中插入一行,初始化每行中的 HTML 控件,然后初始化底部按钮属性。

  • 更新”按钮的 onClick 事件处理程序调用 updateAssignments 函数。 该函数获取应用于每个选定分配的完成百分比值;或者,如果“完成百分比”文本框为空,则函数获取表中每个选定工作分配的完成百分比。 然后 ,updateAssignments 函数保存并提交状态更新,并将有关结果的消息写入页面底部。

过程 3. 创建 JavaScript 函数

  1. 在 Visual Studio 中,打开 App.js 文件,然后删除该文件中的所有内容。

  2. 添加全局变量和文档 就绪 事件处理程序。 使用 jQuery 函数访问 文档 对象。

    表标题检查框的单击事件处理程序设置行检查框的选中状态。 如果选中所有行检查框或全部已清除,则行检查框的单击事件处理程序将设置标题检查框的选中状态。 单击事件处理程序还会将页面底部的结果消息设置为空字符串。

     var projContext;
     var pwaWeb;
     var projUser;
     // This code runs when the DOM is ready and creates a ProjectContext object.
     // The ProjectContext object is required to use the JSOM for Project Server.
     $(document).ready(function () {
         projContext = PS.ProjectContext.get_current();
         pwaWeb = projContext.get_web();
         getUserInfo();
         getAssignments();
         // Bind a click event handler to the table header check box, which sets the row check boxes
         // to the checked state of the header check box, and sets the results message to an empty string.
         $('#headercheckbox').live('click', function (event) {
             $('input:checkbox:not(#headercheckbox)').attr('checked', this.checked);
             $get("message").innerText = "";
         });
         // Bind a click event handler to the row check boxes. If any row check box is cleared, clear
         // the header check box. If all of the row check boxes are selected, select the header check box.
         $('input:checkbox:not(#headercheckbox)').live('click', function (event) {
             var isChecked = true;
             $('input:checkbox:not(#headercheckbox)').each(function () {
                 if (this.checked == false) isChecked = false;
                 $get("message").innerText = "";
             });
             $("#headercheckbox").attr('checked', isChecked);
         });
     });
    
  3. 添加 getUserInfo 函数,如果查询成功,该函数将调用 onGetUserNameSuccessonGetUserNameSuccess 函数将描述文字段落的内容替换为包含用户名的表描述文字。

         // Get information about the current user.
         function getUserInfo() {
             projUser = pwaWeb.get_currentUser();
             projContext.load(projUser);
             projContext.executeQueryAsync(onGetUserNameSuccess,
                 // Anonymous function to execute if getUserInfo fails.
                 function (sender, args) {
                     alert('Failed to get user name. Error: ' + args.get_message());
             });
         } 
         // This function is executed if the getUserInfo call is successful.
         function onGetUserNameSuccess() {
             var prefaceInfo = 'Assignments for ' + projUser.get_title();
             $('#tableCaption').text(prefaceInfo);
         }
    
  4. 添加 getAssignments 函数,该函数调用 onGetAssignmentsSuccess (如果分配查询成功,请参阅步骤 5) 。 “包含”选项将查询限制为仅返回指定的字段。

     // Get the collection of assignments for the current user.
     function getAssignments() {
         assignments = PS.EnterpriseResource.getSelf(projContext).get_assignments();
         // Register the request that you want to run on the server. The optional "Include" parameter 
         // requests only the specified properties for each assignment in the collection.
         projContext.load(assignments,
             'Include(Project, Name, ActualWork, ActualWorkMilliseconds, PercentComplete, RemainingWork, Finish, Task)');
         // Run the request on the server.
         projContext.executeQueryAsync(onGetAssignmentsSuccess,
             // Anonymous function to execute if getAssignments fails.
             function (sender, args) {
                 alert('Failed to get assignments. Error: ' + args.get_message());
             });
     }
    
  5. 添加 onGetAssignmentsSuccess 函数,该函数为表中的每个赋值添加一行。 prevProjName 变量用于确定行是否适用于其他项目。 如果是这样,项目名称以加粗字体显示;如果没有,则项目名称设置为空字符串。

    注意

    JSOM 不包括 CSOM 包含的 TimeSpan 属性,例如 ActualWorkTimeSpan。 相反,JSOM 使用毫秒数的属性,例如 PS。StatusAssignment.actualWorkMilliseconds 属性。 获取该属性的方法是 get_actualWorkMilliseconds,它返回整数值。 >get_actualWork 方法返回一个字符串,例如“3h”。 可以在 QuickStatus 应用中使用任一值,但以不同的方式显示它。 赋值查询包含这两个属性,因此可以在调试期间测试值。 如果删除 actualWork 变量,还可以删除赋值查询中的 ActualWork 属性。

    最后, onGetAssignmentsSuccess 函数使用单击事件处理程序初始化 “更新 ”按钮和 “刷新 ”按钮。 也可以在 HTML 代码中设置 “更新 ”按钮的文本值。

         // Get the enumerator, iterate through the assignment collection, 
         // and add each assignment to the table.
         function onGetAssignmentsSuccess(sender, args) {
             if (assignments.get_count() > 0) {
                 var assignmentsEnumerator = assignments.getEnumerator();
                 var projName = "";
                 var prevProjName = "3D2A8045-4920-4B31-B3E7-9D0C5195FC70"; // Any unique name.
                 var taskNum = 0;
                 var chkTask = "";
                 var txtPctComplete = "";
                 // Constants for creating input controls in the table.
                 var INPUTCHK = '<input type="checkbox" class="chkTask" checked="checked" id="chk';
                 var LBLCHK = '<label for="chk';
                 var INPUTTXT = '<input type="text" size="4"  maxlength="4" class="txtPctComplete" id="txt';
                 while (assignmentsEnumerator.moveNext()) {
                     var statusAssignment = assignmentsEnumerator.get_current();
                     projName = statusAssignment.get_project().get_name();
                     // Get an integer, such as 3600000.
                     var actualWorkMilliseconds = statusAssignment.get_actualWorkMilliseconds(); 
                     // Get a string, such as "1h". Not used here.
                     var actualWork = statusAssignment.get_actualWork();
                     if (projName === prevProjName) {
                         projName = "";
                     }
                     prevProjName = statusAssignment.get_project().get_name();
                     // Create a row for the assignment information.
                     var row = assignmentsTable.insertRow();
                     taskNum++;
                     // Create an HTML string with a check box and task name label, for example:
                     // <input type="checkbox" class="chkTask" checked="checked" id="chk1" /> <label for="chk1">Task 1</label>
                     chkTask = INPUTCHK + taskNum + '" /> ' + LBLCHK + taskNum + '">' 
                         + statusAssignment.get_name() + '</label>';
                     txtPctComplete = INPUTTXT + taskNum + '" />';
                     // Insert cells for the assignment properties.
                     row.insertCell().innerHTML = '<strong>' + projName + '</strong>';
                     row.insertCell().innerHTML = chkTask;
                     row.insertCell().innerText = actualWorkMilliseconds / 3600000 + 'h';
                     row.insertCell().innerHTML = txtPctComplete;
                     row.insertCell().innerText = statusAssignment.get_remainingWork();
                     row.insertCell().innerText = statusAssignment.get_finish();
                     // Initialize the percent complete cell.
                     $get("txt" + taskNum).innerText = statusAssignment.get_percentComplete() + '%'
                 }
             }
             else {
                 $('p#message').attr('style', 'color: #0f3fdb');     // Blue text.
                 $get("message").innerText = projUser.get_title() + ' has no assignments'
             }
             // Initialize the button properties.
             $get("btnSubmitUpdate").onclick = function() { updateAssignments(); };
             $get("btnSubmitUpdate").innerText = 'Update';
             $get('btnRefresh').onclick = function () { window.location.reload(true); };
             $get('btnExit').onclick = function () { exitToPwa(); };
         }
    
  6. 为“更新”按钮添加 updateAssignments click 事件处理程序。 当用户更改任务完成百分比的值或在 percentComplete 文本框中添加值时,可以采用多种格式输入该值,例如“60”、“60%”或“60 %。 getNumericValue 方法返回输入文本的数值。

    注意

    在设计用于生产用途的应用中,数值信息的输入值应包括字段验证和其他错误检查。

    updateAssignments 示例包括一些基本的错误检查,并在页面底部的消息段落中显示信息-如果更新查询成功,则为绿色,如果存在输入错误或更新查询不成功,则为红色。

    在使用 submitAllStatusUpdates 方法之前,应用必须使用 PS 将更新保存到服务器 。StatusAssignmentCollection.update 方法。

         // Update all checked assignments. If the bottom percent complete field is blank,
         // use the value in the % complete field of each selected row in the table.
         function updateAssignments() {
             // Get percent complete from the bottom text box.
             var pctCompleteMain = getNumericValue($('#pctComplete').val()).trim();
             var pctComplete = pctCompleteMain;
             var assignmentsEnumerator = assignments.getEnumerator();
             var taskNum = 0;
             var taskRow = "";
             var indexPercent = "";
             var doSubmit = true;
             while (assignmentsEnumerator.moveNext()) {
                 var pctCompleteRow = "";
                 taskRow = "chk" + ++taskNum;
                 if ($get(taskRow).checked) {
                     var statusAssignment = assignmentsEnumerator.get_current();
                     if (pctCompleteMain === "") {
                         // Get percent complete from the text box field in the table row.
                         pctCompleteRow = getNumericValue($('#txt' + taskNum).val());
                         pctComplete = pctCompleteRow;
                     }
                     // If both percent complete fields are empty, show an error.
                     if (pctCompleteMain === "" && pctCompleteRow === "") {
                         $('p#message').attr('style', 'color: #e11500');     // Red text.
                         $get("message").innerHTML =
                             '<b>Error:</b> Both <i>Percent complete</i> fields are empty, in row '
                             + taskNum
                             + ' and in the bottom textbox.<br/>One of those fields must have a valid percent.'
                             + '<p>Please refresh the page and try again.</p>';
                         doSubmit = false;
                         taskNum = 0;
                         break;
                     }
                     if (doSubmit) statusAssignment.set_percentComplete(pctComplete);
                 }
             } 
             // Save and submit the assignment updates.
             if (doSubmit) {
                 assignments.update();
                 assignments.submitAllStatusUpdates();
                 projContext.executeQueryAsync(function (source, args) {
                     $('p#message').attr('style', 'color: #0faa0d');     // Green text.
                     $get("message").innerText = 'Assignments have been updated.';
                 }, function (source, args) {
                     $('p#message').attr('style', 'color: #e11500');     // Red text.
                     $get("message").innerText = 'Error updating assignments: ' + args.get_message();
                 });
             }
         }
         // Get the numeric part for percent complete, from a string. For example, with "20 %", return "20".
         function getNumericValue(pctComplete) {
             pctComplete = pctComplete.trim();
             pctComplete = pctComplete.replace(/ /g, "");    // Remove interior spaces.
             indexPercent = pctComplete.indexOf('%', 0);
             if (indexPercent > -1) pctComplete = pctComplete.substring(0, indexPercent);
             return pctComplete;
         }
    
  7. 添加 exitToPwa 函数,该函数使用主机Project Web App站点的 URL 的 SPHostUrl 查询字符串参数。 若要导航回“任务”页,请追加 "/Tasks.aspx" 到 URL。 例如, spHostUrl 变量将设置为 https://ServerName/ProjectServerName/Tasks.aspx

    getQueryStringParameter 函数拆分 QuickStatus 页的 URL,以提取并返回 URL 选项中的指定参数。 下面是 文档的示例。QuickStatus 文档的 URL 值 (一行) :

     https://app-ef98082fa37e3c.servername.officeapps.selfhost.corp.microsoft.com/pwa/
         QuickStatus/Pages/Default.aspx
         ?SPHostUrl=https%3A%2F%2Fsphvm%2D85178%2Fpwa
         &SPLanguage=en%2DUS
         &SPClientTag=1
         &SPProductNumber=15%2E0%2E4420%2E1022
         &SPAppWebUrl=https%3A%2F%2Fapp%2Def98082fa37e3c%2Eservername
             %2Eofficeapps%2Eselfhost%2Ecorp%2Emicrosoft%2Ecom%2Fpwa%2FQuickStatus
    

    对于前面的 URL,getQueryStringParameter 函数返回 SPHostUrl 查询字符串值 https://ServerName/pwa

         // Exit the QuickStatus page and go back to the Tasks page in Project Web App.
         function exitToPwa() {
             // Get the SharePoint host URL, which is the top page of PWA, and add the Tasks page.
             var spHostUrl = decodeURIComponent(getQueryStringParameter('SPHostUrl'))
                             + "/Tasks.aspx";
             // Set the top window for the QuickStatus IFrame to the Tasks page.
             window.top.location.href = spHostUrl;
         }
         // Get a specified query string parameter from the {StandardTokens} URL option string.
         function getQueryStringParameter(urlParameterKey) {
             var docUrl = document.URL;
             var params = docUrl.split('?')[1].split('&');
             for (var i = 0; i < params.length; i++) {
                 var theParam = params[i].split('=');
                 if (theParam[0] == urlParameterKey)
                     return decodeURIComponent(theParam[1]);
             }
         }
    

如果此时发布 QuickStatus 应用并将其添加到Project Web App,则可以从“网站内容”页面运行该应用,但用户不容易使用它。 为了帮助用户查找和运行应用,可以在“任务”页上将应用的按钮添加到功能区。 过程 4 演示如何添加功能区自定义操作。

添加功能区自定义操作

Project Web App的功能区选项卡、组和控件在 pwaribbon.xml 文件中指定,该文件安装在[Program Files]\Common Files\Microsoft Shared\Web Server Extensions\15\TEMPLATE\FEATURES\PWARibbon\listtemplates运行 Project Server 的计算机上的目录中。 为了帮助设计Project Web App功能区的自定义操作,Project 2013 SDK 下载包括 pwaribbon.xml 的副本。

Project Web App对“任务”页使用不同的功能区定义,具体取决于Project Web App实例是否使用单一输入模式,该模式允许用户为时间表和任务状态输入值。 如果对Project Web App具有管理权限,若要确定进入模式,请在页面右上角的下拉菜单中选择“PWA 设置”。 在“PWA 设置”页上,选择“时间表设置”和“默认值”,然后查看页面底部的“单一输入模式检查”框。

当单一输入模式处于关闭状态时,“任务”页上的功能区由“我的工作”区域定义,pwaribbon.xml:

   <!-- REGION My Work Ribbon-->
   <CustomAction
      Id="Ribbon.ContextualTabs.MyWork"
      . . .

当单一输入模式处于打开状态时,“任务”页面功能区由 pwaribbon.xml 中的“绑定模式”区域定义:

   <!-- REGION Tied Mode Ribbon-->
   <CustomAction
      Id="Ribbon.ContextualTabs.TiedMode"
      . . .

尽管每个区域中的组和控件看起来相似,但绑定模式的控件可以调用与非绑定模式的相同控件不同的函数。 过程 4 显示当单一输入模式处于关闭状态时如何为 QuickStatus 应用添加按钮控件, (“单一输入模式检查”框已清除) 。

注意

有关将自定义操作添加到功能区或 SharePoint 应用程序中的菜单的常规信息,请参阅 创建自定义操作以使用 SharePoint 应用进行部署

过程 4. 将功能区自定义操作添加到“任务”页

  1. 检查 Project Web App 中的“任务”页上的功能区。 选择功能区上的“ 任务 ”选项卡,并计划如何对其进行修改。 有七个组,例如“提交”、“任务”和“时间段”。 “提交”组有两个控件:“保存”按钮和“发送状态”下拉菜单。 可以在组中的任何位置添加控件,在 “任务 ”选项卡的任何位置添加具有新控件的组,或添加具有自定义组和控件的另一个功能区选项卡。 在此示例中,我们将第三个按钮添加到 “提交 ”组,其中按钮调用 QuickStatus 应用的 URL。

  2. 在 Visual Studio 的“解决方案资源管理器”窗格中,右键单击“QuickStatus”项目,然后添加新项。 在“ 添加新项 ”对话框中,选择“ 功能区自定义操作” (请参阅图 4) 。 例如,将自定义操作 RibbonQuickStatusAction 命名为 ,然后选择“ 添加”。

    图 4. 添加功能区自定义操作

    添加功能区自定义操作

  3. “为功能区创建自定义操作” 向导的第一页上,保持选中 “主机 Web ”选项,在自定义操作范围的下拉列表中选择“ ”,然后选择“ 下一步 ” (请参阅图 5) 。 下拉列表中的项与 SharePoint 相关,而不是与 Project Server 相关。 我们将替换自定义操作的大部分生成的 XML,以便将其应用于 Project Server。

    图 5. 指定功能区自定义操作的属性

    指定功能区自定义操作的属性

  4. “为功能区创建自定义操作” 向导的下一页上,保留设置的所有默认值,然后选择“ 完成” (请参阅图 6) 。 Visual Studio 创建 RibbonQuickStatusAction 文件夹,其中包含 Elements.xml 文件。

    图 6. 指定按钮控件的设置

    指定按钮控件的设置

  5. 修改功能区自定义操作的 Elements.xml 文件中的默认生成代码。 下面是默认的 XML 代码:

     <?xml version="1.0" encoding="utf-8"?>
     <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
         <CustomAction Id="21ea3aaf-79e5-4aac-9479-8eef14b4d9df.RibbonQuickStatusAction"
                     Location="CommandUI.Ribbon"
                     Sequence="10001"
                     Title="Invoke &apos;RibbonQuickStatusAction&apos; action">
         <CommandUIExtension>
             <!-- 
             Update the UI definitions below with the controls and the command actions
             that you want to enable for the custom action.
             -->
             <CommandUIDefinitions>
             <CommandUIDefinition Location="Ribbon.ListItem.Actions.Controls._children">
                 <Button Id="Ribbon.ListItem.Actions.RibbonQuickStatusActionButton"
                         Alt="Request RibbonQuickStatusAction"
                         Sequence="100"
                         Command="Invoke_RibbonQuickStatusActionButtonRequest"
                         LabelText="Request RibbonQuickStatusAction"
                         TemplateAlias="o1"
                         Image32by32="_layouts/15/images/placeholder32x32.png"
                         Image16by16="_layouts/15/images/placeholder16x16.png" />
             </CommandUIDefinition>
             </CommandUIDefinitions>
             <CommandUIHandlers>
             <CommandUIHandler Command="Invoke_RibbonQuickStatusActionButtonRequest"
                                 CommandAction="~appWebUrl/Pages/Default.aspx"/>
             </CommandUIHandlers>
         </CommandUIExtension >
         </CustomAction>
     </Elements>
    
    1. CustomAction 元素中,删除 Sequence 属性和 Title 属性。

    2. 若要将控件添加到 Submit 组,请在 pwaribbon.xml 文件中查找集合中的 Ribbon.ContextualTabs.MyWork.Home.Groups 第一个组,该文件是开头的 <Group Id="Ribbon.ContextualTabs.MyWork.Home.Page" Command="PageGroup" Sequence="10" Title="$Resources:pwafeatures,PAGE_PDP_CM_SUBMIT"元素。 若要将子控件添加到 Submit 组,以下代码显示 Elements.xml 文件中 CommandUIDefinition 元素的正确 Location 属性:

        <CommandUIDefinitions>
          <CommandUIDefinition Location="Ribbon.ContextualTabs.MyWork.Home.Page.Controls._children">
             . . .
          </CommandUIDefinition>
        </CommandUIDefinitions>
      
    3. 更改子 Button 元素的属性值,如下所示:

           <Button Id="Ribbon.ContextualTabs.MyWork.Home.Page.QuickStatus"
                   Alt="Quick Status app"
                   Sequence="30"
                   Command="Invoke_QuickStatus"
                   LabelText="Quick Status"
                   TemplateAlias="o1"
                   Image16by16="_layouts/15/1033/images/ps16x16.png" 
                   Image16by16Left="-80"
                   Image16by16Top="-144"
                   Image32by32="_layouts/15/1033/images/ps32x32.png" 
                   Image32by32Left="-32"
                   Image32by32Top="-288" 
                   ToolTipTitle="QuickStatus"
                   ToolTipDescription="Run the QuickStatus app" />
      
      • 若要使按钮成为组中的第三个控件, Sequence 属性可以大于 Sequence="20" 现有 “发送状态” 控件的值, (这是 pwaribbon.xml) 中的 FlyoutAnchor 元素。 按照约定,组和控件的序列号为 10, 20, 30, …,这允许将元素插入中间位置。

      • Command 属性指定要在 CommandUIHandler 元素中运行的命令 (请参阅以下步骤 5.d) 。 可以简化命令名称,以便为下一个开发人员简化命令名称。 例如 Command="Invoke_QuickStatus" ,比 Command="Invoke_RibbonQuickStatusActionButtonRequest"更易于阅读。

      • 图像属性指定按钮控件的 16 x 16 像素图标和 32 x 32 像素图标。 在默认 Elements.xml 文件中, Image32by32="_layouts/15/images/placeholder32x32.png" 指定一个橙色点。 可以从运行 Project Server 的计算机上的目录中安装 [Program Files]\Common Files\Microsoft Shared\Web Server Extensions\15\TEMPLATE\LAYOUTS\1033\IMAGES 的图像映射文件 (ps16x16.png 和 ps32x32.png) 中提取图标。 例如,32 x 32 像素图标位于左侧的第二列图标中,从 ps32x32.png 图像地图顶部向下的第十行 (图标顶部位于第九行的末尾;9 行 x 32 像素/行 = 288 像素) 。

      • 若要显示按钮控件的工具提示,请添加 ToolTipTitle 属性和 ToolTipDescription 属性。

    4. 更改 CommandUIHandler 元素的属性。 例如,确保 Command 属性与 Button 元素的 Command 属性值匹配。 对于 CommandAction 属性, ~appWebUrlQuickStatus 网页 URL 的占位符。 当功能区按钮调用 QuickStatus 应用时, {StandardTokens} 令牌将替换为 URL 选项,其中包括 SPHostUrlSPLanguageSPClientTagSPProductNumberSPAppWebUrl

          <CommandUIHandlers>
              <CommandUIHandler Command="Invoke_QuickStatus"
                                CommandAction="~appWebUrl/Pages/Default.aspx?{StandardTokens}"/>
          </CommandUIHandlers>
      
  6. 解决方案资源管理器中,打开 Feature1.feature 设计器,并将 RibbonQuickStatusAction 项从“解决方案”窗格中的“项”移动到“功能”窗格中的“项”。 如果随后打开 Package.package 设计器, RibbonQuickStatusAction 项将位于 “包”窗格中的“项 ”中。

开发应用并添加功能区按钮时,通常会测试应用并在 JavaScript 代码中设置断点进行调试。 按 F5 开始调试时,Visual Studio 会编译应用,将其部署到 QuickStatus 项目的“网站 URL”属性中指定的站点,并显示一个询问你是否信任该应用的页面。 继续并退出 QuickStatus 应用时,它会返回到 Project Web App 中的“任务”页。

注意

图 7 显示功能区“任务”选项卡上的“快速状态”按钮已禁用。 使用 Visual Studio 进行多次调试部署后,在继续在同一测试服务器上调试或部署已发布的应用时,可能会阻止自定义功能区控件。 若要启用按钮,请删除 Visual Studio 中的 RibbonQuickStatusAction 项,然后创建具有不同名称和 ID 的新功能区操作。 如果这不能解决问题,请尝试从 Project Web App 测试实例中删除应用,然后使用不同的应用 ID 重新创建应用。

图 7. 查看禁用的“快速状态”按钮的工具提示

查看已禁用按钮的工具提示

过程 5 演示如何部署和安装 QuickStatus 应用。 过程 6 显示了在安装应用后测试应用的其他一些步骤。

部署 QuickStatus 应用

可通过多种方法将应用程序部署到 SharePoint Web 应用程序,例如Project Web App。 使用哪种部署将取决于是要将应用程序发布到专用 SharePoint 目录还是公共 Office 应用商店,以及 SharePoint 是在本地安装还是联机租户。 过程 5 演示如何将 QuickStatus 应用部署到专用应用目录中的本地安装。 有关详细信息,请参阅 安装和管理适用于 SharePoint 2013 的应用程序和 发布 SharePoint 应用程序

注意

将应用程序添加到 SharePoint 目录需要 SharePoint 管理员权限。

过程 5. 部署 QuickStatus 应用

  1. 在 Visual Studio 中保存所有文件,然后右键单击解决方案资源管理器中的 QuickStatus 项目,然后选择“发布”。

  2. 由于 QuickStatus 应用是 SharePoint 托管的,因此发布 (请参阅图 8) 。 在 “发布 Office 和 SharePoint 应用 ”对话框中,选择“ 完成”。

    图 8. 发布 QuickStatus 应用

    使用发布向导

  3. 将 QuickStatus.app 文件从 ~\QuickStatus\bin\Debug\app.publish\1.0.0.0 目录复制到本地计算机上的便捷目录 (或 SharePoint 计算机,以便进行本地安装) 。

  4. 在 SharePoint 管理中心中,在“快速启动”中选择“ 应用程序 ”,然后选择“ 管理应用程序目录”。

  5. 如果应用程序目录不存在,请遵循在 SharePoint 2013 中管理应用程序目录中为 Web 应用程序配置应用程序目录网站部分,为应用程序目录创建网站集。

    如果存在应用程序目录,请导航到“管理应用程序目录”页上的网站 URL。 例如,在以下步骤中,应用目录网站为 https://ServerName/sites/TestApps

  6. 在应用程序目录页上,在“快速启动”中选择 “SharePoint 应用程序 ”。 在“SharePoint 应用程序”页上,在功能区的“ 文件 ”选项卡上,选择“ 上传文档”。

  7. 在“ 添加文档 ”对话框中,浏览 QuickStatus.app 文件,添加版本的注释,然后选择“ 确定”。

  8. 添加应用时,还可以为应用说明、图标和其他信息添加本地信息。 在 “SharePoint 应用程序 - QuickStatus.app ”对话框中,添加要在 SharePoint 网站集中为应用程序显示的信息。 例如,添加以下信息:

    1. “简短说明” 字段:键入“快速状态测试应用”。

    2. “说明 ”字段:键入“测试应用”以更新多个项目中任务的完成百分比。

    3. 图标 URL 字段:将应用图标的 96 x 96 像素图像添加到应用程序目录的网站资产。 例如,导航到 https://ServerName/sites/TestApps,在“设置”下拉菜单中选择“网站内容”,选择“网站资产”,然后添加 quickStatusApp.png 图像。 右键单击 quickStatusApp 项,选择 “属性”,然后在“属性”对话框中复制 “地址 (URL) 。 例如,复制 https://ServerName/sites/TestApps/SiteAssets/QuickStatusApp.png,然后将该值粘贴到 “图标 URL Web 地址”字段中。 键入图标的说明,例如 (如图 9) 中所示,键入 QuickStatus 应用图标。 测试 URL 是否有效。

      图 9. 为 QuickStatus 应用添加图标 URL

      在 SharePoint 中为应用

    4. 类别 字段:选择现有类别,或指定自己的值。 例如,键入 Statusing。

      注意

      名为 “状态”的 类别仅用于测试目的。 Project Server 应用的典型类别是 “项目管理”。

    5. 发布者名称 字段:键入发布者的名称。 在此示例中,键入 Project SDK。

    6. 已启用字段:若要使应用对安装Project Web App站点管理员可见,请选中“启用检查”框。

    7. 其他字段是可选的。 例如,可以为应用详细信息页添加支持 URL 和多个帮助图像。 在图 9 中, “图像 URL 1 ”字段包括应用屏幕截图的 URL 和屏幕截图的说明。

    8. “SharePoint 应用程序 - QuickStatus.app ”对话框中,选择“ 保存”。 在图 9 中,“SharePoint 应用”库中的“快速状态更新”项已签出以供编辑,因此,在对话框功能区的“编辑”选项卡上,可以选择Check In完成该过程 (请参阅图 10) 。

      图 10. QuickStatus 应用将添加到 Apps for SharePoint 库中。

      将 QuickStatus 应用程序添加到 SharePoint

  9. 在Project Web App的“设置”下拉菜单中,选择“添加应用”。 在“你的应用”页上的“快速启动”中,选择“从你的组织”,然后选择“快速状态更新”应用的应用详细信息。 图 11 显示了详细信息页,其中包含在上一步中添加的应用图标、屏幕截图和其他信息。

    图 11. 使用 Project Web App 中的“快速状态更新详细信息”页

    将 QuickStatus 应用添加到 Project Web App

  10. 在“快速状态更新详细信息”页上,选择“ 添加 IT”。 Project Web App显示一个对话框,其中列出了 QuickStatus 应用可以执行的操作 (请参阅图 12) 。 操作列表派生自 AppManifest.xml 文件中的 AppPermissionRequest 元素。

    图 12. 验证你是否信任“快速状态”应用

    验证 QuickStatus 应用的信任

  11. 在“ 是否信任快速状态更新 ”对话框中,选择“ 信任它”。 应用将添加到“Project Web App网站内容”页 (见图 13) 。

    图 13. 在“网站内容”页上查看“快速状态”应用

    在“网站内容”中查看 QuickStatus 应用

在“网站内容”页上,可以选择“ 快速状态更新 ”图标来运行应用。

注意

有关提供有关应用信息的其他命令,请在“网站内容”页上,选择包含快速状态更新名称和省略号 (...) 的区域。你可以查看应用的“关于”页面,查看包含应用错误相关信息的“应用详细信息”页,查看应用权限页面,或者从Project Web App中删除应用。

在Project Web App (的“任务”页上,请参阅图 14) ,功能区上应启用“QuickStatus”按钮。 如果禁用了“ 快速状态 ”按钮,请尝试图 7 的说明中所述的操作。

图 14. 从“任务”选项卡启动 QuickStatus 应用程序

从“任务”选项卡启动 QuickStatus 应用 从“任务”

过程 6 显示使用 QuickStatus 应用进行一些测试。

测试 QuickStatus 应用

用户可能在 QuickStatus 应用中尝试的每个操作都应在 Project Server 的测试安装上进行测试,然后再将应用部署到生产服务器或Project Online的生产租户。 通过测试安装,可以更改和删除用户的分配,而不会影响实际项目。 测试还应涉及具有不同权限集的多个用户,例如管理员、项目经理和团队成员。 彻底测试可以发现应在应用中进行的更改,这些更改在开发期间测试中并不明显。 过程 6 列出了 QuickStatus 应用的多个测试,但不包括详尽的测试系列。

过程 6. 测试 QuickStatus 应用

  1. 运行用户没有分配的 QuickStatus 应用。 应用应在页面底部显示蓝色消息,例如 ,“用户名”没有分配

    选择“ 更新”,消息更改为绿色 的“分配”已更新

    注意

    应更改应用行为,以便在没有分配时禁用 “更新 ”按钮。

  2. 运行用户在多个不同项目中具有多个分配且某些分配未完成的应用。 请注意应用的外观并执行如下操作 (见图 15) :

    1. onGetAssignmentsSuccess 函数在表中为当前用户的每个分配创建一行。 对于每个项目中的第一个工作分配,项目名称仅以加粗字体显示一次。

    2. 清除“任务名称”列标题中的“检查”框。 表标题单击事件处理程序清除任务行中的所有其他检查框。

    3. 选择所有任务。 每行的单击事件处理程序确定是否选择了所有行,如果是,则选择 “任务名称 ”列标题。

    4. 再次清除所有检查框,然后选择一个包含一些剩余工时的工作分配。 例如,图 15 显示首要任务 T1 有 20% 的剩余工时要完成。

    5. “设置完成百分比 ”文本框中,键入 80,然后选择“ 更新”。 页面底部应显示绿色消息, “作业已更新”。

      图 15. 更新 QuickStatus 应用程序中的工作分配

      在 QuickStatus 应用中

  3. 选择 “刷新 ” (请参阅图 16) 。 再次选择所有任务,顶部任务显示 80% 完成。

    图 16. 刷新“快速状态更新”页

    刷新 QuickStatus 页

  4. 清除所有检查框,然后选择另一个任务。 例如,选择“ 从 PWA 新建任务”。 将 “设置完成百分比 ”文本框留空,删除所选任务的“ 完成百分比 ”列中的所有文本,然后选择“ 更新”。 由于这两个文本框都是空的,因此应用会显示一条红色错误消息, (见图 17) 。

    图 17. 测试错误消息

    测试错误消息

  5. 将上一个任务更新为 80% 完成,然后选择“ 退出”。 exitToPwa 函数将浏览器窗口位置更改为 SharePoint 主机应用程序中的“任务”页 (即 URL 更改为 <https://ServerName/pwa/Tasks.aspx>) 。 图 18 显示 T1 任务和 PWA 任务中的新任务 各显示 80% 已完成。

    图 18. 在 Project Web App 中验证任务是否已更新

    验证中更新的任务Project Web App

  6. 在 2013 Project Professional显示更新状态之前,必须提交更改以供审批,然后由项目经理批准。

测试显示了 在 QuickStatus 应用中应进行的几项其他更改,以提高可用性。 例如:

  • 文本框值应进行其他错误检查和验证。 目前,用户可以为完成百分比输入非数值或负值,这会导致出现不友好的错误消息。 例如,如果值为负值,则错误消息为 更新分配错误:PJClientCallableException: StatusingSetDataValueInvalid

  • 空白文本框的错误消息可能列出项目和任务以及行号。

  • 成功消息可能包括更新的任务列表;或者,如果 updateAssignments 函数成功,它可以执行自动页面刷新,并用不同的颜色和加粗字体显示更新的任务或百分比。

  • 为了避免非常大的表,工作分配表应限制为完成率低于 100% 的任务。 或者,添加一个选项以显示所有任务。 也可以使用基于 jQuery 的网格而不是表来解决此问题,因为表可以轻松实现筛选和网格分页。

  • 由于 QuickStatus 应用不提交状态,因此功能区“任务”选项卡上的“快速状态”图标更合乎逻辑地是“任务”组中的第一个图标,而不是“提交”组中的最后一个图标。

  • 由于 onGetAssignmentsSuccess 函数初始化 btnSubmitUpdate 按钮文本,但其他按钮文本值以 HTML 格式初始化,因此当 getAssignments 函数运行时,页面将保持部分初始化状态。 如果文本值都以 HTML 格式初始化,则页面上的按钮将显示更一致。

最重要的是,应在生产应用中修改 QuickStatus 应用使用的方法(其中更改分配的完成百分比)。 有关详细信息,请参阅 后续步骤 部分。

QuickStatus 应用的示例代码

Default.aspx 文件

以下代码位于 Pages\Default.aspxQuickStatus 项目的 文件中:

    <%-- The following lines are ASP.NET directives needed when using SharePoint components --%>
    <%@ Page Inherits="Microsoft.SharePoint.WebPartPages.WebPartPage, Microsoft.SharePoint, Version=15.0.0.0, 
    Culture=neutral, PublicKeyToken=71e9bce111e9429c" MasterPageFile="~masterurl/default.master" Language="C#" %>
    <%@ Register TagPrefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=15.0.0.0, 
    Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
    <%@ Register TagPrefix="WebPartPages" Namespace="Microsoft.SharePoint.WebPartPages" Assembly="Microsoft.SharePoint, Version=15.0.0.0, 
    Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
    <%@ Register TagPrefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=15.0.0.0, 
    Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
    <%-- The markup and script in the following Content element will be placed in the <head> of the page.
        For production deployment, change the .debug.js JavaScript references to .js. --%>
    <asp:Content ContentPlaceHolderID="PlaceHolderAdditionalPageHead" runat="server">
    <script type="text/javascript" src="../Scripts/jquery-1.7.1.min.js"></script>
    <script type="text/javascript" src="/_layouts/15/sp.runtime.debug.js"></script>
    <script type="text/javascript" src="/_layouts/15/sp.debug.js"></script>
    <script type="text/javascript" src="/_layouts/15/ps.debug.js"></script>
    <!-- CSS styles -->
    <link rel="Stylesheet" type="text/css" href="../Content/App.css" />
    <!-- Add your JavaScript to the following file -->
    <script type="text/javascript" src="../Scripts/App.js"></script>
    </asp:Content>
    <%-- The markup and script in the following Content element will be placed in the <body> of the page --%>
    <asp:Content ContentPlaceHolderID="PlaceHolderMain" runat="server">
    <form>
        <fieldset>
        <legend>Select assigned tasks</legend>
        <table id="assignmentsTable">
            <caption id="tableCaption">Replace caption</caption>
            <thead>
            <tr id="headerRow">
                <th>Project name</th>
                <th><input type="checkbox" id="headercheckbox" checked="checked" />Task name</th>
                <th>Actual work</th>
                <th>% complete</th>
                <th>Remaining work</th>
                <th>Due date</th>
            </tr>
            </thead>
        </table>
        </fieldset>
        <div id="inputPercentComplete" >
        Set percent complete for all selected assignments, or leave this
        <br /> field blank and set percent complete for individual assignments: 
        <input type="text" name="percentComplete" id="pctComplete" size="4"  maxlength="4" />
        </div>
        <div id="submitResult">
        <p><button id="btnSubmitUpdate" type="button" class="bottomButtons" ></button></p>
        <p id="message"></p>
        </div>
        <div id="refreshPage">
        <p><button id="btnRefresh" type="button" class="bottomButtons" >Refresh</button></p>
        </div>
    <div id="exitPage">
        <p><button id="btnExit" type="button" class="bottomButtons" >Exit</button></p>
    </div>
    </form>
    </asp:Content>

App.js 文件

以下代码位于 Scripts\App.jsQuickStatus 项目的 文件中:

    var projContext;
    var pwaWeb;
    var projUser;
    // This code runs when the DOM is ready and creates a ProjectContext object.
    // The ProjectContext object is required to use the JSOM for Project Server.
    $(document).ready(function () {
        projContext = PS.ProjectContext.get_current();
        pwaWeb = projContext.get_web();
        getUserInfo();
        getAssignments();
        // Bind a click event handler to the table header check box, which sets the row check boxes
        // to the selected state of the header check box, and sets the results message to an empty string.
        $('#headercheckbox').live('click', function (event) {
            $('input:checkbox:not(#headercheckbox)').attr('checked', this.checked);
            $get("message").innerText = "";
        });
        // Bind a click event handler to the row check boxes. If any row check box is cleared, clear
        // the header check box. If all of the row check boxes are selected, select the header check box.
        $('input:checkbox:not(#headercheckbox)').live('click', function (event) {
            var isChecked = true;
            $('input:checkbox:not(#headercheckbox)').each(function () {
                if (this.checked == false) isChecked = false;
                $get("message").innerText = "";
            });
            $("#headercheckbox").attr('checked', isChecked);
        });
    });
    // Get information about the current user.
    function getUserInfo() {
        projUser = pwaWeb.get_currentUser();
        projContext.load(projUser);
        projContext.executeQueryAsync(onGetUserNameSuccess,
            // Anonymous function to execute if getUserInfo fails.
            function (sender, args) {
                alert('Failed to get user name. Error: ' + args.get_message());
        });
    }
    // This function is executed if the getUserInfo call is successful.
    // Replace the contents of the 'caption' paragraph with the project user name.
    function onGetUserNameSuccess() {
        var prefaceInfo = 'Assignments for ' + projUser.get_title();
        $('#tableCaption').text(prefaceInfo);
    }
    // Get the collection of assignments for the current user.
    function getAssignments() {
        assignments = PS.EnterpriseResource.getSelf(projContext).get_assignments();
        // Register the request that you want to run on the server. The optional "Include" parameter 
        // requests only the specified properties for each assignment in the collection.
        projContext.load(assignments,
            'Include(Project, Name, ActualWork, ActualWorkMilliseconds, PercentComplete, RemainingWork, Finish, Task)');
        // Run the request on the server.
        projContext.executeQueryAsync(onGetAssignmentsSuccess,
            // Anonymous function to execute if getAssignments fails.
            function (sender, args) {
                alert('Failed to get assignments. Error: ' + args.get_message());
            });
    }
    // Get the enumerator, iterate through the assignment collection, 
    // and add each assignment to the table.
    function onGetAssignmentsSuccess(sender, args) {
        if (assignments.get_count() > 0) {
            var assignmentsEnumerator = assignments.getEnumerator();
            var projName = "";
            var prevProjName = "3D2A8045-4920-4B31-B3E7-9D0C5195FC70"; // Any unique name.
            var taskNum = 0;
            var chkTask = "";
            var txtPctComplete = "";
            // Constants for creating input controls in the table.
            var INPUTCHK = '<input type="checkbox" class="chkTask" checked="checked" id="chk';
            var LBLCHK = '<label for="chk';
            var INPUTTXT = '<input type="text" size="4"  maxlength="4" class="txtPctComplete" id="txt';
            while (assignmentsEnumerator.moveNext()) {
                var statusAssignment = assignmentsEnumerator.get_current();
                projName = statusAssignment.get_project().get_name();
                // Get an integer value for the number of milliseconds of actual work, such as 3600000.
                var actualWorkMilliseconds = statusAssignment.get_actualWorkMilliseconds();
                // Get a string value for the assignment actual work, such as "1h". Not used here.
                var actualWork = statusAssignment.get_actualWork();                         
                if (projName === prevProjName) {
                    projName = "";
                }
                prevProjName = statusAssignment.get_project().get_name();
                // Create a row for the assignment information.
                var row = assignmentsTable.insertRow();
                taskNum++;
                // Create an HTML string with a check box and task name label, for example:
                //     <input type="checkbox" class="chkTask" checked="checked" id="chk1" /> 
                //     <label for="chk1">Task 1</label>
                chkTask = INPUTCHK + taskNum + '" /> ' + LBLCHK + taskNum + '">'
                    + statusAssignment.get_name() + '</label>';
                txtPctComplete = INPUTTXT + taskNum + '" />';
                // Insert cells for the assignment properties.
                row.insertCell().innerHTML = '<strong>' + projName + '</strong>';
                row.insertCell().innerHTML = chkTask;
                row.insertCell().innerText = actualWorkMilliseconds / 3600000 + 'h';
                row.insertCell().innerHTML = txtPctComplete;
                row.insertCell().innerText = statusAssignment.get_remainingWork();
                row.insertCell().innerText = statusAssignment.get_finish();
                // Initialize the percent complete cell.
                $get("txt" + taskNum).innerText = statusAssignment.get_percentComplete() + '%'
            }
        }
        else {
            $('p#message').attr('style', 'color: #0f3fdb');     // Blue text.
            $get("message").innerText = projUser.get_title() + ' has no assignments'
        }
        // Initialize the button properties.
        $get("btnSubmitUpdate").onclick = function() { updateAssignments(); };
        $get("btnSubmitUpdate").innerText = 'Update';
        $get('btnRefresh').onclick = function () { window.location.reload(true); };
        $get('btnExit').onclick = function () { exitToPwa(); };
    }
    // Update all selected assignments. If the bottom percent complete field is blank,
    // use the value in the % complete field of each selected row in the table.
    function updateAssignments() {
        // Get percent complete from the bottom text box.
        var pctCompleteMain = getNumericValue($('#pctComplete').val()).trim();
        var pctComplete = pctCompleteMain;
        var assignmentsEnumerator = assignments.getEnumerator();
        var taskNum = 0;
        var taskRow = "";
        var indexPercent = "";
        var doSubmit = true;
        while (assignmentsEnumerator.moveNext()) {
            var pctCompleteRow = "";
            taskRow = "chk" + ++taskNum;
            if ($get(taskRow).checked) {
                var statusAssignment = assignmentsEnumerator.get_current();
                if (pctCompleteMain === "") {
                    // Get percent complete from the text box field in the table row.
                    pctCompleteRow = getNumericValue($('#txt' + taskNum).val());
                    pctComplete = pctCompleteRow;
                }
                // If both percent complete fields are empty, show an error.
                if (pctCompleteMain === "" && pctCompleteRow === "") {
                    $('p#message').attr('style', 'color: #e11500');     // Red text.
                    $get("message").innerHTML =
                        '<b>Error:</b> Both <i>Percent complete</i> fields are empty, in row '
                        + taskNum
                        + ' and in the bottom textbox.<br/>One of those fields must have a valid percent.'
                        + '<p>Please refresh the page and try again.</p>';
                    doSubmit = false;
                    taskNum = 0;
                    break;
                }
                if (doSubmit) statusAssignment.set_percentComplete(pctComplete);
            }
        } 
        // Save and submit the assignment updates.
        if (doSubmit) {
            assignments.update();
            assignments.submitAllStatusUpdates();
            projContext.executeQueryAsync(function (source, args) {
                $('p#message').attr('style', 'color: #0faa0d');     // Green text.
                $get("message").innerText = 'Assignments have been updated.';
            }, function (source, args) {
                $('p#message').attr('style', 'color: #e11500');     // Red text.
                $get("message").innerText = 'Error updating assignments: ' + args.get_message();
            });
        }
    }
    // Get the numeric part for percent complete, from a string. 
    // For example, with "20 %", return "20".
    function getNumericValue(pctComplete) {
        pctComplete = pctComplete.trim();
        pctComplete = pctComplete.replace(/ /g, "");    // Remove interior spaces.
        indexPercent = pctComplete.indexOf('%', 0);
        if (indexPercent > -1) pctComplete = pctComplete.substring(0, indexPercent);
        return pctComplete;
    }
    // Exit the QuickStatus page and go back to the Tasks page in Project Web App.
    function exitToPwa() {
        // Get the SharePoint host URL, which is the top page of PWA, and add the Tasks page.
        var spHostUrl = decodeURIComponent(getQueryStringParameter('SPHostUrl'))
                        + "/Tasks.aspx";
        // Set the top window for the QuickStatus IFrame to the Tasks page.
        window.top.location.href = spHostUrl;
    }
    // Get a specified query string parameter from the {StandardTokens} URL option string.
    function getQueryStringParameter(urlParameterKey) {
        var docUrl = document.URL;
        var params = docUrl.split('?')[1].split('&');
        for (var i = 0; i < params.length; i++) {
            var theParam = params[i].split('=');
            if (theParam[0] == urlParameterKey)
                return decodeURIComponent(theParam[1]);
        }
    }

App.css 文件

以下 CSS 代码位于 Content\App.cssQuickStatus 项目的 文件中:

    /* Custom styles for the QuickStatus app. */
    /*============= Table elements ========================================*/
    table {
        width: 90%;
    }
    caption {
        font-size: 16px;
        padding-bottom: 5px;
        font-weight: bold;
        color: gray;
    }
    table th {
        background-color: gray;
        color: white;
    }
    table td, th {
        width: auto;
        text-align: left;
        padding: 2px;
        border: solid 1px whitesmoke;
        color: gray;
    }
    /*=== Class for check boxes added to rows 
    */
    .chkTask {
        width: 12px;
        height: 12px;
        color: gray;
    }
    /*========== DIV id for the Percent Complete text box ================*/
    #inputPercentComplete {
        position: fixed;
        top: auto;
        height: auto;
        padding-top: 20px;
        margin-left: 30px;
    }
    /*========== DIV id for the Submit Result button ====================*/
    #submitResult {
        position: fixed;
        top: auto;
        height: auto;
        padding-top: 60px;
    }
    /*========== DIV id for the Refresh Page button ====================*/
    #refreshPage {
        position: fixed;
        top: auto;
        height: auto;
        padding-top: 60px;
        margin-left: 120px;
    }
    /*========== DIV id for the Exit Page button ====================*/
    #exitPage {
        position: fixed;
        top: auto;
        height: auto;
        padding-top: 60px;
        margin-left: 240px;
    }
    /*========== Class for the buttons at the bottom of the page =======*/
    .bottomButtons {
        color: gray;
        font-weight: bold; 
        font-size: 12px; 
        border-color: darkgreen;
        border-width: thin;
    }

功能区 Elements.xml 文件

功能区上 “任务 ”选项卡上添加的按钮的以下 XML 定义位于 RibbonQuickStatusAction\Elements.xmlQuickStatus 项目的 文件中:

    <?xml version="1.0" encoding="utf-8"?>
    <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
    <CustomAction Id="21ea3aaf-79e5-4aac-9479-8eef14b4d9df.RibbonQuickStatusAction"
                    Location="CommandUI.Ribbon">
        <CommandUIExtension>
        <!-- 
        Add a button that invokes the QuickStatus app. The Quick Status button is displayed as  
        the third control in the Page group (the group title is "Submit").
        -->
        <CommandUIDefinitions>
            <CommandUIDefinition Location="Ribbon.ContextualTabs.MyWork.Home.Page.Controls._children">
            <Button Id="Ribbon.ContextualTabs.MyWork.Home.Page.QuickStatus"
                    Alt="Quick Status app"
                    Sequence="30"
                    Command="Invokae_QuickStatus"
                    LabelText="Quick Status"
                    TemplateAlias="o1"
                    Image16by16="_layouts/15/1033/images/ps16x16.png" 
                    Image16by16Left="-80"
                    Image16by16Top="-144"
                    Image32by32="_layouts/15/1033/images/ps32x32.png" 
                    Image32by32Left="-32"
                    Image32by32Top="-288" 
                    ToolTipTitle="Quick Status"
                    ToolTipDescription="Run the QuickStatus app" />
            </CommandUIDefinition>
        </CommandUIDefinitions>
        <CommandUIHandlers>
            <CommandUIHandler Command="Invoke_QuickStatus"
                            CommandAction="~appWebUrl/Pages/Default.aspx?{StandardTokens}"/>
        </CommandUIHandlers>
        </CommandUIExtension >
    </CustomAction>
    </Elements>

AppManifest.xml 文件

下面是 QuickStatus 项目的应用清单的 XML,其中包括更新多个项目中应用用户分配状态所需的两个权限请求范围:

    <?xml version="1.0" encoding="utf-8" ?>
    <!--Created:cb85b80c-f585-40ff-8bfc-12ff4d0e34a9-->
    <App xmlns="http://schemas.microsoft.com/sharepoint/2012/app/manifest"
        Name="QuickStatus"
        ProductID="{bbc497e7-1221-4d7b-a0ae-141a99546008}"
        Version="1.0.0.0"
        SharePointMinVersion="15.0.0.0"
    >
    <Properties>
        <Title>Quick Status Update</Title>
        <StartPage>~appWebUrl/Pages/Default.aspx?{StandardTokens}</StartPage>
    </Properties>
    <AppPrincipal>
        <Internal />
    </AppPrincipal>
    <AppPermissionRequests>
        <AppPermissionRequest Scope="https://sharepoint/projectserver/statusing" Right="SubmitStatus" />
        <AppPermissionRequest Scope="https://sharepoint/projectserver/projects" Right="Read" />
    </AppPermissionRequests>
    </App>

AppIcon.png 文件

QuickStatus 应用的完整 Visual Studio 解决方案包括自定义 AppIcon.png 文件。 该解决方案将包含在 Project 2013 SDK 下载中。

后续步骤

QuickStatus 应用是一个相对简单的示例,演示如何编写可安装在 Project Server 2013 上并Project Online的应用。 测试 QuickStatus 应用部分列出了一些可改进的改进,以提高可用性。 QuickStatus 应用使用 JavaScript 函数更新Project Web App的分配状态。 但是,更改分配完成百分比不是建议的项目管理做法。 另一种方法是更新已分配任务的实际开始日期和剩余持续时间。 有关问题的讨论,请参阅 MPUG 新闻稿中的 “更新更好 ”。

另请参阅