Team System Web Access: Time Entry Host Custom Control

Previously, Michael Ruminer had created a Silverlight TFS TimeEntry control for use within Team Explorer. His project is hosted within the MSDN Code Gallery at: "http://code.msdn.microsoft.com/SilverlightWITimeEnt". His project creates everything needed for a rudimentary time tracking system within TFS. One of the remaining open issues was how to utilize the same Silverlight TimeEntry control through Team System Web Access (TSWA).

I have created a simple server side web control that will host Michael's TimeEntry control. When I first tried to tackle this issue, I had to learn about the Work Item Templates used by TSWA and what controls already existed. I found the existing controls to be quite limited. As a result, I opted to create my own custom web control. To my surprise, the code changes ended up being extremely simple. First, let me explain what is required to create a custom web control that can be used by a work item template in TSWA.

For TSWA to render a custom control, the control must:
1. Inherit from System.Web.UI.WebControls.WebControl
2. The control must have a default constructor
3. Implement Microsoft.TeamFoundation.WorkItemTracking.Controls.IWorkItemControl *
4. Implement Microsoft.TeamFoundation.WebAccess.WorkItemTracking.Controls.IWorkItemWebControl *

* Note: These two interfaces can both be found in: "C:\Program Files\Microsoft Visual Studio 2008 Team System Web Access\Web\bin\". My initial struggles with getting the control render related to the fact I failed to implement BOTH interfaces. Creating a custom windows control only requires the "IWorkItemControl" interface be implemented.

I had wanted to keep the control as simple as possible so all it does is render a simple HTML page with an IFrame. The IFrame source points to the silverlight control. At first, I tried to just render an IFrame on it's own. The control seemed to load and function properly, however, I was getting a javascript error indicating "this.m_buttonsTable.offsetLeft" was null. I found this somewhat confusing since I hadn't written any javascript and the error occured regardless of what the IFrame pointed to. I then realized the script was in a WebResource.axd file, therefore, it must have been generated by the .NET framework. I resolved the javascript issue by wrapping the IFrame in a fully valid HTML page. The control continued to work but without throwing any javascript errors.

Once the control has been built, you have to deploy it. Create a "wicc" file by the same name as the library. Copy both the wicc file and the dll to "C:\Program Files\Microsoft Visual Studio 2008 Team System Web Access\Web\App_Data\CustomControls\" on the server hosting TSWA (assuming you used the default install for TSWA). TSWA will look in the CustomControls folder by default when trying to resolve the assemblies. In my example, my library name was "TimeEntryHost" so my wicc file was titled "TimeEntryHost.wicc".

Its contents are:

  1. <?xml version="1.0"?>
  2. <CustomControl xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  3.   <Assembly>TimeEntryHost.dll</Assembly>
  4.   <FullClassName>TFS.TimeEntryHost</FullClassName>
  5. </CustomControl>



The template itself renders the control in a tab. Here is the snippet showing how that should be:
  1. <Tab Label="Time Entry">
  2.   <Control Type="TimeEntryHost" Label="" LabelPosition="Top" Dock="Left" />
  3. </Tab>


This control could be used to host any web page as well. I'm actually surprised that it wasn't one of the standard pre-built controls due to its simplicity. There is, however, a caveat to the whole thing IF you are rendering DIFFERENT custom controls in Team Explorer (windows) and TSWA (web). Simply put, the work item template has a layout tag. Well, the layout tag needs to be duplicated each with a target attribute. For windows, it should read and for TSWA it should read . I had found this useful tidbit on Shai Raiten's blog at http://blogs.microsoft.co.il/blogs/shair/archive/2008/10/22/how-to-manage-custom-controls-in-team-system-and-web-access.aspx.

The final issue I ran into was importing the template into TFS so it rendered in both Team Explorer and TSWA. If you use the "Process Editor" tool in Visual Studio, you will lose one of the layouts you created causing the control to only render in one of the environments. To get around this issue, import the template from the commmand line. I don't recall where I found that information otherwise I would give proper credit. The import command will look something like the one below:
witimport /f "C:\\Task.xml" /t http:// /p ""

