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

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2014年2月20日

CPOL

8分钟阅读

viewsIcon

44223

downloadIcon

325

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 文件。

参考文献

© . All rights reserved.