Click here to Skip to main content
15,892,298 members
Articles / Programming Languages / C#

Expression Control for TFS Work Items

Rate me:
Please Sign up or sign in to vote.
4.55/5 (8 votes)
5 Oct 2010CPOL16 min read 66.1K   2.1K   17  
Custom control for TFS Work Items that shows the result of calculating an expression based on the work item fields contents
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Windows.Forms;
using Evaluant.Calculator;
using Microsoft.TeamFoundation.WorkItemTracking.Client;
using Microsoft.TeamFoundation.WorkItemTracking.Controls;
using System.Linq;

namespace Konamiman.TFS.Controls
{
    /// <summary>
    /// Custom control that shows the result of a calculation
    /// that involves the value of one or more numeric fields of the work item.
    /// </summary>
    /// <remarks>
    /// <para>
    /// The definition for this control on the work item template needs an attribute named "expression" that details
    /// how the calculation is performed. Numeric fields on the work item can be referenced as "[Name]" in the expression.
    /// For example: <c>&lt;Control FieldName="MyFields.Exposure" Type="CalculationControl" Label="Exposure:" ReadOnly="True" expression="([Probability]*[Impact])/100"&lt;</c>
    /// </para>
    /// <para>
    /// If the control is associated to a numeric field, the calculated value is saved within the work item and can be displayed in work item queries.
    /// </para>
    /// <para>
    /// In order to perform the calculation, an expression evaluator posted at The Code Project is used:
    /// http://www.codeproject.com/KB/recipes/sota_expression_evaluator.aspx
    /// (note that it depens on an external library, Evaluant.Antlr.dll)
    /// </para>
    /// </remarks>
    public partial class ExpressionControl : UserControl, IWorkItemControl
    {
        #region Private fields

        private decimal calculationValue;
        Expression expressionObject = null;
        private WorkItem workItem = null;
        private Field[] workItemFields = null;
        private StringDictionary properties = null;
        private List<string> fieldsInExpression = null;

        #endregion

        #region Constructor

        public ExpressionControl() : base()
        {
            InitializeComponent();
            fieldsInExpression = new List<string>();
        }

        #endregion

        #region Public properties and events

        /// <summary>
        /// Get and set the control attributes.
        /// When setting, the expression is retrieved and the expression object created.
        /// </summary>
        StringDictionary IWorkItemControl.Properties
        {
            get
            {
                return properties;
            }
            set
            {
                properties = value;
                fieldsInExpression.Clear();
                if(properties == null) {
                    return;
                }

                var expression = properties["expression"];
                if(string.IsNullOrEmpty(expression)) {
                    expression = properties["Expression"];
                }

                if(string.IsNullOrEmpty(expression)) {
                    expressionObject = null;
                    txtValue.Text = "(Expression not defined)";
                    return;
                }

                expressionObject = new Expression(expression);
                expressionObject.EvaluateParameter += new EvaluateParameterHandler(expression_EvaluateParameter);

                //We evaluate the expression once so that
                //fieldsInExpression is appropriately filled.
                try {
                    expressionObject.Evaluate();
                }
                catch(Exception) {
                }
            }
        }

        /// <summary>
        /// Get and set the read only status for the control.
        /// </summary>
        bool IWorkItemControl.ReadOnly
        {
            get
            {
                return txtValue.ReadOnly;
            }
            set
            {
                txtValue.ReadOnly = value;
            }
        }

        /// <summary>
        /// Get or set the work item the control is contained in.
        /// When setting, we capture the FieldChanged event of the work item.
        /// </summary>
        object IWorkItemControl.WorkItemDatasource
        {
            get
            {
                return workItem;
            }
            set
            {
                if(workItem != null) {
                    workItem.FieldChanged -= OnFieldChanged;
                }

                workItem = value as WorkItem;

                if(workItem == null) {
                    workItemFields = new Field[0];
                }
                else {
                    workItemFields = new Field[workItem.Fields.Count];
                    ((ICollection)workItem.Fields).CopyTo(workItemFields, 0);
                }

                if(workItem != null) {
                    workItem.FieldChanged += OnFieldChanged;
                }
            }
        }

        /// <summary>
        /// Get or set the reference name of the field associated to the control.
        /// </summary>
        public string /*IWorkItemControl.*/WorkItemFieldName
        {
            get;
            set;
        }

