Click here to Skip to main content
11,576,064 members (58,246 online)
Click here to Skip to main content
Add your own
alternative version

Expression Control for TFS Work Items

, 5 Oct 2010 CPOL 32.2K 1.7K 15
Custom control for TFS Work Items that shows the result of calculating an expression based on the work item fields contents
ExpressionControlForTFSWI.zip
ExpressionControl_installer.zip
Custom Controls Setup.msi
setup.exe
ExpressionControl_Risk.zip
ExpressionControl_source.zip
Custom Controls Setup
Custom Controls Setup.vdproj
Release
Konamiman's TFS custom controls
bin
Debug
ExpressionControl
Release
ExpressionControl
ExpressionControl.wicc
ExpressionEvaluator
Domain
Function.cd
LogicalExpression.cd
Properties
Konamiman's TFS custom controls.csproj.user
Properties
Referenced Assemblies
Evaluant.Antlr.dll
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)

Share

About the Author

konamiman.MSX
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!

You may also be interested in...

| Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.150603.1 | Last Updated 6 Oct 2010
Article Copyright 2010 by konamiman.MSX
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid