将自定义类型列表集成到 TFS 生成模板中
将自定义类型列表集成到 TFS 生成模板中
引言
在本文中,我将展示如何将自定义对象列表集成到 TFS 生成模板中。我将假设您之前已经开发了一个自定义 TFS 生成活动,并且对生成模板和定义有所了解;如果没有,请参考本文末尾的资源以获得进一步的解释。
如果您的生成正在执行一系列相似的任务,那么将它们制成列表通常会更灵活,并使您的生成定义更易于配置。我在这方面花了一些功夫才使其奏效,所以希望这能帮助到遇到同样问题的人。最终结果如下所示。
我定义了三个 NServiceBus 服务,一旦展开,其属性可以直接在生成定义中修改,或者通过使用自定义编辑器进行修改。
背景
TFS 生成支持基本的原始类型自定义,例如,我们过去通过使用 bools 和 strings 自定义模板来部署服务,如下所示。
虽然这工作正常,但不够灵活。如果我们添加更多服务,将需要修改生成模板,向自定义活动添加必要的代码来处理新服务,然后重新编译活动,修改定义等。这涉及到许多重复的步骤。这是将其转换为列表的主要动机之一,以便我们可以更好地管理它们。
我查看了默认模板,发现最接近的功能是“要生成的配置”或添加要运行的自动化测试。
我想看看 Microsoft 是如何实现该功能的。受到 Rory Primrose 的博文的启发,我使用 ILSpy 深入研究了工作流活动。经过大量的试错,我最终在 Microsoft.TeamFoundation.Build.Workflow.Acitivities.dll 程序集中找到了 PlatformConfigurationList
。在研究了 PlatformConfigurationList
的代码并追踪了引用的对象后,我对实现我想要的功能所需的内容有了一个大致的了解,我将在下一节中进行解释,并附上必要的代码。
代码
这是我想要实现的目标,我将在适当的时候指出圈出的部分。
首先,我有一个 Service
对象,它代表我们需要部署的单个 NServiceBus 服务,一旦在生成定义中展开,其属性就是可直接编辑的。
using System;
using System.ComponentModel;
namespace MyWorkflowActivities
{
[Serializable]
[TypeConverter(typeof(ExpandableObjectConverter))]
public class Service
{
[Browsable(true)]
[RefreshProperties(System.ComponentModel.RefreshProperties.All)]
public bool Deploy { get; set; }
[Browsable(true)]
[RefreshProperties(System.ComponentModel.RefreshProperties.All)]
public string Server { get; set; }
[Browsable(true)]
[RefreshProperties(System.ComponentModel.RefreshProperties.All)]
public string TargetFolder { get; set; }
[Browsable(true)]
[RefreshProperties(System.ComponentModel.RefreshProperties.All)]
public string ServiceName { get; set; }
[Browsable(true)]
[RefreshProperties(System.ComponentModel.RefreshProperties.All)]
public string Description { get; set; }
[Browsable(false)]
public override string ToString()
{
return string.Format("{0}@{1}, deploy: {2}",
this.ServiceName,
this.Server,
this.Deploy);
}
}
}
然后我有一个继承自 BindingList<T>
的 ServiceList
,这样我就可以轻松地将服务绑定到自定义编辑器中的 DataGridView
显示。ServiceList
实现 ICustomTypeDescriptor
,这是必需的,因为一旦在生成定义中展开,就会显示服务的列表而不是 BindingList<T>
的属性。ServiceList
是将在生成模板和我的活动中使用的对象。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
namespace MyWorkflowActivities
{
[Serializable]
[TypeConverter(typeof(ServiceListConverter))]
public class ServiceList : BindingList<Service>, ICustomTypeDescriptor
{
public ServiceList()
{
}
public ServiceList(IList<Service> services)
: base(services)
{
}
[Browsable(false)]
public override string ToString()
{
var c = this.Count(x => x.Deploy);
return string.Format("{0}/{1} services are set to deploy", c, this.Count);
}
public AttributeCollection GetAttributes()
{
return System.ComponentModel.TypeDescriptor.GetAttributes(this, true);
}
public string GetClassName()
{
return System.ComponentModel.TypeDescriptor.GetClassName(this, true);
}
public string GetComponentName()
{
return System.ComponentModel.TypeDescriptor.GetComponentName(this, true);
}
public TypeConverter GetConverter()
{
return System.ComponentModel.TypeDescriptor.GetConverter(this, true);
}
public EventDescriptor GetDefaultEvent()
{
return System.ComponentModel.TypeDescriptor.GetDefaultEvent(this, true);
}
public PropertyDescriptor GetDefaultProperty()
{
return System.ComponentModel.TypeDescriptor.GetDefaultProperty(this, true);
}
public object GetEditor(Type editorBaseType)
{
return System.ComponentModel.TypeDescriptor.GetEditor(this, editorBaseType, true);
}
public EventDescriptorCollection GetEvents(Attribute[] attributes)
{
return System.ComponentModel.TypeDescriptor.GetEvents(this, attributes, true);
}
public EventDescriptorCollection GetEvents()
{
return System.ComponentModel.TypeDescriptor.GetEvents(this, true);
}
public PropertyDescriptorCollection GetProperties(Attribute[] attributes)
{
return this.GetProperties();
}
public PropertyDescriptorCollection GetProperties()
{
System.ComponentModel.PropertyDescriptorCollection propertyDescriptorCollection = new System.ComponentModel.PropertyDescriptorCollection(null);
for (int i = 0; i < this.Count; i++)
propertyDescriptorCollection.Add(new ServicePropertyDescriptor(this, i));
return propertyDescriptorCollection;
}
public object GetPropertyOwner(PropertyDescriptor pd)
{
return this;
}
}
}
ServiceList
有一个 ServiceListConverter
,它继承自 ExpandableObjectConverter
。这里有趣的部分是 ConvertTo()
,你可以使用返回的对象(这里是字符串)来显示关于列表的状态消息,如上图红圈所示。
using System;
using System.ComponentModel;
using System.Linq;
namespace MyWorkflowActivities
{
public class ServiceListConverter : ExpandableObjectConverter
{
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
return destinationType == typeof(string) || base.CanConvertTo(context, destinationType);
}
public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
{
if (destinationType == typeof(string) && value is ServiceList)
{
ServiceList list = (ServiceList)value;
var c = list.Count(x => x.Deploy);
return string.Format("{0}/{1} service(s) are set to deploy", c, list.Count);
}
return base.ConvertTo(context, culture, value, destinationType);
}
}
}
ServicePropertyDescriptor
用于 ServiceList.GetProperties()
,其中最终在展开时显示服务列表。我们也可以为每个服务显示状态消息,如上图蓝圈所示。
using System;
using System.ComponentModel;
namespace MyWorkflowActivities
{
public class ServicePropertyDescriptor : PropertyDescriptor
{
private ServiceList serviceList;
private Service service;
private int index;
public ServicePropertyDescriptor(ServiceList serviceList, int index) :
base(string.Format("{0}{1}. {2}",
(index + 1) > 9 ? string.Empty : "0",
(index + 1).ToString(System.Globalization.CultureInfo.InvariantCulture),
serviceList[index].ServiceName), new System.Attribute[0])
{
this.serviceList = serviceList;
this.index = index;
this.service = this.serviceList[this.index];
}
public override bool CanResetValue(object component)
{
return false;
}
public override Type ComponentType
{
get { return this.PropertyType; ; }
}
public override object GetValue(object component)
{
return this.service;
}
public override bool IsReadOnly
{
get { return false; }
}
public override Type PropertyType
{
get { return this.service.GetType(); }
}
public override void ResetValue(object component)
{
throw new NotImplementedException();
}
public override void SetValue(object component, object value)
{
this.service = (Service)value;
this.serviceList[this.index] = this.service;
}
public override bool ShouldSerializeValue(object component)
{
return false;
}
}
}
然后是 UITypeEditor
,它充当 TFS 生成和我们自定义编辑器之间的中间人。每当我们打开自定义编辑器时,就会读取生成定义中的现有数据并将其传递给自定义编辑器。当我们完成编辑器中的编辑后,它会将数据发送回以进行保存。
using System;
using System.ComponentModel;
using System.Drawing.Design;
using System.Windows.Forms;
using System.Windows.Forms.Design;
namespace MyWorkflowActivities
{
public class SvcEditor : UITypeEditor
{
public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
{
if (provider != null)
{
IWindowsFormsEditorService editorService = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService));
if (editorService == null)
{
return value;
}
var serviceList = value as ServiceList;
using (SvcDialog dialog = new SvcDialog())
{
dialog.ServiceList = serviceList;
if (editorService.ShowDialog(dialog) == DialogResult.OK)
{
value = new ServiceList(dialog.ServiceList);
}
}
}
return value;
}
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
{
return UITypeEditorEditStyle.Modal;
}
}
}
说到自定义编辑器,它实际上只是一个驻留在同一命名空间中的简单 Windows 窗体。
它的代码隐藏文件。
using System;
using System.Windows.Forms;
namespace MyWorkflowActivities
{
public partial class SvcDialog : Form
{
public ServiceList ServiceList { get; set; }
public DataGridView DataGridView
{
get { return this.dgServices; }
}
public SvcDialog()
{
InitializeComponent();
}
private void btnAdd_Click(object sender, EventArgs e)
{
this.ServiceList.Add(new Service { TargetFolder = @"Infrastructure" });
}
private void btnRemove_Click(object sender, EventArgs e)
{
if (this.dgServices.SelectedRows.Count > 0)
this.ServiceList.Remove((Service)this.dgServices.SelectedRows[0].DataBoundItem);
}
private void btnOK_Click(object sender, EventArgs e)
{
this.DialogResult = System.Windows.Forms.DialogResult.OK;
this.Close();
}
private void SvcDialog_Load(object sender, EventArgs e)
{
this.dgServices.DataSource = this.ServiceList;
}
private void btnUp_Click(object sender, EventArgs e)
{
if (this.dgServices.SelectedRows.Count == 0) return;
var row = this.dgServices.SelectedRows[0];
var prevIdx = row.Index;
if (prevIdx - 1 < 0) return;
var item = this.ServiceList[prevIdx];
this.ServiceList.Remove(item);
this.ServiceList.Insert(prevIdx - 1, item);
this.dgServices.Rows[prevIdx - 1].Selected = true;
}
private void btnDown_Click(object sender, EventArgs e)
{
if (this.dgServices.SelectedRows.Count == 0) return;
var row = this.dgServices.SelectedRows[0];
var prevIdx = row.Index;
if (prevIdx + 1 >= this.ServiceList.Count) return;
var item = this.ServiceList[prevIdx];
this.ServiceList.Remove(item);
this.ServiceList.Insert(prevIdx + 1, item);
this.dgServices.Rows[prevIdx + 1].Selected = true;
}
}
}
最后,我们在生成模板中将所有内容粘合在一起,我将假定您之前已经处理过生成模板,并且只在此处显示相关部分。
首先,导入我们的程序集,您还需要确保生成可以访问您的程序集,如果您不确定如何操作,请查看一些引用的资源。
xmlns:ldbw="clr-namespace:MyWorkflowActivities;assembly=MyWorkflowActivities"
创建一个属性来保存已添加/修改服务的列表。
<x:Members>
...
<x:property name="NServiceBusServices" type="InArgument(ldbw:ServiceList)">
</x:property>
</x:Members>
为 NServiceBusServices
属性挂接我们的自定义编辑器。
<this:Process.Metadata>
<mtbw:ProcessParameterMetadataCollection>
...
<mtbw:ProcessParameterMetadata Category="Services Configuration" Description="Configure NServiceBus Services" DisplayName="NServiceBus Services" Editor="MyWorkflowActivities.SvcEditor, MyWorkflowActivities" ParameterName="NServiceBusServices" />
然后将我们的自定义活动作为生成的一部分调用。
<Sequence DisplayName="">
<ldbw:SvcDeployAsync DisplayName="Service Deployment"
ServiceList="[NServiceBusServices]"
</Sequence>
请注意,我没有包含自定义活动的 C# 代码,因为它将类似于您可能开发的任何活动,它将有一个公共属性 InArgment<ServiceList>
来读取模板中指定的 ServiceList
,然后一旦拥有服务列表,我们就会执行我们需要的操作。
就这样!坐下来享受这个很棒的自定义编辑器吧。
关注点
以下帖子对我帮助很大,如果有什么不清楚的地方,请阅读它们。
- Customize Team Build 2010 - Ewald Hofman 著
- Jason Prickett 的 TFS 系列博文
- Rory Primrose 著:TFS Build 2010 生成定义编辑器对自定义类型的完全支持
历史
2014 年 5 月 19 日 - 初始帖子。