Tuesday, April 19, 2016

A simple rules engine

I'm extracting data from some OCR'd letters and, in order to determine which type of letter I'm parsing, I'm using a method similar to this:

        public Letter Parse(string text)
        {
            Letter result;
            if (text.IndexOf("...", StringComparison.OrdinalIgnoreCase) >= 0)
                letter = new LetterA();
            else
                letter = new LetterB();

            //... additional processing
            return letter;
        }

If the letter contains a specific text, I know it's of one type; otherwise I'll default to the other type. Unfortunately that's going to get really complicated, really fast once I start adding new letter types. I read somewhere that "you should move logic out of the code and into the data when possible"; it made sense and I never had a reason to regret it. So, let me try to do that here.

First I'll add a "rules list" class that will allow me to store the various criteria:

    public class RulesList<T, TResult>
    {
        public RulesList()
        {
            rules = new List<Tuple<Predicate<T>, Func<TResult>>>();
        }

        public void Add(Predicate<T> condition, Func<TResult> constructor)
        {
            rules.Add(Tuple.Create(condition, constructor));
        }

        public TResult Get(T criteria)
        {
            return rules
                .Where(rule => rule.Item1(criteria))
                .Select(rule => rule.Item2())
                .FirstOrDefault();
        }

        //

        private readonly List<Tuple<Predicate<T>, Func<TResult>>> rules;
    }

I've made this class more generic by replacing the string type with T; honestly, I don't think there will ever be a need for anything else in this project but… it wasn't a big "expense".

Using this is quite simple at the moment:

    public class LetterSelector
    {
        public LetterSelector()
        {
            rules = new RulesList<string, Letter>();

            rules.Add(s => s.IndexOf("...", StringComparison.OrdinalIgnoreCase) >= 0, () => new LetterA());
            rules.Add(_ => true, () => new LetterB());
        }

        public Letter Parse(string text)
        {
            var letter = rules.Get(text);
            //... additional processing

            return letter;
        }

        //

        private readonly RulesList<string, Letter> rules;
    }

Is this a big gain? Right now it doesn't look like I gained anything; however, bitter experience taught me that methods with many conditionals quickly become an unmaintainable mess (you haven't lived until you've had to fix a method with 700+ lines and a cyclomatic complexity over 200). This will allow me to separate those conditionals into their own lambdas or small private methods in the LetterSelector class.

Tuesday, April 12, 2016

Crystal Reports woes

This took me an hour to figure out so I thought I'd write it down in case it helps anyone else.

If you have a form that's going to display a Crystal Report and you want to zoom it by default, the "normal" way would be to do this in form_Shown:

    private void ReportViewer_Shown(object sender, EventArgs e)
    {
        viewer.Zoom(2); // 1 = page width, 2 = whole page, 25..400 = zoom factor
    }

(Where viewer is the CrystalReportViewer component.)

Unfortunately, it takes CR a while to compute and display the actual report; by the time that happens, the .Zoom() call has already been executed (and ignored).

I have tried a number of workarounds (including launching a thread, waiting for two seconds and then calling the Zoom method - it worked but it was a horrible hack) before I discovered that CR has a "hidden" PageChanged event (it has a [Browsable(false)] attribute). Use that event by assigning a handler in the constructor:

    viewer.PageChanged += Viewer_PageChanged;

and then add the Viewer_PageChanged method:

    private void Viewer_PageChanged(object sender, EventArgs e)
    {
        viewer.Zoom(2);

        viewer.PageChanged -= Viewer_PageChanged;
    }

(Note that, in order to avoid leaking references, I have removed the PageChanged handler immediately after calling the Zoom method.)