6
\$\begingroup\$

I've created a small User Control library in WPF that contains a few GUI widgets that allow users to enter regular expressions. The widgets provide visual feedback when the user has entered an invalid regular expression, and support all the RegexOptions that are provided by the .NET regular expressions library.

Here is the view-model for all the regular-expression-input widgets:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;

namespace NewellClark.Wpf.UserControls.ViewModels
{
    internal sealed class RegexViewModel : INotifyPropertyChanged
    {
        public RegexViewModel()
        {
            _flagBools = new List<FlagBool>();
            _dirtyProperties = new HashSet<string>();

            _ignoreCase = CreateFlag(RegexOptions.IgnoreCase);
            _multiline = CreateFlag(RegexOptions.Multiline);
            _explicitCapture = CreateFlag(RegexOptions.ExplicitCapture);
            _compiled = CreateFlag(RegexOptions.Compiled);
            _singleline = CreateFlag(RegexOptions.Singleline);
            _ignorePatternWhiteSpace = CreateFlag(RegexOptions.IgnorePatternWhitespace);
            _rightToLeft = CreateFlag(RegexOptions.RightToLeft);
            _ecmaScript = CreateFlag(RegexOptions.ECMAScript);
            _cultureInvariant = CreateFlag(RegexOptions.CultureInvariant);
        }

        public Regex Regex
        {
            get { return _regex; }
            set { UpdateRegex(value); }
        }
        private Regex _regex;

        public string Pattern
        {
            get { return _pattern; }
            set { SetField(ref _pattern, value); }
        }
        private string _pattern;

        public bool IsValid
        {
            get { return _isValid; }
            private set { SetField(ref _isValid, value); }
        }
        private bool _isValid;

        public RegexError Error
        {
            get { return _error; }
            set { SetField(ref _error, value); }
        }
        private RegexError _error;

        public RegexOptions Options
        {
            get { return _options; }
            set
            {
                foreach (FlagBool flag in _flagBools)
                {
                    RegexOptions next = flag.ValueWhenEnabled & value;
                    RegexOptions current = flag.ValueWhenEnabled & _options;
                    if (next != current)
                        SetDirty(flag.Name);
                }

                SetField(ref _options, value);
            }
        }
        private RegexOptions _options;

        public bool IgnoreCase
        {
            get { return _ignoreCase.Enabled; }
            set { _ignoreCase.Enabled = value; }
        }
        private FlagBool _ignoreCase;

        public bool Multiline
        {
            get { return _multiline.Enabled; }
            set { _multiline.Enabled = value; }
        }
        private FlagBool _multiline;

        public bool ExplicitCapture
        {
            get { return _explicitCapture.Enabled; }
            set { _explicitCapture.Enabled = value; }
        }
        private FlagBool _explicitCapture;

        public bool Compiled
        {
            get { return _compiled.Enabled; }
            set { _compiled.Enabled = value; }
        }
        private FlagBool _compiled;

        public bool Singleline
        {
            get { return _singleline.Enabled; }
            set { _singleline.Enabled = value; }
        }
        private FlagBool _singleline;

        public bool IgnorePatternWhitespace
        {
            get { return _ignorePatternWhiteSpace.Enabled; }
            set { _ignorePatternWhiteSpace.Enabled = value; }
        }
        private FlagBool _ignorePatternWhiteSpace;

        public bool RightToLeft
        {
            get { return _rightToLeft.Enabled; }
            set { _rightToLeft.Enabled = value; }
        }
        private FlagBool _rightToLeft;

        public bool ECMAScript
        {
            get { return _ecmaScript.Enabled; }
            set { _ecmaScript.Enabled = value; }
        }
        private FlagBool _ecmaScript;

        public bool CultureInvariant
        {
            get { return _cultureInvariant.Enabled; }
            set { _cultureInvariant.Enabled = value; }
        }
        private FlagBool _cultureInvariant;

