4
\$\begingroup\$

Users can define custom controls that are added to an entry form at run-time. The values of these controls are later stored in a MySQL database. Optionally, users can define 'recipes' to auto calculate some controls.

For an example, a user could enter Gross and Tare weight and use a FieldCalc for the Net Weight. In this case the ValueChanged event of both Gross and Tare will call FieldCalcValueChanged below.

Multiple recipes could be triggered by a change to a single field. There is no loop protection/detection.

CalcTotal does the work, processing each line of the recipe, then assigning the outputs. This could cascade other recipes.

In general, the above example is the normal case. In a few cases, there are 3-6 mileage fields, which total to a single field.

The code works great. But slowly. So much so, is some cases, that users choose to disable the FieldCalc recipes, and run a 10-key, which is not acceptable.

Any suggestions for speed or otherwise are appreciated.

configfieldcalc:

id   fieldcalcname                                 code 
---- --------------------------------------------- ---- 
2    NetWeight                                          
6    MilesDifference                                    
5    NetScale                                           
7    MilesCalc                                          

configfieldcalcstep:

id   configfieldcalcid step linetype output                                        operator                 valuetype option                                        notes                                         
---- ----------------- ---- -------- --------------------------------------------- ------------------------ --------- --------------------------------------------- --------------------------------------------- 
2    2                 1    math     weightnet                                     math_assign              metric    weightgross                                                                                 
3    2                 2    math     weightnet                                     math_subtract            metric    weighttare                                                                                  

7    5                 1    if       factor                                        if_not_equal             number    0                                                                                           
8    5                 2    if       weightnet                                     if_not_equal             number    0                                                                                           
9    5                 3    math     scalenet                                      math_assign              metric    weightnet                                                                                   
10   5                 4    math     scalenet                                      math_multiply            number    1000.0000                                                                                   
11   5                 5    math     scalenet                                      math_divide              metric    factor                                                                                      
12   5                 6    round    scalenet                                                                         0                                                                                           
13   5                 7    endif                                                                                                                                                                                 
14   5                 8    endif                                                                                                                                                                                 

19   6                 1    if       miles                                         if_not_equal             number    0                                                                                           
20   6                 2    math     milesdifference                               math_assign              metric    miles                                                                                       
21   6                 3    math     milesdifference                               math_subtract            metric    milesWaTotal                                                                                
22   6                 4    math     milesdifference                               math_subtract            metric    milesWaOffroad                                                                              
23   6                 5    math     milesdifference                               math_subtract            metric    milesOrLoaded                                                                               
24   6                 6    math     milesdifference                               math_subtract            metric    milesOrEmpty                                                                                
25   6                 7    math     milesdifference                               math_subtract            metric    milesOrRUAF                                                                                 
26   6                 8    endif                                                                                                                                                                                 

15   7                 1    if       milesend                                      if_not_equal             number    0                                                                                           
16   7                 2    math     miles                                         math_assign              metric    milesend                                                                                    
17   7                 3    math     miles                                         math_subtract            metric    milesstart                                                                                  
18   7                 4    endif               

code:

using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Windows.Forms;
using LoggingControls;
using LoggingUtility;

namespace TicketEntry
{
public class FieldCalc
{
    private static DataView _configFieldCalcStepView;
    private readonly IEnumerable<ITicketRateMetric> _metricControls;
    private readonly IEnumerable<Time> _timeControls;

    static FieldCalc()
    {
        SetFieldCalcStepView();
    }

    static public void SetFieldCalcStepView()
    {
        _configFieldCalcStepView = new DataView
        {
            Table = LoggingUtility.Data.ConfigFieldCalcStep(),
            Sort = "configfieldcalcid,step"
        };
    }

    public FieldCalc(IEnumerable<ITicketRateMetric> metricControls, IEnumerable<Time> timeControls)
    {
        _metricControls = metricControls;
        _timeControls = timeControls;
    }

