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





5.00/5 (6投票s)
Team Foundation Server (TFS) 是一个应用生命周期管理 (ALM) 工具。它可以根据业务需求进行扩展。可以创建控件以填充到工作项窗体中。通过创建控件,我们可以添加额外的业务功能。TFS 提供的 API 使这一切成为可能。
摘要
在开发领域,随着最灵活的应用程序可用,我们有机会扩展软件的功能。市场上类似的软件无法满足所需的功能,但人们会更倾向于某个特定的应用程序,这取决于它提供的功能,例如免费、开源、可扩展性、专业知识等。所选的应用程序可能没有另一个应用程序提供的特定功能,或者需要根据业务需求的新功能。为了实现此功能,我们可能由于各种原因(如支持成本、时间、频繁更改等)而无法联系供应商。为了克服这种情况,一些供应商提供了 API,这些 API 使最终用户/开发人员能够根据我们的需求扩展应用程序。有些可能只是简单的配置更改,而另一些则会利用提供的 API 来扩展功能。这使我们能够实现所需的功能,并掌握控制权。
引言
Team Foundation Server (TFS) 是一个应用生命周期管理 (ALM) 工具。它可以根据业务需求进行扩展。可以创建控件以填充到工作项窗体中。通过创建控件,我们可以添加额外的业务功能。TFS 提供的 API 使这一切成为可能。仅当子工作项关闭时才关闭工作项,这并非 TFS 的标准功能。我们需要通过使用 TFS 提供的 API 来扩展功能以实现此目的。TFS 可以借助各种接口使用。Visual Studio 和 Web 是最突出的。在本文中,我将解释如何在 Visual Studio 中实现上述功能。有关 Web 的内容,请参阅我的其他文章。
背景
TFS 作为一种配置管理工具,用于存储代码版本,后来发展成为一个应用生命周期管理 (ALM) 工具。由于提供了额外功能,但它不像市场上其他工具那样高效,这可能是由于我们使用的过程模板。因此,我们需要在 TFS 中开发自定义功能,这可以通过 Microsoft 为 TFS 提供的 API 实现。
需要限制在子工作项打开时关闭工作项
TFS 是一个 ALM 工具,可以在其中记录用户故事、任务、问题、错误等。我们可以应用经过验证的敏捷实践来管理我们的应用程序生命周期。虽然本文描述的功能并非标准功能的一部分,但我们打算开发一个。创建这样一个工具不仅可以帮助我们遵循经过验证的敏捷实践,还可以方便开发人员、经理等跟踪工作。
定义
WIT | 工作项类型 |
ALM | 应用生命周期管理 |
.WICC | 工作项自定义控件 |
为 Visual Studio 界面开发控件
注意:我们正在为 Visual Studio 开发一个客户端控件,因此此控件需要安装在访问 Visual Studio 中 TFS 的每台计算机上。如果客户端上没有此控件,则此功能将不起作用,但现有功能将保持不变。
创建一个名为“Windows Forms Control Library”的新项目,并继承自 IWorkItemControl。
public partial class WITEventHandler : UserControl, IWorkItemControl { }
我们的任务是,当工作项的状态更改为“关闭”时,我们需要检查是否有子工作项是打开的,然后禁用状态更改。
尽管 Visual Studio 界面看起来很简单,但我们无法捕获下拉列表的 onChange 事件,因为我们无法识别该控件。
要实现此类功能,我们需要为工作项的 onChange
事件添加一个事件处理程序。这意味着将捕获工作项上的任何更改。不仅是状态,还有优先级、排名等的更改!
因此,添加一个名为 AddEventHandler()
的方法,该方法为工作项添加一个事件。
private void AddEventHandler() { workItem.FieldChanged += new WorkItemFieldChangeEventHandler (this.workItem_FieldChanged); }
在 WorkItemDatasource
属性中调用此方法。
object IWorkItemControl.WorkItemDatasource { get { return workItem; } set { workItem = (WorkItem)value; AddEventHandler(); } }
现在,在 workItem_FieldChanged
方法中执行所需功能。
根据我们的要求,我们需要识别更改的字段是否为状态,以及是否将其设置为“关闭”。这可以通过检查事件参数来实现,如下所示。
(e.Field.Name == "State" && e.Field.Value.ToString() == "Closed")
一旦我们确保捕获了正确的事件,现在就需要查找该工作项是否有子项。
通过 WorkItem.WorkItemLinks
查找工作项的链接。同时检查链接是否为子链接(可能还有其他链接,如父链接等)。
一旦我们找到了子链接,我们就需要检查它们的状态。
Foreach (WorkItemLink link in parentWorkItem.WorkItemLinks) { WorkItem child = null; if (link.LinkTypeEnd.Name == "Child") { child = parentWorkItem.Store.GetWorkItem(link.TargetId); if (child.State != "Closed") { workItemDesc.Append(child.Id + "\t" + trimDescription(child.Type.Name, 10) + "\t" + trimDescription(child.Title, 18) + "\t\t" + child.Fields["priority"].Value + "\n"); isOpen = true; } } }
如果子项的状态不是“关闭”,那么我们需要发出警报,指出子项是打开的,然后将父项的状态更改恢复到原始状态。
private void ResetState() { workItem.State = originalState; workItem.Fields["Assigned To"].Value = originalAssignedTo; }
您还可以通过添加一行额外的代码来捕获详细信息,从而向用户显示工作项的详细信息。
在此过程中,我们发现当工作项发生更改时,事件就会触发。最初,当我们尝试将父项的状态更改为“关闭”时,它会检查子项的状态并撤销对父项所做的更改,这也是一种更改,这会再次触发 onChange
事件,从而导致无限循环。为了防止这种情况,我们需要重置线程。
System.Threading.Thread resetThread = new System.Threading.Thread(new System.Threading.ThreadStart(ResetState)); resetThread.Start();
整个源代码可在下方找到。它包含更精炼、更可靠的代码,可供使用。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Data; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using Microsoft.TeamFoundation.WorkItemTracking.Client; using Microsoft.TeamFoundation.WorkItemTracking.Controls; using System.Collections.Specialized; namespace FF.TFSControls { public partial class WITEventHandler : UserControl, IWorkItemControl { string originalState = string.Empty; string originalAssignedTo = string.Empty; bool isSettingFieldsManually = false; public WITEventHandler() { InitializeComponent(); } private static object EventBeforeUpdateDatasource = new object(); private static object EventAfterUpdateDatasource = new object(); event EventHandler IWorkItemControl.AfterUpdateDatasource { add { Events.AddHandler(EventAfterUpdateDatasource, value); } remove { Events.RemoveHandler(EventAfterUpdateDatasource, value); } } event EventHandler IWorkItemControl.BeforeUpdateDatasource { add { Events.AddHandler(EventBeforeUpdateDatasource, value); } remove { Events.RemoveHandler(EventBeforeUpdateDatasource, value); } } void IWorkItemControl.Clear() { //throw new NotImplementedException(); } void IWorkItemControl.FlushToDatasource() { // throw new NotImplementedException(); } void IWorkItemControl.InvalidateDatasource() { // throw new NotImplementedException(); } private StringDictionary properties; StringDictionary IWorkItemControl.Properties { get { return properties; } set { properties = value; } } private bool readOnly; bool IWorkItemControl.ReadOnly { get { return readOnly; } set { readOnly = value; } } private IServiceProvider sProvider; void IWorkItemControl.SetSite(IServiceProvider serviceProvider) { sProvider = serviceProvider; } private WorkItem workItem; // testing object IWorkItemControl.WorkItemDatasource { get { return workItem; } set { workItem = (WorkItem)value; AddEventHandler(); } } private void AddEventHandler() { if (workItem != null && workItem.Id != 0 && workItem.State != "Closed") { workItem.FieldChanged += new WorkItemFieldChangeEventHandler(this.workItem_FieldChanged); originalState = workItem.Fields["State"].Value.ToString(); originalAssignedTo = workItem.Fields["Assigned To"].Value.ToString(); } } private void workItem_FieldChanged(object sender, WorkItemEventArgs e) { StringBuilder workItemDesc = new StringBuilder(); if (!this.IsDisposed && e.Field != null) { if (isSettingFieldsManually == false && (e.Field.Name == "Assigned To" || (e.Field.Name == "State" && e.Field.Value.ToString() != "Closed"))) { originalState = workItem.Fields["State"].Value.ToString(); originalAssignedTo = workItem.Fields["Assigned To"].Value.ToString(); } if (e.Field.Name == "State" && e.Field.OriginalValue.ToString() != "Closed" && e.Field.Value.ToString() == "Closed") { if (isChildOpen(workItem,workItemDesc)) { string message ="Below child workitems are not closed. \n\n"+ "ID\tType\tDescription\t\tPriority\n\n" + workItemDesc.ToString() + "\n" + "Bugs with priority 4 and 5 can be moved to backlog.\n" + " \nClick on 'All Links' in the work item to see the Child items for this work item."; MessageBox.Show(message, "Dependency Error", MessageBoxButtons.OK, MessageBoxIcon.Error); System.Threading.Thread resetThread = new System.Threading.Thread(new System.Threading.ThreadStart(ResetState)); resetThread.Start(); } } } } private bool isChildOpen(WorkItem parentWorkItem, StringBuilder workItemDesc) { bool isOpen = false; foreach (WorkItemLink link in parentWorkItem.WorkItemLinks) { WorkItem child = null; if (link.LinkTypeEnd.Name == "Child") { child = parentWorkItem.Store.GetWorkItem(link.TargetId); if (child.State != "Closed") { workItemDesc.Append(child.Id + "\t" + trimDescription(child.Type.Name, 10) + "\t" + trimDescription(child.Title, 18) + "\t\t" + child.Fields["priority"].Value + "\n"); isOpen = true; } } } return isOpen; } private string trimDescription(string description, int len) { if (description.Length > len) return description.Substring(0, len-3) + "..."; else return description; } private void ResetState() { isSettingFieldsManually = true; workItem.State = originalState; workItem.Fields["Assigned To"].Value = originalAssignedTo; isSettingFieldsManually = false; } private string fieldName; string IWorkItemControl.WorkItemFieldName { get { return fieldName; } set { fieldName = value; } } } }
创建此程序集是不够的;我们需要一种机制来在 Visual Studio 中使用它。
创建一个名为“WITEventHandler.wicc”的 .wicc 文件,并在其中粘贴以下内容。
<?xml version="1.0"?> <CustomControl xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <Assembly>FF.TFSControls.dll</Assembly> <FullClassName>FF.TFSControls.WITEventHandler</FullClassName> </CustomControl>
这指定了程序集的名称和类名。
安装
准备好这两个文件(程序集 .dll 和 .wicc 文件)后,将它们复制到以下位置。
C:\ProgramData\Microsoft\Team Foundation\Work Item Tracking\Custom Controls\12.0
这可能因计算机而异,具体取决于操作系统。请检查开发代码的环境。稍作修改即可在任何环境中运行代码。
修改工作项类型
将文件复制到指定位置是不够的,因为我们正在尝试修改工作项,我们需要告诉工作项使用我们开发的新控件。为此,我们需要修改工作项模板。
为此,我们有多个第三方工具,其中 TFS Power Tools 是最突出的。
从 Visual Studio 导航到“工具”->“进程编辑器”->“工作项类型”->“从服务器打开 WIT”。
选择您喜欢的项目,然后从展开项中选择“任务”。
点击“新建”添加一个新字段。
填写以下详细信息:
转到“布局”部分并添加控件。
确保不要为控件添加任何标签,因为我们不希望在 Visual Studio 的工作项窗体上显示该控件。
尽管我们构建的控件没有任何界面,但添加标签会显示标签文本以及一个默认的文本框。
完成后,单击“保存”,这将把工作项保存到服务器。
测试
由于我们修改了特定服务器和特定项目上的“任务”工作项,请从该特定位置打开一个“任务”。
您会发现工作项窗体上没有任何区别,因为我们没有进行任何视觉更改。
打开一个尚未关闭且拥有尚未关闭的子项的任务。
将状态更改为“关闭”,然后会显示一个警报框,告知哪些子工作项是打开的,一旦单击“确定”,父控件的状态就会恢复到原始状态。
调试
调试控件非常简单,但您需要打开两个 Visual Studio 应用程序。一个包含编写代码的项目,另一个包含已安装了该功能的(工作项类型)项目。
将第一个 Visual Studio 的 devenv 进程附加。
在需要的地方设置断点。
不要忘记将 .pdb 文件复制到目标位置。
扩展控件
本文中的代码和描述与“任务”工作项和状态更改控件相关。通过进行一些小的修改,可以将其扩展到其他控件。
通过仅修改工作项类型,也可以将其扩展到其他工作项类型。这就像“一次安装,可用于许多其他工作项类型”。
以这段代码为基础,我们可以开发多个满足敏捷实践的控件。
遇到的挑战
- 捕获控件的 on change 事件
- 编写了一个处理程序来捕获表单上的任何事件。
- 消除 onChange 的无限循环
- 已应用多线程来消除此问题。
- 批量修改和导入工作项类型
- 创建了一个批处理文件来批量导入/导出工作项类型。
- 频繁将程序集文件复制到指定位置
- 创建了一个批处理文件,用于复制 .dll、.pdb 和 .wicc 文件。
- 创建了一个批处理文件,用于复制 .dll、.pdb 和 .wicc 文件。