将 HTML 字段数据导入 TFS 数据仓库
关于如何将 HTML 数据导入数据仓库的一个小技巧。
问题
之前,我面临这样一种情况:我想在流程模板工作项中使用 HTML 控件,但需要将该控件中的数据传递到数据仓库以进行报告。对于那些了解流程 WIT 如何工作的人来说,您会知道您不能将 HTML 数据类型字段标记为可报告。今天,我将向您展示我是如何做到这一点的。
开始之前
在您阅读本文之前,我只想告诉您,这仅允许少量 HTML,它旨在用于小的标记更改,例如粗体、斜体和下划线。我还没有完全分析所有兼容性问题,但我知道当您将图像添加到 HTML 时,此方法有时不起作用。这很可能是因为字符串字段在 TFS 数据仓库中是 `[nvarchar]`(256)。
成功的步骤
安装正确的工具
您不需要这些工具,但它会使开发、管理和实施流程模板更改变得更容易。
添加 HTML 字段
我使用 Card.xml,它位于由 ALM Rangers 提供的 Microsoft Kanban 1.0 流程模板中,这篇 博文解释了如何安装它。因此,使用此流程模板,您可以浏览到 `~\Microsoft Kanban 1.0\WorkItem Tracking\TypeDefinitions` 并在 Visual Studio 2012 中打开 *Card.xml*。您应该看到如下所示的窗口
单击“新建”并填写如下所示的表格
单击“规则”选项卡,然后单击“新建”并选择“AllowExistingValue”,如下所示
现在添加第二个字段,该字段与第一个字段相同,只是这次将 _Copy 添加到名称和引用名称字段中,如下所示。在此之后,添加与之前添加的相同的规则
将新的 HTML 字段添加到布局
单击上面的“布局”,然后通过右键单击任何不是控件的节点并选择 *新建控件* 来添加新控件。填写“字段名称”和“类型”属性,如下所示,其余由您决定。
为团队更新 WIT
接下来,我们需要更新 TFS 中的 Card WIT。为此,请单击“工具”>“流程编辑器”>“工作项类型”>“导入 WIT”。浏览到您正在修改的 Card.xml 文件,然后选择要导入此更改的团队,然后单击“确定”。
创建 TFS 的订阅服务器插件
这现在已经通过 TFS 更新了所选团队的 Card 窗口的窗口。转到 TFS WebAccess 门户并添加一张新卡,您会注意到该字段在那里。如果您填写表格并单击“保存”,您会注意到一切都保存得很顺利,没有任何问题。接下来,我们需要创建一个新的类库并添加一个 *TFSFunctions.cs* 类(此类中的源代码取自 ALM Planning zip 文件中的 GlobalList Updater 项目并进行了一些修改)。将下面的代码添加到 *TFSFunction.cs* 中。
namespace HtmlFieldsInReports
{
#region
using System;
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.Framework.Server;
using Microsoft.TeamFoundation.WorkItemTracking.Client;
#endregion
public static class TFSFunctions
{
#region Public Methods and Operators
public static WorkItemCollection ExecuteQuery(WorkItemStore store,
string query, string teamProjectName, string processStepWorkItemType)
{
query = query.ToLower().Replace("@project", teamProjectName);
query = query.ToLower().Replace("@processstepworkitemtype",
processStepWorkItemType);
return store.Query(query);
}
public static Uri GetTFSUri(TeamFoundationRequestContext requestContext)
{
return new Uri(
requestContext.GetService<TeamFoundationLocationService>().GetServerAccessMapping(
requestContext).AccessPoint.Replace("localhost",
Environment.MachineName) + "/" + requestContext.ServiceHost.Name);
}
public static TfsTeamProjectCollection GetTeamProjectCollection(
string requestContextVirtualDirectory, string workItemChangedEventDisplayUrl)
{
string tpcUrl = GetTeamProjectCollectionUrl(
requestContextVirtualDirectory, workItemChangedEventDisplayUrl);
var collection = new TfsTeamProjectCollection(new Uri(tpcUrl));
collection.EnsureAuthenticated();
return collection;
}
public static string GetTeamProjectCollectionUrl(
string requestContextVirtualDirectory, string workItemChangedEventDisplayUrl)
{
string[] strArray = workItemChangedEventDisplayUrl.Split('/');
return string.Format("{0}//{1}{2}",
strArray[0], strArray[2], requestContextVirtualDirectory);
}
public static WorkItemStore GetWorkItemStore(TfsTeamProjectCollection collection)
{
return (WorkItemStore)collection.GetService(typeof(WorkItemStore));
}
#endregion
}
}
并且还添加一个 *HtmlFieldSyncSubscriber.cs* 文件,并在其中包含以下代码。
namespace HtmlFieldsInReports
{
#region
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.Common;
using Microsoft.TeamFoundation.Framework.Server;
using Microsoft.TeamFoundation.WorkItemTracking.Client;
using Microsoft.TeamFoundation.WorkItemTracking.Server;
#endregion
public class HtmlFieldSyncSubscriber : ISubscriber
{
// ISubscriber - requested event types
// ISubscriber - name of the plugin
#region Public Properties
public string Name
{
get
{
return "BinaryDigit.Subscribers.HtmlFieldsInReports";
}
}
public SubscriberPriority Priority
{
get
{
return SubscriberPriority.Low;
}
}
#endregion
// ISubscriber - event handler
#region Public Methods and Operators
public EventNotificationStatus ProcessEvent(
TeamFoundationRequestContext requestContext, NotificationType notificationType,
object notificationEventArgs, out int statusCode,
out string statusMessage, out ExceptionPropertyCollection properties)
{
this.WriteInfo("In the EventNotificationStatus ProcessEvent method now",
EventLogEntryType.Information);
string strDump = string.Empty;
statusCode = 0;
properties = null;
statusMessage = string.Empty;
try
{
if (notificationType == NotificationType.Notification &&
notificationEventArgs is WorkItemChangedEvent)
{
// change this object to be a type we can easily get into
var workItemEvent = notificationEventArgs as WorkItemChangedEvent;
string currentField = string.Empty;
IntegerField id = null;
foreach (IntegerField item in workItemEvent.CoreFields.IntegerFields)
{
if (string.Compare(item.Name, "ID", true) == 0)
{
id = item;
break;
}
}
if (id != null)
{
try
{
Uri uri = TFSFunctions.GetTFSUri(requestContext);
if (workItemEvent.TextFields != null)
{
var affectedWorkItems = new List<WorkItem>();
using (TeamFoundationServer tfs =
TeamFoundationServerFactory.GetServer(uri))
{
var wit = (WorkItemStore)tfs.GetService(typeof(WorkItemStore));
foreach (TextField item in workItemEvent.TextFields)
{
if (item.Name.ToLower().EndsWith("_copy") &&
item.ReferenceName.ToLower().EndsWith("_copy"))
{
currentField = item.Name;
WorkItem wi = this.SetFieldValue(wit, id, item, uri);
if (wi != null)
{
affectedWorkItems.Add(wi);
}
}
}
if (affectedWorkItems.Count > 0)
{
wit.BatchSave(affectedWorkItems.ToArray());
}
}
}
else
{
return EventNotificationStatus.ActionPermitted;
}
}
catch (Exception ex)
{
this.WriteInfo("Failed to update field '" +
currentField + "'.\n\n" + ex, EventLogEntryType.Error);
if (!string.IsNullOrEmpty(currentField))
{
statusMessage =
"Failed to update field '" + currentField + "'.";
return EventNotificationStatus.ActionDenied;
}
}
}
}
}
catch (Exception ex)
{
// diagnostics
strDump = string.Format(
"There was a unhandled exception: {0} \n {1}", ex, ex.StackTrace);
this.WriteInfo(strDump, EventLogEntryType.Error);
}
return EventNotificationStatus.ActionPermitted;
}
public Type[] SubscribedTypes()
{
return new[] { typeof(WorkItemChangedEvent) };
}
// helper - dumping diagnostics
public void WriteInfo(string strDump, EventLogEntryType entryType)
{
var log = new EventLog();
log.Source = "TFS Services";
if (!EventLog.SourceExists(log.Source))
{
EventLog.CreateEventSource(log.Source, "Application");
}
string strMessage = string.Format("The TFS server plugin {0} provides " +
"the following logging information:\n\n{1}",
Assembly.GetCallingAssembly().GetName().Name, strDump);
log.WriteEntry(strMessage, entryType);
}
#endregion
#region Methods
private WorkItem SetFieldValue(WorkItemStore wit,
IntegerField id, TextField currentHtmlField, Uri uri)
{
WorkItem affectedWorkItem = null;
WorkItemCollection result = wit.Query(
"SELECT [System.Id] FROM WorkItems WHERE [System.Id] = " +
id.NewValue);
foreach (WorkItem wi in result)
{
string fieldLookingFor = currentHtmlField.Name.Remove(
currentHtmlField.Name.ToLower().IndexOf("_copy")).Trim();
foreach (Field item in wi.Fields)
{
if (string.Compare(item.Name, fieldLookingFor, true) == 0)
{
if (item.Value.ToString() != currentHtmlField.Value)
{
wi.Open();
item.Value = currentHtmlField.Value;
affectedWorkItem = wi;
}
break;
}
}
}
return affectedWorkItem;
}
#endregion
}
}
您需要将以下引用添加到您的项目中,然后它将构建
将插件添加到 TFS
编译完成后,获取输出的程序集并将它们复制到 TFS 服务器上的 *C:\Program Files\Microsoft Team Foundation Server 11.0\Application Tier\Web Services\bin\Plugins*。现在,当您在任何 Card 工作项窗口中更改 Card 时,此插件会将 HTML 从您的 _Copy 字段复制到另一个字段中,并且当数据移动到数据仓库时,HTML(字符串字段)也将被移动。
验证数据
数据同步后,您可以使用下面的 sql 查询来验证插件是否正常工作。
SELECT *
FROM [dbo].[DimWorkItem] with (nolock)
WHERE [Fields_MyHtmlField] IS NOT NULL
order by [WorkItemSK] desc
在报告中使用 HTML 字段数据
现在您可以编写报告,并在显示该字段时将其标记为 HTML 字段,如下所示。
希望这能帮助您,如果您需要,请随时要求提供有关此主题的更多信息。