Below is the source code to the control itself. I'm sure it is not the most efficient and there is definitely room to improve, but it simple working example of how you can host a Silverlight control in a Work Item Template.

  1. using System;
  2. using System.ComponentModel;
  3. using System.Web.UI;
  4. using System.Web.UI.WebControls;
  5. using Microsoft.TeamFoundation.WorkItemTracking.Controls;
  6. using Microsoft.TeamFoundation.WebAccess.WorkItemTracking.Controls;
  7. using Microsoft.TeamFoundation.WorkItemTracking.Client;
  8.  
  9. namespace TfsImplementation {
  10.     [Serializable]
  11.     public class TimeEntryHost : WebControl, IWorkItemControl, IWorkItemWebControl {
  12.         public TimeEntryHost() {
  13.             SetDefaults();
  14.         }
  15.  
  16.         private void SetDefaults() {
  17.             this.Width = 843;
  18.             this.Height = 493;
  19.         }
  20.  
  21.         protected override void RenderContents(HtmlTextWriter output) {
  22.             string iframeHtml = "<iframe src=\"{0}\" style=\"WIDTH: {1}; HEIGHT: {2};\"></iframe>";
  23.             string urlPath = string.Format("http://TimeControl_TFS.Web/TimeControl_TFSTestPage.aspx?name={0}&wi={1}",
  24.                 Environment.UserName,
  25.                 _workItem.Id);
  26.  
  27.             output.Write("<html>");
  28.             output.Write("<head>");
  29.             output.Write("<title></title>");
  30.             output.Write("</head>");
  31.             output.Write("<body>");
  32.             output.Write("<form id=\"form1\">");
  33.             output.Write("<div>");
  34.             output.Write(string.Format(iframeHtml, urlPath, Width, Height));
  35.             output.Write("</div>");
  36.             output.Write("</form>");
  37.             output.Write("</body>");
  38.             output.Write("</html>");
  39.         }
  40.  
  41.         #region IWorkItemControl Members
  42.  
  43.         public event EventHandler AfterUpdateDatasource;
  44.         public event EventHandler BeforeUpdateDatasource;
  45.  
  46.         public void Clear() {
  47.             RenderContents(new HtmlTextWriter(new System.IO.StringWriter()));
  48.         }
  49.  
  50.         public void FlushToDatasource() {
  51.             RenderContents(new HtmlTextWriter(new System.IO.StringWriter()));
  52.         }
  53.  
  54.         public void InvalidateDatasource() {
  55.             RenderContents(new HtmlTextWriter(new System.IO.StringWriter()));
  56.         }
  57.  
  58.         public void SetSite(IServiceProvider serviceProvider) {
  59.             //throw new NotImplementedException();
  60.         }
  61.  
  62.         public System.Collections.Specialized.StringDictionary Properties { get; set; }
  63.         public bool ReadOnly { get; set; }
  64.         public string WorkItemFieldName { get; set; }
  65.  
  66.         private WorkItem _workItem;
  67.         public object WorkItemDatasource {
  68.             get { return _workItem; }
  69.             set { _workItem = value as WorkItem; }
  70.         }
  71.  
  72.  
  73.         #endregion
  74.  
  75.         #region IWorkItemWebControl Members
  76.  
  77.         public string ClientEditorObjectId { get; set; }
  78.         public string ClientObjectId { get; private set; }
  79.         public string ControlId { get; set; }
  80.         public string Label { get; set; }
  81.         public string ThemeUrl { get; set; }
  82.  
  83.         public string GetClientUpdateScript() {
  84.             return string.Empty;
  85.         }
  86.  
  87.         public void InitializeControl() {
  88.             //throw new NotImplementedException();
  89.         }
  90.  
  91.         #endregion
  92.     }
  93. }