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

将自定义类型列表集成到 TFS 生成模板中

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2014年5月19日

CPOL

4分钟阅读

viewsIcon

14148

将自定义类型列表集成到 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,然后一旦拥有服务列表,我们就会执行我们需要的操作。

就这样!坐下来享受这个很棒的自定义编辑器吧。

关注点

以下帖子对我帮助很大,如果有什么不清楚的地方,请阅读它们。

历史

2014 年 5 月 19 日 - 初始帖子。

© . All rights reserved.