        public event PropertyChangedEventHandler PropertyChanged;

        //  Because many of the properties depend on each other, we have to update them all at once.
        //  I don't want any event subscribers to see the object in an invalid state, so I hold off on firing any of the 
        //  PropertyChanged events until all properties have been updated. 
        private void UpdateProperties()
        {
            _isUpdating = true;

            SetRegexValueSwallowExceptions(_pattern, _options);
            IsValid = Regex != null;
            RaiseEventsOnDirtyProperties();

            _isUpdating = false;
        }

        /// <summary>
        /// Same as <c>UpdateProperties</c>, but called when the <c>Regex</c> property was set.
        /// </summary>
        /// <param name="regex"></param>
        private void UpdateRegex(Regex regex)
        {
            _isUpdating = true;

            if (!SetField(ref _regex, regex, nameof(Regex)))
            {
                _isUpdating = true;
                return;
            }

            if (_regex != null)
            {
                Options = _regex.Options;
                Pattern = _regex.ToString();
                IsValid = _regex != null;
            }

            RaiseEventsOnDirtyProperties();

            _isUpdating = false;
        }

        private bool SetField<T>(ref T field, T value, [CallerMemberName]string name = "")
        {
            //return Common.SetField(PropertyChanged, ref field, value, name);
            if (EqualityComparer<T>.Default.Equals(field, value))
                return false;
            field = value;
            SetDirty(name);
            if (!_isUpdating)
                UpdateProperties();
            return true;
        }

        private void SetDirty(string propertyName)
        {
            _dirtyProperties.Add(propertyName);
        }

        private void RaiseEventsOnDirtyProperties()
        {
            foreach (string dirtyName in _dirtyProperties)
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(dirtyName));

            _dirtyProperties.Clear();
        }

        private void SetRegexValueSwallowExceptions(string pattern, RegexOptions options)
        {
            Regex result;
            RegexError error = RegexError.None;
            try
            {
                result = new Regex(pattern, options);
            }
            catch (ArgumentNullException)
            {
                result = null;
                error = RegexError.NullPattern;
            }
            catch (ArgumentOutOfRangeException)
            {
                result = null;
                error = RegexError.InvalidOptions;
            }
            catch (ArgumentException)
            {
                result = null;
                error = RegexError.InvalidPattern;
            }
            Error = error;
            Regex = result;
        }

        private FlagBool CreateFlag(RegexOptions valueWhenEnabled)
        {
            FlagBool result = new FlagBool(this, valueWhenEnabled);
            _flagBools.Add(result);

            return result;
        }

        /// <summary>
        /// Sets the bit-flag that was assigned to it on <c>RegexViewModel.Options</c> when its <c>Enabled</c> property is set to true. 
        /// Also raises a PropertyChanged event on the containing <c>RegexViewModel</c> object when its <c>Enabled</c> property changes.
        /// </summary>
        private class FlagBool
        {
            public FlagBool(RegexViewModel outer, RegexOptions valueWhenEnabled)
            {
                _outer = outer;
                ValueWhenEnabled = valueWhenEnabled;
                Name = Enum.GetName(typeof(RegexOptions), valueWhenEnabled);
            }

            public RegexOptions ValueWhenEnabled { get; }

            public RegexOptions CurrentValue
            {
                get { return _outer.Options & ValueWhenEnabled; }
            }

            public bool Enabled
            {
                get { return (_outer.Options & CurrentValue) == ValueWhenEnabled; }
                set
                {
                    if (value)
                    {
                        _outer.Options |= ValueWhenEnabled;
                        return;
                    }
                    _outer.Options &= ~ValueWhenEnabled;
                }
            }

            public string Name { get; }

            private RegexViewModel _outer;
        }

        private List<FlagBool> _flagBools;
        private HashSet<string> _dirtyProperties;
        private bool _isUpdating = false;
    }

    internal enum RegexError
    {
        None = 0,
        NullPattern = 1,
        InvalidPattern = 2,
        InvalidOptions = 3
    }
}

The view-model does work, and I already have a couple regex-input-widgets that use it and they work fine. However, I feel like the view-model is somewhat ugly in some areas. In particular, the code that raises PropertyChanged events is getting pretty tangled. The problem is that most of the properties on this object are dependent on other properties, so to raise all the correct property-changed events, any time I change one of the properties, I have to update all the properties.

This happens in the UpdateProperties() method, unless it was the Regex property that was initially mutated, in which case the UpdateRegex() method is called instead. This was necessary because usually when all the properties are updated, a new regular expression object is created based on the values of the other properties. However, when it was the Regex property that was mutated, we need to preserve its value and set the values of the other properties based on the properties of the regular expression that we were handed.

I feel like the code that implements INotifyPropertyChanged is starting to smell, and is going to be a source of fragility if I ever need to add any properties to this class. I feel like I'm missing something when it comes to implementing INotifyPropertyChanged. Any critiques would be appreciated.

\$\endgroup\$
1
\$\begingroup\$

In particular, the code that raises PropertyChanged events is getting pretty tangled. The problem is that most of the properties on this object are dependent on other properties, so to raise all the correct property-changed events, any time I change one of the properties, I have to update all the properties.

There are only a bunch of properties. I think you can update them all every time. You don't need to track changes or send notifications by name for all properties because

An Empty value or null for the propertyName parameter indicates that all of the properties have changed.

PropertyChangedEventArgs Constructor (String)


If you however want to keep the change tracking then you should encapsulate it in another class. You could reuse it for other models and it wouldn't polute the API with unnecessary methods or fields, even if only private - they don't belong to the model.

You may have the idea to derive the model from a change-tracked-model but I don't think this is such a good idea as in WPF you'll need to often derive the view-model from the DependencyObject.


Speaking of the DependencyObject. Have you considered actually deriving the RegexViewModel from it and creating DependencyPropertyies? It would take care of many things you currently do like sending notifications or validating the input.

The entire

private void SetRegexValueSwallowExceptions(string pattern, RegexOptions options)

wouldn't be necessary as you could specify the validation rules via the DependencyProperty.Register method. They would be better encapsulated and directly related to the property they validate rather then being a part of a large method responsible for validationg several properties.

\$\endgroup\$
  • \$\begingroup\$ I'm a bit hesitant to derive from DependencyObject because that would couple RegexViewModel to WPF. I like the idea of encapsulating the change-tracking into a separate class that I could reuse in other view-models. +1 \$\endgroup\$ – Newell Clark Jan 17 '17 at 19:38
  • 1
    \$\begingroup\$ @NewellClark are you going to reuse the RegexViewModel for other projects? I don't think so. It's WPF and the DependencyObject supports WPF and gives you the tools to do exactly for what you are doing now but in a WPF way - this is with dependency properties. WPF is all about dependency objects and properties. I find there is nothing wrong in using it but the choice is yours ;-) maybe this question on SO will help you make the decision with good arguments for one or the other approach. \$\endgroup\$ – t3chb0t Jan 17 '17 at 19:46
  • 1
    \$\begingroup\$ actually yes, I likely will port this view-model to other GUI frameworks. Thanks for the stackoverflow link, by the way. That was very helpful. One of the things that was mentioned in that thread is all the restrictions that are on DependencyObjects. I did not realize, for example, that DependencyObject seals Equals() and GetHashCode(). Also, the thread-afinity could be annoying in the future. I will be taking your first piece of advice and moving the change-tracking logic into its own class. \$\endgroup\$ – Newell Clark Jan 17 '17 at 20:30
  • \$\begingroup\$ I've implemented your suggestion (encapsulating property-tracking in a separate class), so I've accepted this answer. \$\endgroup\$ – Newell Clark Feb 10 '17 at 18:47

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.