    public void ClearEventHandlers()
    {
        Debug.WriteLine("<ClearEventHandlers>");
        Debug.Indent();

        foreach (ITicketRateMetric metric in _metricControls)
        {
            metric.ClearRateMetricValueChanged();
        }

        foreach (var time in _timeControls)
        {
            time.TextChanged -= FieldCalcValueChanged;
        }

        Debug.Unindent();
        Debug.WriteLine("</ClearEventHandlers>");
    }

    public void SetEventHandlers() 
    {
        Debug.WriteLine("<SetEventHandlers>");
        Debug.Indent();

        ClearEventHandlers();

        var stepOptions = from DataRow basisstep in _configFieldCalcStepView.Table.Rows
                          where basisstep.Field<string>("valuetype") == "metric"
                          select new
                          {
                              configfieldcalcid = basisstep.Field<uint>("configfieldcalcid"),
                              option = basisstep.Field<string>("option")
                          };

        foreach (var o in stepOptions)
        {
            try
            {
                var eo = o; //Must capture variable to avoid access to modified closure
                var metric = (from control in _metricControls
                              where control.Name == eo.option
                              select control).First();

                metric.RateMetricValueChanged += FieldCalcValueChanged;
            }
            catch (InvalidOperationException) { /* NOP */ }
        }

        stepOptions = from DataRow basisstep in _configFieldCalcStepView.Table.Rows
                      where basisstep.Field<string>("valuetype") == "time"
                      select new
                      {
                          configfieldcalcid = basisstep.Field<uint>("configfieldcalcid"),
                          option = basisstep.Field<string>("option")
                      };

        foreach (var o in stepOptions)
        {
            try
            {
                var eo = o; //Must capture variable to avoid access to modified closure
                var time = (from control in _timeControls
                            where control.Name == eo.option
                            select control).First();

                time.TextChanged += FieldCalcValueChanged;
            }
            catch (InvalidOperationException) { /* NOP */ }
        }

        Debug.Unindent();
        Debug.WriteLine("</SetEventHandlers>");
    }

    private void FieldCalcValueChanged(object sender, EventArgs e)
    {
        var s = sender as Control;

        if (s != null)
        {
            CalcTotal(s.Parent.Name);
        }
    }

    private void CalcTotal(string fieldTriggered)
    {
        var output = new Dictionary<string, decimal>();

        var fieldCalcId = from DataRow basisstep in _configFieldCalcStepView.Table.Rows
                          where basisstep.Field<string>("option") == fieldTriggered
                          select basisstep.Field<uint>("configfieldcalcid");

        foreach (var f in fieldCalcId)
        {
            _configFieldCalcStepView.RowFilter = string.Format("configfieldcalcid = '{0}'", f);

            var skipIfCount = 0; //Track count of IF types, to find correct matching ENDIF, when there are nested IF types 

            foreach (DataRowView step in _configFieldCalcStepView)
            {
                Debug.WriteLine("LineType = " + step.Row.Field<string>("linetype"));
                BasisUtility.BasisLineType b = BasisUtility.ParseBasisLineType(step.Row.Field<string>("linetype"));

                var o = step.Row.Field<string>("output");
                if (o != string.Empty && output.ContainsKey(o) == false && (b == BasisUtility.BasisLineType.MATH || b == BasisUtility.BasisLineType.ROUND))
                {
                    output.Add(o, decimal.Zero);
                }

                switch (b)
                {
                    default:
                        break;
                    case BasisUtility.BasisLineType.ENDIF:
                        skipIfCount--;
                        break;
                    case BasisUtility.BasisLineType.MATH:
                        if (skipIfCount == 0)
                        {
                            CalcLineTypeMath(step, output);
                        }
                        break;
                    case BasisUtility.BasisLineType.IF:
                        if (skipIfCount == 0)
                        {
                            var ifPassed = CalcLineTypeIf(step, output);

                            if (!ifPassed)
                            {
                                skipIfCount++;
                            }
                        }
                        else
                        {
                            skipIfCount++;
                        }
                        break;
                    case BasisUtility.BasisLineType.ROUND:
                        if (skipIfCount == 0)
                        {
                            CalcLineTypeRound(step, output);
                        }
                        break;
                }

                Debug.WriteLine("skipIfCount = " + skipIfCount);
            }

            foreach (var o in output)
            {
                var eo = o; //Must capture variable to avoid access to modified closure
                var metric = (from control in _metricControls
                              where control.Name == eo.Key
                              select control).First();

                metric.Value = o.Value;
            }

            _configFieldCalcStepView.RowFilter = string.Empty;
        }
    }

    private void CalcLineTypeMath(DataRowView step, IDictionary<string, decimal> output)
    {
        Debug.WriteLine("<CalcLineTypeMath>");
        Debug.Indent();

        var outputVar = step.Row.Field<string>("output");
        BasisUtility.BasisOperator op = BasisUtility.ParseBasisOperator(step.Row.Field<string>("operator"));
        BasisUtility.BasisValueType valueType = BasisUtility.ParseBasisValueType(step.Row.Field<string>("valuetype"));
        var option = step.Row.Field<string>("option");

        Debug.WriteLine("outputVar = " + step.Row.Field<string>("output"));
        Debug.WriteLine("op = " + step.Row.Field<string>("operator"));
        Debug.WriteLine("valueType = " + step.Row.Field<string>("valuetype"));
        Debug.WriteLine("option = " + step.Row.Field<string>("option"));

        decimal input = CalcInput(output, valueType, outputVar, option);

        switch (op)
        {
            case BasisUtility.BasisOperator.MATH_ASSIGN:
                output[outputVar] = input;
                break;
            case BasisUtility.BasisOperator.MATH_ADD:
                output[outputVar] += input;
                break;
            case BasisUtility.BasisOperator.MATH_SUBTRACT:
                output[outputVar] -= input;
                break;
            case BasisUtility.BasisOperator.MATH_MULTIPLY:
                output[outputVar] *= input;
                break;
            case BasisUtility.BasisOperator.MATH_DIVIDE:
                try
                {
                    output[outputVar] /= input;
                }
                catch (DivideByZeroException)
                {
                    output[outputVar] = 1;
                }
                break;
            case BasisUtility.BasisOperator.MATH_POWER_OF:
                output[outputVar] = (decimal)Math.Pow((double)output[outputVar], (double)input);
                break;
            case BasisUtility.BasisOperator.MATH_MODULUS:
                output[outputVar] %= input;
                break;
            default:
                break;
        }

        Debug.WriteLine("output[outputVar] = " + output[outputVar]);

        Debug.Unindent();
        Debug.WriteLine("</CalcLineTypeMath>");
    }

    private decimal CalcInput(IDictionary<string, decimal> output, BasisUtility.BasisValueType valueType, string outputVar, string option)
    {
        Debug.WriteLine("<CalcInput>");
        Debug.Indent();

        var input = decimal.Zero;
        decimal md;

        Debug.WriteLine("valueType = " + valueType);

        switch (valueType)
        {
            default:
                break;
            case BasisUtility.BasisValueType.ENTITY:
                //TODO add lookup for ENTITY:
                input = -2;
                break;
            case BasisUtility.BasisValueType.METRIC:
                var o = (from m in _metricControls
                         where m.Name == option
                         select m.Value.ToString()).First();
                decimal.TryParse(o, out md);
                input = md;
                break;
            case BasisUtility.BasisValueType.TIME:
                var t = (from m in _timeControls
                         where m.Name == option
                         select m.Value).First();

                decimal.TryParse(t.TotalHours.ToString(), out md);
                input = md;
                break;
            case BasisUtility.BasisValueType.INTERNAL:
                input = (option.Equals("DEFAULT")) ? decimal.Zero : output[outputVar];
                break;
            case BasisUtility.BasisValueType.NUMBER:
                decimal d;
                decimal.TryParse(option, out d);
                input = d;
                break;
        }

        Debug.WriteLine("input = " + input);

        Debug.Unindent();
        Debug.WriteLine("</CalcInput>");

        return input;
    }

