65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2014年2月26日

CPOL

8分钟阅读

viewsIcon

28475

downloadIcon

260

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.ControlsTFS.WorkItemTrackingTFS.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。

image004

image006

单击“新建”添加一个新字段。

image008

填写以下详细信息:

image009

转到布局部分并添加控件。

image011

确保不要为控件添加任何标签,因为我们不希望控件显示在 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 中的工作项自定义控件开发 – 部署

http://blogs.msdn.microsoft.com/serkani/archive/2012/06/22/work-item-custom-control-development-in-tf-web-access-2012-deployment.aspx

TFS 2013 PowerTools

http://visualstudiogallery.msdn.microsoft.com/f017b10c-02b4-4d6d-9845-58a06545627f

© . All rights reserved.