TFS 工作项的表达式控件






4.55/5 (8投票s)
TFS 工作项的自定义控件,用于显示基于工作项字段内容计算出的表达式结果
引言
本文介绍了一个用于 Team Foundation Server 工作项表单的自定义控件。该控件是一个 `textbox`,用于显示数学表达式的计算结果,该数学表达式可以包含工作项中的其他数字字段。
背景
注意:本文并非旨在提供创建/修改 TFS 工作项或自定义控件的全面指南。有关详细信息,请参阅 “更多信息”部分中的链接。
Team Foundation Server 是一个优秀的应用程序生命周期管理平台。它提供的一项功能是能够创建和管理工作项,这些工作项封装了单个的工作单元。
TFS 开箱即用地提供了标准的“需求”、“Bug”、“任务”、“风险”等工作项类型,用于管理软件开发项目中常见的各种工件(这些项在两个项目模板中定义:MSF for Agile Software Development 和 MSF for CMMI Process Improvement)。然而,也可以定义新的工作项类型并修改现有的工作项类型(项目模板也是如此)。
工作项包含用于存储各种信息的字段;典型的字段包括标题、工作项创建者姓名、分配的优先级或估算的实现工作量。为了编辑这些字段的信息,每个工作项还定义了一个包含各种可编辑控件的表单的布局(也有不可编辑的控件,例如工作项更改历史记录)。当您创建新工作项或编辑现有工作项时,Team Explorer(Visual Studio 的一个组件)会显示这些表单。
Team Explorer 提供了几种现成的控件,例如简单的单行文本框、HTML 富文本控件、日期选择器、下拉列表或浏览器。可以创建新型控件并在 TFS 工作项表单中使用它们,这也是本文将要介绍的内容。
那么,这是什么?
在本文中,您将了解到 `ExpressionControl`,一个 TFS 自定义控件,用于显示数学表达式的计算结果。它的优点在于您可以在表达式中引用工作项的其他字段,并且如果更改其中一个字段的值,表达式将重新计算,控件上的值也会自动更新。如果您将一个工作项字段与该控件关联,计算值将存储在工作项本身中,因此可用于工作项查询和报告。
作为概念验证,我提供了一个修改版的 Risk 工作项,该工作项来自 MSF for CMMI Process Improvement 项目模板。在此版本中,我用“Impact”(影响)字段替换了“Severity”(严重性)字段,这是一个可以接受任何值的数字字段。我还添加了一个“Exposure”(风险暴露度)字段,该字段由 `ExpressionControl` 自动计算为“Probability”(概率)乘以“Impact”(影响)。
控件的源代码以 Visual Studio 2010 解决方案的形式提供,该解决方案包含一个 C# 类库和一个安装程序项目。您可以将此解决方案用作创建自己的 TFS 自定义控件的框架。
安装和使用控件
如果您想立即安装和使用 `ExpressionControl`,请按照以下步骤操作
- 显而易见,但请确保您已安装 Team Explorer 2010。
- 从 “下载”部分下载安装程序。
- 运行安装程序。这只会将两个 `.dll` 文件和一个 `.wicc` 文件(只是一个 XML 文件)复制到 Visual Studio 安装文件夹下的一个特殊子文件夹中。
现在,您可以将修改后的 Risk 工作项定义上传到服务器。您可以使用 Team Explorer 附带的 `witadmin` 工具来完成此操作。打开 Visual Studio 命令提示符并执行
witadmin importwitd /collection:(URL_of_your_TFS_server_and_collection)
/p:(name_of_the_team_project) /f:Risk.wit
或者,如果您已在 Visual Studio 中安装了 TFS Power Tools,请在 Visual Studio 菜单中选择 `Tools - Process Editor - Work Item Types - Import WIT`。
现在尝试创建一个名为 Risk 的新工作项。您会发现,当您修改 Probability 或 Impact 字段时,Exposure 字段会自动更新。
更通用地说,在工作项中使用该控件的方法是定义一个类型为 `ExpressionControl` 的新控件,并添加一个名为 `Expression` 的属性,其值为要计算的表达式。该表达式可以包含基本运算(+ - * /),操作数可以是数字,也可以是用方括号括起来的字段名,例如 `[Priority]`。例如,在修改后的 Risk 定义中,我们可以找到以下行
<Control
FieldName = "Konamiman.TFS.Exposure"
Type = "ExpressionControl"
Expression = "[Probability]*[Impact]"
Label = "Exposure:"
LabelPosition = "Left"
ReadOnly = "True">
将字段与控件关联不是强制性的,但如果您希望将计算值持久化到工作项中,则这是必需的。例如,我们可以创建一个显示具有优先级、影响和风险暴露度的 Risk 的工作项查询。
对于修改后的 Risk 工作项,我们定义了一个后备字段如下:
<FIELD name="Exposure" refname="Konamiman.TFS.Exposure" type="Integer">
<HELPTEXT>Exposure of the risk, calculated as probability by impact.</HELPTEXT>
</FIELD>
如果您知道计算结果将为非整数,则可能希望将字段定义为 `double` 而不是 `integer`。例如,您可以将表达式定义为 `([Probability]*[Impact])/100`。
要将新字段和控件插入现有工作项,您需要从服务器下载工作项定义文件。使用 `witadmin` 工具,您将执行:
witadmin exportwitd /collection:(URL_of_your_TFS_server_and_collection)
/p:(name_of_the_team_project) /n:(work_item_type_name) /f:(destination_file_name>
或者使用 TFS Power Tools,在 Visual Studio 菜单中选择 `Tools - Process Editor - Work Item Types - Export WIT`。
项目
TFS 自定义控件只是一个实现了 IWorkItemControl 接口的 Windows Forms 用户控件,并附带一个 XML 格式的描述符文件(`.wicc`)(有关详细信息,请参阅 “更多信息”部分中的链接)。因此,创建自定义控件所需的步骤如下:
- 创建一个新的 Visual Studio 解决方案,类型为“类库”。
- 添加对 `System.Windows.Forms` 和 `System.Drawing` 系统程序集的引用。
- 添加对以下 Team Explorer 程序集的引用:
%ProgramFiles%\Microsoft Visual Studio 10.0\Common7\IDE\PrivateAssemblies\ Microsoft.TeamFoundation.WorkItemTracking.Controls.dll
%ProgramFiles%\Microsoft Visual Studio 10.0\Common7\IDE\ReferenceAssemblies\ v2.0\Microsoft.TeamFoundation.WorkItemTracking.Client.dll
(注意:在您的系统中,根目录可能是 `%ProgramFiles(x86)%`。)
- 添加一个新项,类型为“用户控件”,并使其实现 `IWorkItemControl`。
- 在用户控件设计器表面添加适当的控件。
- 添加 `IWorkItemControl` 的代码(稍后将详细介绍)。
-
添加一个新 XML 文件,将其命名为 `YourControlName.wicc`(文件名将是 TFS 用来引用您的控件的名称),并将其编译操作设置为“Content”。文件的内容必须如下:
<?xml version="1.0"?> <CustomControl xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <Assembly>YourClassLibraryFileName.dll</Assembly> <FullClassName>YourControlFullClassName</FullClassName> </CustomControl>
编译代码后,您需要将生成的 `.dll` 文件与 `.wicc` 文件一起复制到以下位置:
%ProgramData%\Microsoft\Team Foundation\Work Item Tracking\Custom Controls\10.0
就是这样,Team Explorer 将识别您的控件并显示它,只要您已在工作项中声明了它。
安装程序
尽管部署控件就像将两个文件复制到 TFS 自定义控件文件夹一样简单,但拥有一个安装程序项目来完成此任务也是不错的选择。 Nick Hoggard 解释了如何创建这样的项目,但是由于我比他多做了几件事,我将在此描述我为创建安装程序所遵循的完整过程。
- 在解决方案中创建一个新项目,类型为“Setup project”。
- 打开“File System Editor”选项卡,并创建一个类型为“Custom Folder”的新文件夹,命名为 `CommonAppDataFolder`。将其 `DefaultLocation` 属性设置为 `[CommonAppDataFolder]`。
- 在 `CommonAppData` 文件夹下,按顺序创建一个文件夹层次结构 `Microsoft\Team Foundation\Work Item Tracking\Custom Controls\10.0`。请参阅以下截图,该截图摘自 Hoggart 的页面本身。
- 右键单击 10.0 文件夹,然后选择“Add - Project Output”。从您的类库项目中选择“Primary Output”和“Content Files”(可选,您也可以选择“Debug Symbols”,这在您想调试代码时很有用)。
- 在 Solution Explorer 中设置项目的“Detected Dependencies”文件夹下,选择所有检测到的 `Microsoft.TeamFoundation.*` 依赖项,右键单击并选择“Exclude”,以避免安装程序将它们与您的库一起复制。
- 在“User Interface Editor”窗格中,删除第二屏和第三屏。这些是安装位置选择屏幕(不是必需的,因为文件始终安装在固定位置)和安装确认屏幕(如果删除了安装位置选择屏幕,则无用)。当然,除非您想让用户选择是为所有用户安装还是仅为当前用户安装(我总是将 `InstallAllUser` 项目属性设置为 `True`)。
现在,我们将添加一个启动条件,以避免在未安装 Team Explorer 的情况下运行安装程序。这严格来说不是必需的,但会让我们的安装程序更整洁。
- 打开“Launch Conditions Editor”选项卡。
- 右键单击“Search Target Machine”,然后选择“Add Registry Search”。
- 选择刚添加的注册表搜索,打开属性窗口,并按如下方式设置属性:Name = Team Explorer 2010, Property = TEAMEXPLORERINSTALLED, Root = vsdrrHKCU, RegKey = `Software\Microsoft\VisualStudio\10.0_Config\InstalledProducts\Team Explorer`。请参阅以下截图。
- 右键单击“Launch Conditions”,然后选择“Add Launch Condition”。
- 选择刚添加的启动条件,打开属性窗口,并按如下方式设置属性:Name = Team Explorer 2010, Condition = TEAMEXPLORERINSTALLED, Message = Error: Team Explorer 2010 is not installed. 请参阅以下截图。
最后一步是适当地配置设置项目属性(作者、描述、URL 等)。您可以通过将“Konamiman”替换为您自己的姓名或公司名称来为自己使用提供的设置项目(以及类库项目)。
代码
现在我将解释控件代码本身的关键点。如果您想仔细查看,可以 下载 Visual Studio 解决方案。
表达式求值器
首先,我必须说我没有自己编写表达式求值器。相反,我使用了非常出色的 Sebastien Ros 的 State of the Art Expression Evaluation,它提供了我所需的所有功能,甚至更多。我建议您查看 Sebastien 的文章,但为了理解在 `ExpressionControl` 中如何使用求值器,您需要了解以下基本要点:
- 求值器的核心是 `Expression` 类。在实例化该类时,您传递一个要计算的表达式的 `string`,例如:
var expression = new Expression("([Priority] * [Impact]) / 100");
- 您使用 `Expression` 对象的 `Evaluate` 方法来触发表达式求值。该方法返回一个包含求值结果的对象(`ExpressionControl` 只处理数字,但 `Expression` 类也可以处理字符串、布尔值和 `DateTime`)。
- `Expression` 类可以处理参数,即在求值时动态解析的(用方括号括起来的)字母数字标识符。在求值过程中找到参数时,会触发一个名为 `EvaluateParameter` 的事件。此事件提供参数名称,并期望将参数值设置在关联的 `EventArgs` 的某个属性上。例如:
Expression e = new Expression("2 * [Pi]"); e.EvaluateParameter += delegate(string name, ParameterArgs args) { if (name == "Pi") { args.Result = 3.14; } }; var result = e.Evaluate();
表达式求值器的源代码嵌入在 `ExpressionControl` 项目本身中,位于 `ExpressionEvaluator` 文件夹下。求值器代码依赖于一个库 `Evaluant.Antlr.dll`,该库已被项目引用(当然,在安装时必须与控件库一起复制)。
控件代码
`ExpressionControl` 的可视化界面只是一个用于显示表达式求值结果的单行 `TextBox`。根据在包含工作项表单中如何配置控件的 `ReadOnly` 属性,它可以是只读的,也可以不是(当然,建议将其配置为只读)。Team Explorer 在您创建新工作项或选择现有工作项进行编辑时会创建控件的实例,前提是该控件已在工作项表单定义中声明。
我现在将枚举 `ExpressionControl` 类的主要成员,解释每个成员的关键点,以便您能对控件的工作方式有一个大致的了解。
- IWorkItemControl.Properties:这是一个 `StringDictionary`,其中包含控件的所有属性,这些属性在工作项表单定义(XML `Control` 元素的所有 XML 属性都包含在此处)中声明。当设置此属性时,我们会检索 `Expression` 属性并用它来实例化一个 `Expression` 对象,该对象的 `EvaluateParameter` 事件已分配给一个处理方法(`expression_EvaluateParameter`)。
- IWorkItemControl.WorkItemDatasource:这是包含该控件的表单正在编辑的工作项(尽管属性声明为 `Object`,但值始终为 `WorkItem` 类型)。当设置此属性时,我们会为工作项的 `FieldChanged` 事件(`OnFieldChanged` 方法)分配一个处理程序,以便在必要时能够适当地重新评估表达式并更新控件值。
- IWorkItemControl.FlushToDatasource:当需要将控件值备份到工作项的关联字段时(以便值与工作项数据一起持久化到 TFS 数据库中),会调用此方法。我们只需检查控件是否实际与工作项字段关联(如果未关联,则无处持久化),并且该字段是否为数字类型(字符串字段也可以接受,在这种情况下,值在设置之前简单地转换为 `ToString`),然后设置字段的新值。
- GenerateNewTextboxValue:在工作项表单加载时(通过 `IWorkItemControl.InvalidateDatasource` 方法)以及表达式中引用的字段之一更改时(通过 `OnFieldChanged` 方法)调用此方法。它只是调用 `Expression` 对象的 `Evaluate` 方法,并将结果作为 `string` 返回以设置到 `TextBox`(结果也缓存在 `calculationValue` 类字段中,以便 `FlushToDatasource` 方法可以使用它)。该方法处理所有可能的错误情况,在必要时返回适当的错误消息而不是计算结果。
- expression_EvaluateParameter:每当表达式求值器遇到表达式中的参数时,都会调用此方法。我们假设参数是字段名,并查询工作项以获取字段值。如果工作项未声明具有该名称的字段,或者字段不是数字类型,我们将抛出异常,该异常将被 `GenerateNewTextboxValue` 方法捕获并转换为错误消息。
以上就是控件的大部分内容。还有一些辅助成员,但控件的核心在于上面列出的成员。
调试
当我完成控件后,我发现它工作得很好,除了一个细节:当保存包含该控件的工作项时,Visual Studio 会显示一个“对象引用未设置为对象实例”的消息框,但否则会正确保存工作项。结果发现,`FieldChanged` 事件在工作项即将保存时触发,但 `WorkItemEventArgs` 参数的 `Field` 属性为 `null`。我不知道这是“bug 还是 feature”,但据我所见,忽略 `Field` 属性为 `null` 的事件似乎是安全的。
如果我没有调试代码,我永远找不到这个 bug。但是,您不能像调试普通 Visual Studio 项目那样直接调试 TFS 自定义控件(您无法“运行”它)。那么,我们能做什么呢? MSDN 有答案(向下滚动直到看到已接受的答案)。请注意:您需要跳过步骤 10 和 11(您无法“启动”类库项目)。为完整起见,我在此粘贴了该过程(原始内容引用的是 VS 2008,我已经将引用更改为 VS 2010)。
- 打开 VS 2010 并加载您的项目。
- 删除项目中的所有断点。
- 右键单击项目并转到项目属性。
- 对于配置,选择“Active (Debug)”。
- 在“Enable debuggers”部分,勾选“Enable unmanaged code debugging”并启用“Visual Studio hosting process”。
- 保存。
- 右键单击您的项目并单击“Clean”。
- 再次右键单击您的项目并单击“Rebuild”。
- 在 Windows Explorer 中,转到 `debug` 文件夹并将输出(`.dll`、`.exe` 等)复制到 `C:\Documents and Settings\All Users\Application Data\Microsoft\Team Foundation\Work Item Tracking\Custom Controls\10.0`(这是 TF 2010 WI 查找 `.dll` 和 `.wicc` 文件的位置)。
- (跳过此项!)在工具栏上,将下拉列表设置为“debug”,然后单击箭头。
- (跳过此项!)您将看到一个带有控件的小屏幕。应该有一个“load”按钮。单击它,并确保从步骤 9 中指定的路径加载 `.dll` / `.exe`。
- 保持打开状态,不要关闭任何东西。打开另一个 VS 2010 实例。
- 在 Team Explorer 中,打开包含您的自定义控件的工作项类型(Add Work Item-> . . . . . )。
- 返回到 VS 2010 的第一个实例(步骤 1)并转到 Debug->Attach to Process。
- 在“Attach to Process”窗口中,将“Transport”保留为“Default”,将“Attach To”字段设置为选中“managed code”和“native code”。
- 在“Available processes”窗口中,您将看到进程 `devenv.exe` 及其 ID 和您刚刚创建的工作项的标题。选择该行并单击“Attach”。
- 您将看到一些后台处理,因为它正在加载符号。完成后,相应地设置您的断点。
- 返回到您的第二个 VS2010 实例(步骤 12),并执行您需要对在步骤 13 中创建的工作项执行的操作,以确保命中断点。
- 一旦您认为断点已命中,请返回到第一个 VS2010 实例(步骤 1 和步骤 17),您将看到您设置的断点已命中。现在您只需调试(叹气!)。
更多信息
如果您想了解更多关于 TFS 工作项自定义和 TFS 自定义控件的信息,请参阅以下资源:
- MSDN 上的自定义 TFS 工作项类型
- MSDN 上的工作项跟踪自定义控件
- Gregg Boer 博客上的工作项跟踪自定义控件(推荐,包含一些高级信息)
- Nick Hoggard 博客上的自定义工作项控件入门(不要介意它引用的是 TFS 的 beta 版本,提供的信息对于 RTM 版本也相关)。 CodePlex 上的 TFS 工作项跟踪自定义控件(提供的解决方案针对 TFS 2008,但只需更新 `Microsoft.TeamFoundation.*` 程序集的项目引用到其 TFS 2010 对应版本,即可在 TFS 2010 中正常工作)。
最后说明
这是我的第一个 TFS 自定义控件,因此代码可能包含错误。如果出现这种情况,或者您有改进建议,请给我留言。
历史
- 2010 年 10 月 1 日:第一个版本
- 2010 年 10 月 6 日:更新了屏幕截图,现在所有 Visual Studio 标签都以英语显示。