    private bool CalcLineTypeIf(DataRowView step, IDictionary<string, decimal> output)
    {
        Debug.WriteLine("<CalcLineTypeIf>");
        Debug.Indent();

        var outputVar = step.Row.Field<string>("output");
        BasisUtility.BasisOperator op = BasisUtility.ParseBasisOperator(step.Row.Field<string>("operator"));
        BasisUtility.BasisValueType valueType = BasisUtility.ParseBasisValueType(step.Row.Field<string>("valuetype"));
        var option = step.Row.Field<string>("option");

        Debug.WriteLine("outputVar = " + step.Row.Field<string>("output"));
        Debug.WriteLine("op = " + step.Row.Field<string>("operator"));
        Debug.WriteLine("valueType = " + step.Row.Field<string>("valuetype"));
        Debug.WriteLine("option = " + step.Row.Field<string>("option"));

        var input = CalcInput(output, valueType, outputVar, option);

        var outvalue = (output.Keys.Contains(outputVar)) ? output[outputVar] : CalcInput(output, BasisUtility.BasisValueType.METRIC, string.Empty, outputVar);

        var result = false;

        switch (op)
        {
            case BasisUtility.BasisOperator.IF_EQUAL:
                result = (outvalue == input);
                break;
            case BasisUtility.BasisOperator.IF_NOT_EQUAL:
                result = (outvalue != input);
                break;
            case BasisUtility.BasisOperator.IF_LESS_THAN:
                result = (outvalue < input);
                break;
            case BasisUtility.BasisOperator.IF_LESS_THAN_OR_EQUAL:
                result = (outvalue <= input);
                break;
            case BasisUtility.BasisOperator.IF_GREATER_THAN:
                result = (outvalue > input);
                break;
            case BasisUtility.BasisOperator.IF_GREATER_THAN_OR_EQUAL:
                result = (outvalue >= input);
                break;
            default:
                break;
        }

        Debug.WriteLine("result = " + result);

        Debug.Unindent();
        Debug.WriteLine("</CalcLineTypeIf>");

        return result;
    }

    private void CalcLineTypeRound(DataRowView step, IDictionary<string, decimal> output)
    {
        Debug.WriteLine("<CalcLineTypeRound>");
        Debug.Indent();

        var outputVar = step.Row.Field<string>("output");
        int decimals;
        int.TryParse(step.Row.Field<string>("option"), out decimals);

        output[outputVar] = Math.Round(output[outputVar], decimals, MidpointRounding.AwayFromZero);

        Debug.WriteLine("output[outputVar] = " + output[outputVar]);

        Debug.Unindent();
        Debug.WriteLine("</CalcLineTypeRound>");
    }
}
}
\$\endgroup\$
2
  • \$\begingroup\$ any idea where the bottleneck might be? \$\endgroup\$
    – dreza
    Oct 30 '12 at 19:00
  • \$\begingroup\$ The foreach (DataRowView step in _configFieldCalcStepView) loop is called every time a digit is entered on the triggered control. So I guess the question is how I can improve the logic/speed of this section. I could move the event to validate? lostfocus? I'm not sure if that would be a better place. And truthfully, some users would not be happy with the 'delay' in updating the fields, see milesdifference in the above step list. \$\endgroup\$
    – SMB
    Oct 30 '12 at 19:04
2
\$\begingroup\$

Hook up a profiler and measure what exactly is causing the slow down. Everything else is basically stabbing in the dark. That being said, there are a few things you could check:

  1. You have a few loops in there checking _metricControls and _timeControls frequently for one with a specific name. Store them in a dictionary keyed of the name rather than as the plain IEnumerables:

    public FieldCalc(IEnumerable<ITicketRateMetric> metricControls, IEnumerable<Time> timeControls)
    {
        _metricControls = metricControls.ToDictionary(c => c.Name, c => c);
        _timeControls = timeControls.ToDictionary(c => c.Name, c => c);
    }
    
  2. You do a fair amount of parsing so you could look at caching it. Caching can speed up things but comes with it's own draw backs so I'd be checking first if it's a problem or not.

The first option is cheap and will reduce some of the code complexity (_metricControls[op.Name] as opposed to a multi-line LINQ statement) so I'd do it anyway.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.