        public event EventHandler AfterUpdateDatasource;
        public event EventHandler BeforeUpdateDatasource;

        #endregion

        #region Public methods

        /// <summary>
        /// Clear the control contents.
        /// </summary>
        void IWorkItemControl.Clear()
        {
            txtValue.Text = string.Empty;
        }

        /// <summary>
        /// Apply the new control value to the associated field on the containing work item.
        /// </summary>
        void IWorkItemControl.FlushToDatasource()
        {
            if(workItem != null && WorkItemFieldName != null) {
                var field = workItemFields.Where(f => f.ReferenceName == WorkItemFieldName).SingleOrDefault();
                if(field != null) {
                    if(IsNumericType(field.FieldDefinition.SystemType)) {
                        SetFieldValue(field, calculationValue);
                    }
                    else if(field.FieldDefinition.SystemType == typeof(string)) {
                        SetFieldValue(field, calculationValue.ToString());
                    }
                }
            }
        }

        /// <summary>
        /// Force the calculation of the control value.
        /// </summary>
        void IWorkItemControl.InvalidateDatasource()
        {
            txtValue.Text = GenerateNewTextboxValue();
        }

        void IWorkItemControl.SetSite(IServiceProvider serviceProvider)
        {
        }

        #endregion

        #region Private methods

        /// <summary>
        /// Apply a value to the associated field, firing the appropriate events.
        /// </summary>
        /// <param name="field"></param>
        /// <param name="value"></param>
        private void SetFieldValue(Field field, object value)
        {
            if(BeforeUpdateDatasource != null) {
                BeforeUpdateDatasource(this, new EventArgs());
            }

            field.Value = value;

            if(AfterUpdateDatasource != null) {
                AfterUpdateDatasource(this, new EventArgs());
            }
        }

        /// <summary>
        /// This is invoked whenever the value of 
        /// any of the fields on the associated work item changes.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void OnFieldChanged(object sender, WorkItemEventArgs e)
        {
            //Beware: e.Field can be null!
            if(e.Field != null && fieldsInExpression.Contains(e.Field.Name)) {
                txtValue.Text = GenerateNewTextboxValue();
            }
        }

        /// <summary>
        /// Calculates a new value for the control
        /// based on the configured expression.
        /// </summary>
        /// <returns></returns>
        private string GenerateNewTextboxValue()
        {
            if(workItem == null) {
                return "(null work item)";
            }

            if(expressionObject == null) {
                return "(Expression not defined)";
            }

            object result = null;
            try {
                result = expressionObject.Evaluate();
            }
            catch(Exception ex) {
                return string.Format("(Error in expression: {0})", ex.Message);
            }

            if(result == null) {
                return "(Expression result is null)";
            }

            if(!IsNumericType(result.GetType())) {
                return string.Format("(Expression result type is '{0}', which is not numeric)", result.GetType().Name);
            }

            try {
                calculationValue = (decimal)result;
                return calculationValue.ToString();
            }
            catch(Exception ex) {
                return string.Format("(Error when converting result to number: {0})", ex.Message);
            }
        }

        /// <summary>
        /// This is invoked when the expression evaluator finds
        /// a non-numeric item in the expression.
        /// We assume it is a field name and retrieve its value
        /// from the work item.
        /// </summary>
        /// <param name="name"></param>
        /// <param name="args"></param>
        private void expression_EvaluateParameter(string name, ParameterArgs args)
        {
            var field = workItemFields.Where(f => f.Name == name).SingleOrDefault();

            if(field == null) {
                throw new InvalidOperationException(string.Format("Can't find field named '{0}'", name));
            }

            if(!IsNumericType(field.FieldDefinition.SystemType)) {
                throw new InvalidOperationException(string.Format("Field named '{0}' is not numeric", name));
            }

            if(!fieldsInExpression.Contains(field.Name)) {
                fieldsInExpression.Add(field.Name);
            }

            args.Result = workItem.Fields[name].Value ?? 0;
        }

        private bool IsNumericType(Type type)
        {
            return (type.IsPrimitive && type != typeof(bool) && type != typeof(char)) || type == typeof(decimal);
        }

        #endregion
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer SunHotels
Spain Spain
Under the secret identity of a C# programmer, a freaky guy who loves MSX computers and japanese culture is hidden. Beware!

Comments and Discussions