仅当子工作项关闭时才关闭工作项 - WebAccess





5.00/5 (2投票s)
TFS WebAccess 上的自定义控件
摘要
在快速发展的世界中,随着最灵活的应用程序的出现,我们有机会扩展软件的功能。市场上类似的软件无法满足所需的功能,但人们会因为某个应用程序提供的功能(如免费、开源、可扩展性、专业性等)而更倾向于某个特定应用程序。所选的应用程序可能不具备另一个应用程序提供的特定功能,或者根据业务需求需要一项新功能。要实现此功能,我们可能由于各种原因(如支持成本、时间、频繁的更改等)而无法联系供应商。为了克服这种情况,一些供应商提供 API,使客户/开发人员能够根据我们的需求扩展应用程序。有些可能只是简单的配置更改,而有些则通过使用的 API 来扩展功能。这使我们能够实现所需的功能,并且控制权在我们手中。
引言
Team Foundation Server 是一个 ALM 工具。它可以根据业务需求进行扩展。可以创建控件以在工作项表单上填充它们。通过创建控件,我们可以添加额外的业务功能。TFS 提供的 API 使这一切成为可能。仅当子工作项关闭时才关闭工作项并非 TFS 的标准功能。我们需要通过使用 TFS 提供的 API 来扩展此功能。TFS 可以通过各种接口使用。Visual Studio 和 Web 是最突出的。在本篇论文中,我将解释如何在 Web 上实现上述功能。请在此处查找我关于 Visual Studio 的其他论文:这里。
背景
TFS 作为一种配置管理工具,存储代码版本,后来发展成为一个应用程序生命周期管理(ALM)工具。尽管提供了额外的功能,但它不像市场上其他工具那样高效,这可能是由于我们使用的过程模板。因此,我们需要在 TFS 中开发自定义功能,这可以通过 Microsoft 为 TFS 提供的 API 来实现。为了给团队成员提供灵活性,ALM 可以通过各种平台进行操作,其中 Web 是最突出的,可以从任何设备访问。
当子工作项未关闭时,限制关闭工作项的需求
TFS 作为 ALM 工具,可以将用户故事、任务、问题、 bug 等记录在其中。我们可以应用经过验证的敏捷实践来管理我们的应用程序生命周期。虽然本文描述的功能并非标准功能的一部分,但我们计划开发一个。创建这样一个工具不仅使我们能够遵循经过验证的敏捷实践,还使开发人员、经理等能够轻松地跟踪工作。
定义
WIT |
工作项类型 |
ALM |
应用程序生命周期管理 |
.WICC |
工作项自定义控件 |
为 Web 界面开发控件
注意:我们将开发一个 JQuery 插件,需要将其安装到 TFS Web 服务器上。
创建一个 .js 文件并创建一个 TFS 模块,将其注册为 FF.WITEventHandler
。声明对 TFS.WorkItemTracking.Controls
、TFS.WorkItemTracking
和 TFS.Core
模块的依赖。
TFS.module("FujiFilm.WITEventHandler",
[
"TFS.WorkItemTracking.Controls",
"TFS.WorkItemTracking",
"TFS.Core"
]
创建一个构造函数并将其继承自 TFS.WorkItemTracking.Controls.WorkItemControl
。
function () {
// module content
var WITOM = TFS.WorkItemTracking,
WITCONTROLS = TFS.WorkItemTracking.Controls,
delegate = TFS.Core.delegate;
// Constructor for WITEventHandler
function WITEventHandler(container, options, workItemType)
{
this.baseConstructor.call(this, container, options, workItemType);
}
// WITEventHandler inherits from WorkItemControl
WITEventHandler.inherit(WITCONTROLS.WorkItemControl, {
_control: null,
_status: null,
WITCONTROLS.registerWorkItemControl("WITEventHandler", WITEventHandler);
}
初始化控件 UI。这里是所有全局变量的去向。
_init:function () {
this._base();
var oldStateValue;
},
当工作项绑定到特定项时更新控件数据。
invalidate: function (flushing) {
},
清除控件数据。框架在控件需要将其状态重置为“空白
”时调用此方法,例如当工作项表单与特定工作项解绑时。
clear: function() {
this._workItem= null;
},
将函数绑定到工作项,我们在其中处理所有逻辑。
bind:function (workItem) {
}
在上述函数中,创建一个函数委托,并将其附加到绑定中的工作项更改事件。
bind:function (workItem) {
this._workItemChangeDelegate = function (sender, args) {
}
workItem.attachWorkItemChanged(this._workItemChangeDelegate);
}
因此,当工作项更改时,this._workItemChangeDelegate
会被触发。此方法包含我们需要为我们的需求实现的逻辑。
逻辑
逻辑很简单:
- 确定工作项是否已更改
- 如果已更改,则检查它是否是状态控件
- 如果它是状态控件且已更改为“关闭”
- 检查子工作项是否已打开
- 如果子工作项已打开,则:
- 向用户显示警报
- 并撤销更改
识别被单击的控件
仔细观察,TFS 工作项 Web 表单上生成的控件是动态控件。我们需要一种机制来识别被单击的控件。
因此,为每个下拉列表创建一个事件(在我们的例子中,我们只处理状态,所以我选择了下拉列表)。
我们正在为上面突出显示的按钮编写一个事件。
$('.drop').bind('click', function () {
if (this.id == "")
stateCtrl = $("#" + this.parentElement.id + "_txt")[0];
else
stateCtrl = $("#" + this.id + "_txt")[0];
});
当按钮被单击时,创建一个变量来加载控件的父级,因为那是实际的下拉列表控件。
将先前的值存储在名为 oldStateValue
的变量中 = workItem.fieldData[2];
现在确定您想检查哪个工作项,Epic、User Story、Task 等。
if (workItem.workItemType.name == "Epic")
wiClosed = "Resolved";
if (workItem.workItemType.name == "Task")
wiClosed = "Closed";
if (workItem.workItemType.name == "User Story")
wiClosed = "Closed";
现在创建一个工作项更改委托函数。
检查更改是否是“字段更改
”。
if (args.change === "field-change")
当工作项中的某个字段更改时,会有多个自动字段更改,例如更改日期、更改人等。对于每个更改的值,我们都对状态字段的更改感兴趣,该字段已更改为“关闭”。
for (var i in args.changedFields) {
if (args.changedFields[i].fieldDefinition.name === "State" && stateCtrl.value === wiClosed) {
获取工作项的链接。链接可以是子项、父项等,我们只需要检查子项。
var links = parentWorkItem.getLinks();
for (var i in links) {
var child = null;
if (links[i].baseLinkType === "WorkItemLink" && links[i].getLinkTypeEnd().name == "Child") {
如果存在子项,则检查子项的状态。如果子项状态未关闭,则向用户显示警报并将状态值重置为先前的值。
state = child.fieldMap.STATE.getValue();
if (state != wiClosed) {
alert("You must close all the open child work items to close this work item.");
stateCtrl.value = oldStateValue;
这里我们只向用户显示一个基本的警报,但不是打开的子项的详细信息。因此,如果找到至少一个打开的子项,则中断循环。
最终代码如下:
// Register this module as "FFFilm.WITEventHandler" and declare
// dependencies on TFS.WorkItemTracking.Controls, TFS.WorkItemTracking and TFS.Core modules
TFS.module("FFFilm.WITEventHandler",
[
"TFS.WorkItemTracking.Controls",
"TFS.WorkItemTracking",
"TFS.Core"
],
function () {
// module content
var WITOM = TFS.WorkItemTracking,
WITCONTROLS = TFS.WorkItemTracking.Controls,
delegate = TFS.Core.delegate;
// Constructor for WITEventHandler
function WITEventHandler(container, options, workItemType) {
this.baseConstructor.call(this, container, options, workItemType);
}
// WITEventHandler inherits from WorkItemControl
WITEventHandler.inherit(WITCONTROLS.WorkItemControl, {
_control: null,
_status: null,
// Initialize the control UI without data (in "blank" state).
// Framework calls this method when the control needs to render its initial UI
// Notes:
// - The work item data is NOT available at this point
// - Keep in mind that work item form is reused for multiple work items
// by binding/unbinding the form to work item data
_init: function () {
this._base();
var oldStateValue;
var stateCtrl;
var wiClosed;
},
// Update the control data
// Framework calls this method when the control needs to update itself, such as when:
// - work item form is bound to a specific work item
// - underlying field value has changed due to rules or another control logic
invalidate: function (flushing) {
},
// Clear the control data
// Framework calls this method when the control needs to reset its state to "blank", such as when:
// - work item form is unbound from a specific work item
clear: function () {
this._workItem = null;
},
bind: function (workItem) {
this._base(workItem);
$('.drop').bind('click', function () {
if (this.id == "")
stateCtrl = $("#" + this.parentElement.id + "_txt")[0];
else
stateCtrl = $("#" + this.id + "_txt")[0];
});
oldStateValue = workItem.fieldData[2];
if (workItem.workItemType.name == "Epic")
wiClosed = "Resolved";
if (workItem.workItemType.name == "Task")
wiClosed = "Closed";
if (workItem.workItemType.name == "User Story")
wiClosed = "Closed";
this._workItemChangeDelegate = function (sender, args) {
if (args.change === "field-change") {
// find a label field, with class workitemcontrol-label and
// title contans helptext of args.changedFields[i].fieldDefinition
// get that for value which is the state control
for (var i in args.changedFields) {
if (typeof stateCtrl != 'undefined') {
if (args.changedFields[i].fieldDefinition.name === "State" && stateCtrl.value === wiClosed) {
var parentWorkItem = workItem;
var links = parentWorkItem.getLinks();
for (var i in links) {
var child = null;
var state = "";
if (links[i].baseLinkType === "WorkItemLink" && links[i].getLinkTypeEnd().name == "Child") {
parentWorkItem.store.beginGetWorkItem(links[i].getTargetId(), function (child, state) {
state = child.fieldMap.STATE.getValue();
// if state is not closed, then return true
if (state != wiClosed) {
// if the previous state is already the same, then do not display any thing to the user.
if (stateCtrl.value != oldStateValue) {
alert("You must close all the open child work items to close this work item.");
stateCtrl.value = oldStateValue;
}
}
});
break;
}
}
}
}
}
}
}
workItem.attachWorkItemChanged(this._workItemChangeDelegate);
},
unbind: function (workItem) {
if (this._workItemChangeDelegate) {
this._workItem.detachWorkItemChanged(this._workItemChangeDelegate);
delete this._workItemChangeDelegate;
}
}
});
// Register this module as a work item custom control called "WITEventHandler"
WITCONTROLS.registerWorkItemControl("WITEventHandler", WITEventHandler);
return {
WITEventHandler: WITEventHandler
};
});
创建 JQuery 类不足以使用该功能。我们需要一个声明它的清单文件。
创建一个 manifest.xml 文件并将以下内容复制到其中:
<WebAccess version="11.0">
<plugin name="Work Item Template Event Handler" vendor="FFFilm" moreinfo="http://www.FFmed.com" version="1.0.0" >
<modules>
<module namespace="FFFilm.WITEventHandler" kind="TFS.WorkItem.CustomControl"/>
</modules>
</plugin>
</WebAccess>
打包
在这种情况下,我们只需要服务器端部署。用户需要 TFS 的管理员权限才能完成此操作。
在部署之前,我们需要打包我们创建的文件。
将 JS 文件命名为 FFFilm.WITEventHandler.min.js。如果需要,可以缩小文件。
复制一份文件并将其重命名为 FujiFilm.WITEventHandler.debug.js
将这三个文件(包括 Manifest.xml)压缩。在压缩之前不要将它们放在文件夹中。Zip 文件的名称无关紧要。
这就创建了我们的包。
部署
打开 TFS WebAccess。
转到控制面板。单击“扩展”选项卡。您应该在此处看到之前安装的所有插件。
单击安装按钮来安装您刚刚创建的插件。如果此按钮不可见,则您没有必要的权限。
选择文件并指向我们创建的 zip 文件。然后单击“确定”安装插件。
默认情况下,已安装的插件将处于禁用状态。单击“启用”来启用它。
现在导航到工作项并执行您的测试。
修改工作项类型
安装文件是不够的,因为我们试图修改工作项,我们需要告诉工作项使用我们开发的新控件。为此,我们需要修改工作项模板。
为此,我们有多个第三方工具,其中 TFS Power tools 是最突出的。
在 Visual Studio 中,导航到工具 -> 进程编辑器 -> 工作项类型 -> 从服务器打开 WIT。
选择您喜欢的项目,然后从扩展中选择 Task。
单击“新建”添加一个新字段。
填写以下详细信息:
转到布局部分并添加控件。
确保不要为控件添加任何标签,因为我们不希望控件显示在 Visual Studio 的工作项表单上。
尽管我们构建的控件没有界面,但添加标签会显示标签文本以及一个默认的文本框。
完成后,单击“保存”,这将把工作项保存到服务器。
使 Visual Studio 和 WebAccess 都能正常工作
要使 Visual Studio 和 Web Access 都能正常工作,您无需对工作项类型进行任何进一步修改。上述修改也足以满足 Visual Studio。确保控件名称匹配。
调试
在这种情况下,调试很简单。在 Chrome 中,按 F12 键打开开发者工具窗口。
选择“源”选项卡,然后展开左侧的文件夹。转到下方指定的文件夹,您将在其中找到您创建的 .js 文件。单击行号设置断点,然后刷新浏览器以命中断点。
扩展控件
本文中的代码和描述与 Task 工作项和状态控件的更改有关。通过进行少量修改,可以将其扩展到其他控件。
通过修改工作项类型,还可以将其扩展到其他工作项类型。就像,安装一次,供许多其他工作项类型使用。
以这段代码为基础,我们可以开发多个能够满足敏捷实践的控件。
面临的挑战
- 捕获控件的更改事件
编写了捕获表单上任何事件的处理器。 - 消除多个警报以显示子项详细信息
只显示了一个通用消息。 - 创建的处理器应被分离并从工作项中移除。否则,对于下一个工作项的每一次更改,处理器都会保留,并且会出现重复的警报框,从而打扰用户。
导航到 TFS.Controls.js 以准确确定如何分离处理器。 - 频繁将程序集文件复制到指定位置。
创建了一个批处理文件,该文件复制 .dll、.pdb 和 .wicc 文件。
参考文献
TF Web Access 2012 中的工作项自定义控件开发 – 部署 |
|
TFS 2013 PowerTools |
http://visualstudiogallery.msdn.microsoft.com/f017b10c-02b4-4d6d-9845-58a06545627f |