Keeping it regular

Last week I posted an example of using a regular expression to control the input of numbers into a TextBox. A couple of my fellow disciples commented that they’d just use a regular expression Behavior or DP to control the input of the text. Now, there are a couple of reasons that I’d use the dedicated NumericTextBoxBehavior.

The first reason is that it’s a simple control for those that aren’t comfortable writing regular expressions. After all, why should you write a complex regular expression when I can write one for you?

The second reason is that the numeric control is internationalised from the get-go. I’ve already take care of sorting out the whole internationalised decimal place issue, so you don’t have to worry about it with your regular expression.

Saying that, the regular expression behavior is a cracking idea, and one I could kick myself for not thinking of earlier. So, in order to add regular expression functionality in your TextBox, all you need do is add the following code:

namespace Goldlight.Base.Behaviors
{
  using System.Linq;
  using System.Windows.Controls;
  using System.Windows.Interactivity;
  using System.Windows;
  using System.Windows.Input;
  using System.Text.RegularExpressions;

  /// <summary>
  /// Apply this behavior to a TextBox to ensure that input matches a regular
  /// expression.
  /// <para>
  /// <remarks>
  /// In the view, this behavior is attached in the following way:
  /// <code>
  /// <TextBox Text="{Binding Price}">
  ///   <i:Interaction.Behaviors>
  ///   <gl:RegularExpressionTextBoxBehavior 
  ///    Mask="^([\(]{1}[0-9]{3}[\)]{1}[ ]{1}[0-9]{3}[\-]{1}[0-9]{4})$" />
  ///   </i:Interaction.Behaviors>
  /// </TextBox>
  /// </code>
  /// <para>
  /// Add references to System.Windows.Interactivity to the view to use
  /// this behavior.
  /// </para>
  /// </remarks>
  public class RegularExpressionTextBoxBehavior : Behavior<TextBox>
  {
    /// <summary>
    /// Gets or sets the regular expression mask.
    /// </summary>
    public string Mask { get; set; }

    #region Overrides
    protected override void OnAttached()
    {
      base.OnAttached();

      AssociatedObject.PreviewTextInput += AssociatedObject_PreviewTextInput;
#if !SILVERLIGHT
      DataObject.AddPastingHandler(AssociatedObject, OnClipboardPaste);
#endif
    }

    protected override void OnDetaching()
    {
      base.OnDetaching();
      AssociatedObject.PreviewTextInput -= AssociatedObject_PreviewTextInput;
#if !SILVERLIGHT
      DataObject.RemovePastingHandler(AssociatedObject, OnClipboardPaste);
#endif
    }
    #endregion

#if !SILVERLIGHT
    /// <summary>
    /// Handle paste operations into the textbox to ensure that the behavior
    /// is consistent with directly typing into the TextBox.
    /// </summary>
    /// <param name="sender">The TextBox sender.</param>
    /// <param name="dopea">Paste event arguments.</param>
    /// <remarks>This operation is only available in WPF.</remarks>
    private void OnClipboardPaste(object sender, DataObjectPastingEventArgs dopea)
    {
      string text = dopea.SourceDataObject.GetData(dopea.FormatToApply).ToString();

      if (!string.IsNullOrWhiteSpace(text) && !Validate(text))
        dopea.CancelCommand();
    }
#endif

    /// <summary>
    /// Preview the text input.
    /// </summary>
    /// <param name="sender">The TextBox sender.</param>
    /// <param name="e">The composition event arguments.</param>
    void AssociatedObject_PreviewTextInput(object sender, TextCompositionEventArgs e)
    {
      e.Handled = !Validate(e.Text);
    }

    /// <summary>
    /// Validate the contents of the textbox with the new content to see if it is
    /// valid.
    /// </summary>
    /// <param name="value">The text to validate.</param>
    /// <returns>True if this is valid, false otherwise.</returns>
    protected bool Validate(string value)
    {
      TextBox textBox = AssociatedObject;

      string pre = string.Empty;
      string post = string.Empty;

      if (!string.IsNullOrWhiteSpace(textBox.Text))
      {
        pre = textBox.Text.Substring(0, textBox.SelectionStart);
        post = textBox.Text.Substring(textBox.SelectionStart + textBox.SelectionLength, 
          textBox.Text.Length - (textBox.SelectionStart + textBox.SelectionLength));
      }
      else
      {
        pre = textBox.Text.Substring(0, textBox.CaretIndex);
        post = textBox.Text.Substring(textBox.CaretIndex, 
          textBox.Text.Length - textBox.CaretIndex);
      }
      string test = string.Concat(pre, value, post);

      string pattern = Mask;

      if (string.IsNullOrWhiteSpace(pattern))
        return true;

      return new Regex(pattern).IsMatch(test);
    }
  }
}

As you can see, it’s similar in code to the other behaviour. The only real difference in it is that it has a Mask string which is used to add the regular expression text.

Advertisements

Getting control of your numbers

[Edit]Dominik posed a problem whereby this didn’t work where the update source was set to PropertyChanged and I promised I would get around to fixing this post. Today I finally had the time to sit down and figure out what was wrong, so here is the edited article which contains the fix for Dominik[/Edit]

Today on Code Project, one of the regulars asked how to set up a textbox so that it only accepted a currency amount. He was concerned that there doesn’t seem to be a simple mechanism to limit the input of data so that it only accepted the relevant numeric amount. Well, this is a feature I recently added into Goldlight, so I thought I’d post it here, along with an explanation of how it works.

Basically, and this will come as no surprise to you, it’s an Attached Behavior that you associate to the TextBox. There are many numeric only behaviors out there, so this one goes a little bit further. First of all, if you want, you can limit it to integers by setting AllowDecimal to false. If you want to limit it to a set number of decimal places, set DecimalLimit to the number of decimal places. If you don’t want to allow the developer to use negative numbers, set AllowNegatives to false. It’s that simple, so the solution to the problem would be to add the behaviour to the TextBox like this:

<TextBox Text="{Binding Price}">
  <i:Interaction.Behaviors>
    <gl:NumericTextBoxBehavior AllowNegatives="False" />
  </i:Interaction.Behaviors>
</TextBox>

The full code to do this is shown below:


namespace Goldlight.Extensions.Behaviors
{
  using System.Windows.Controls;
  using System.Windows.Interactivity;
  using System.Windows.Input;
  using System.Text.RegularExpressions;
  using System.Windows;
  using System.Globalization;

  /// <summary>
  /// Apply this behavior to a TextBox to ensure that it only accepts numeric values.
  /// The property <see cref="NumericTextBoxBehavior.AllowDecimal"/> controls whether or not
  /// the input is an integer or not.
  /// <para>
  /// A common requirement is to constrain the number count that appears after the decimal place.
  /// Setting <see cref="NumericTextBoxBehavior.DecimalLimit"/> specifies how many numbers appear here.
  /// If this value is 0, no limit is applied.
  /// </para>
  /// </summary>
  /// <remarks>
  /// In the view, this behavior is attached in the following way:
  /// <code>
  /// <TextBox Text="{Binding Price}">
  ///   <i:Interaction.Behaviors>
  ///     <gl:NumericTextBoxBehavior AllowDecimal="False" />
  ///   </i:Interaction.Behaviors>
  /// </TextBox>
  /// </code>
  /// <para>
  /// Add references to System.Windows.Interactivity to the view to use
  /// this behavior.
  /// </para>
  /// </remarks>
  public partial class NumericTextBoxBehavior : Behavior<TextBox>
  {
    private bool _allowDecimal = true;
    private int _decimalLimit = 0;
    private bool _allowNegative = true;
    private string _pattern = string.Empty;

    /// <summary>
    /// Initialize a new instance of <see cref="NumericTextBoxBehavior"/>.
    /// </summary>
    public NumericTextBoxBehavior()
    {
      AllowDecimal = true;
      AllowNegatives = true;
      DecimalLimit = 0;
    }

    /// <summary>
    /// Get or set whether the input allows decimal characters.
    /// </summary>
    public bool AllowDecimal
    {
      get
      {
        return _allowDecimal;
      }
      set
      {
        if (_allowDecimal == value) return;
        _allowDecimal = value;
        SetText();
      }
    }
    /// <summary>
    /// Get or set the maximum number of values to appear after
    /// the decimal.
    /// </summary>
    /// <remarks>
    /// If DecimalLimit is 0, then no limit is applied.
    /// </remarks>
    public int DecimalLimit
    {
      get
      {
        return _decimalLimit;
      }
      set
      {
        if (_decimalLimit == value) return;
        _decimalLimit = value;
        SetText();
      }
    }
    /// <summary>
    /// Get or set whether negative numbers are allowed.
    /// </summary>
    public bool AllowNegatives
    {
      get
      {
        return _allowNegative;
      }
      set
      {
        if (_allowNegative == value) return;
        _allowNegative = value;
        SetText();
      }
    }

    #region Overrides
    protected override void OnAttached()
    {
      base.OnAttached();

      AssociatedObject.PreviewTextInput += new TextCompositionEventHandler(AssociatedObject_PreviewTextInput);
#if !SILVERLIGHT
      DataObject.AddPastingHandler(AssociatedObject, OnClipboardPaste);
#endif
    }

    protected override void OnDetaching()
    {
      base.OnDetaching();
      AssociatedObject.PreviewTextInput -= new TextCompositionEventHandler(AssociatedObject_PreviewTextInput);
#if !SILVERLIGHT
      DataObject.RemovePastingHandler(AssociatedObject, OnClipboardPaste);
#endif
    }
    #endregion

    #region Private methods
    private void SetText()
    {
      _pattern = string.Empty;
      GetRegularExpressionText();
    }

#if !SILVERLIGHT
    /// <summary>
    /// Handle paste operations into the textbox to ensure that the behavior
    /// is consistent with directly typing into the TextBox.
    /// </summary>
    /// <param name="sender">The TextBox sender.</param>
    /// <param name="dopea">Paste event arguments.</param>
    /// <remarks>This operation is only available in WPF.</remarks>
    private void OnClipboardPaste(object sender, DataObjectPastingEventArgs dopea)
    {
      string text = dopea.SourceDataObject.GetData(dopea.FormatToApply).ToString();

      if (!string.IsNullOrWhiteSpace(text) && !Validate(text))
        dopea.CancelCommand();
    }
#endif

    /// <summary>
    /// Preview the text input.
    /// </summary>
    /// <param name="sender">The TextBox sender.</param>
    /// <param name="e">The composition event arguments.</param>
    void AssociatedObject_PreviewTextInput(object sender, TextCompositionEventArgs e)
    {
      e.Handled = !Validate(e.Text);
    }

    /// <summary>
    /// Validate the contents of the textbox with the new content to see if it is
    /// valid.
    /// </summary>
    /// <param name="value">The text to validate.</param>
    /// <returns>True if this is valid, false otherwise.</returns>
    protected bool Validate(string value)
    {
      TextBox textBox = AssociatedObject;

      string pre = string.Empty;
      string post = string.Empty;

      if (!string.IsNullOrWhiteSpace(textBox.Text))
      {
        int selStart = textBox.SelectionStart;
        if (selStart > textBox.Text.Length)
            selStart--;
        pre = textBox.Text.Substring(0, selStart);
        post = textBox.Text.Substring(selStart + textBox.SelectionLength, textBox.Text.Length - (selStart + textBox.SelectionLength));
      }
      else
      {
        pre = textBox.Text.Substring(0, textBox.CaretIndex);
        post = textBox.Text.Substring(textBox.CaretIndex, textBox.Text.Length - textBox.CaretIndex);
      }
      string test = string.Concat(pre, value, post);

      string pattern = GetRegularExpressionText();

      return new Regex(pattern).IsMatch(test);
    }

    private string GetRegularExpressionText()
    {
      if (!string.IsNullOrWhiteSpace(_pattern))
      {
        return _pattern;
      }
      _pattern = GetPatternText();
      return _pattern;
    }

    private string GetPatternText()
    {
      string pattern = string.Empty;
      string signPattern = "[{0}+]";

      // If the developer has chosen to allow negative numbers, the pattern will be [-+].
      // If the developer chooses not to allow negatives, the pattern is [+].
      if (AllowNegatives)
      {
        signPattern = string.Format(signPattern, "-");
      }
      else
      {
        signPattern = string.Format(signPattern, string.Empty);
      }

      // If the developer doesn't allow decimals, return the pattern.
      if (!AllowDecimal)
      {
        return string.Format(@"^({0}?)(\d*)$", signPattern);
      }

      // If the developer has chosen to apply a decimal limit, the pattern matches
      // on a
      if (DecimalLimit > 0)
      {
        pattern = string.Format(@"^({2}?)(\d*)([{0}]?)(\d{{0,{1}}})$",
          NumberFormatInfo.CurrentInfo.CurrencyDecimalSeparator,
          DecimalLimit,
          signPattern);
      }
      else
      {
        pattern = string.Format(@"^({1}?)(\d*)([{0}]?)(\d*)$", NumberFormatInfo.CurrentInfo.CurrencyDecimalSeparator, signPattern);
      }

      return pattern;
    }
    #endregion
  }
}

The clever thing is that this behavior doesn’t allow the user to paste an incorrect value in either – the paste operation is subject to the same rules as directly entering the value in the first place.

Anyway, I hope this behavior is as much use to you as it is to me.

Scratching that old itch.

First of all, I must apologise that it’s been so long since I last blogged. It’s been an insanely busy time for me (and I don’t mean that I’ve been coding with my underpants on my head). As you may be aware, I’m a big fan of Blend behaviours, so I thought that I’d take the time to revisit an old favourite of mine. To that end, I present all the code you’ll need to create a watermarked textbox.

namespace Goldlight.Extensions.Behaviors
{
  using System.Windows.Interactivity;
  using System.Windows.Controls;
  using System.Windows.Media;
  using System.Windows;

  public class WatermarkTextBoxBehavior : Behavior<TextBox>
  {
    protected override void OnAttached()
    {
      base.OnAttached();
      AssociatedObject.LostFocus += new RoutedEventHandler(LostFocus);
      AssociatedObject.GotFocus += new RoutedEventHandler(GotFocus);
      SetWatermark();
    }

    protected override void OnDetaching()
    {
      base.OnDetaching();
      AssociatedObject.LostFocus -= new RoutedEventHandler(LostFocus);
      AssociatedObject.GotFocus -= new RoutedEventHandler(GotFocus);
    }
    /// <summary>
    /// Get or set the brush to use as the foreground.
    /// </summary>
    public Brush WatermarkForeground { get; set; }
    /// <summary>
    /// Get or set the brush to use as the background.
    /// </summary>
    public Brush WatermarkBackground { get; set; }
    /// <summary>
    /// Get or set the text to apply as the watermark.
    /// </summary>
    public string WatermarkText { get; set; }

    /// <summary>
    /// Reset the colours of the textbox.
    /// </summary>
    private void SetStandard()
    {
      AssociatedObject.ClearValue(TextBox.ForegroundProperty);
      AssociatedObject.ClearValue(TextBox.BackgroundProperty);

      if (AssociatedObject.Text == WatermarkText)
      {
        AssociatedObject.Text = string.Empty;
      }
    }
    /// <summary>
    /// Set the watermark colours for the textbox.
    /// </summary>
    private void SetWatermark()
    {
      if (WatermarkForeground != null)
      {
        AssociatedObject.Foreground = WatermarkForeground;
      }
      if (WatermarkBackground != null)
      {
        AssociatedObject.Background = WatermarkBackground;
      }
      AssociatedObject.Text = WatermarkText;
    }
    void GotFocus(object sender, RoutedEventArgs e)
    {
      SetStandard();
    }
    void LostFocus(object sender, RoutedEventArgs e)
    {
      CheckText(AssociatedObject.Text);
    }
    private void CheckText(string value)
    {
      if (string.IsNullOrWhiteSpace(value))
      {
        SetWatermark();
      }
      else
      {
        SetStandard();
      }
    }
  }
}

The code is pretty straightforward. When the textbox receives focus, if it contains just the Watermark text, the watermark text is cleared out and the original foreground and background brushes are restored. When the textbox loses focus, if it’s empty the watermark text is displayed and the watermark fore and background brushes are set. Now, for the clever bit, because we’re updating the textbox text directly we aren’t going to be updating any underlying binding

Undoing MVVM

I apologise that it’s been a while since I last blogged, but I’ve been busy working on a MVVM framework and it’s been eating up a lot of time – it’s good eat, but it is time consuming. One of the things I’ve been adding into the code is the ability to handle undo/redo functionality in a ViewModel; and more importantly, coordinating undo/redo across multiple View Models. In this blog post, I’d like to demonstrate how easy it is to add this functionality to properties that support change notification. In a future blog, I’ll be demonstrating how to extend this to supporting ObservableCollections as well.

The first thing that we’re going to do is define a simple undo/redo interface. Here it is, in all it’s glory:

using System;
namespace UndoRedoSample
{
    /// <summary>
    /// The interface describing the Undo/Redo operation.
    /// </summary>
    public interface IUndoRedo
    {
        /// <summary>
        /// The optional name for the Undo/Redo property.
        /// </summary>
        string Name { get; }
        /// <summary>
        /// Code to perform the Undo operation.
        /// </summary>
        void Undo();
        /// <summary>
        /// Code to perform the Redo operation.
        /// </summary>
        void Redo();
    }
}

Now, we need to create a class that implements this interface.

using System;
namespace UndoRedoSample
{
    /// <summary>
    /// This class encapsulates a single undoable property.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class UndoableProperty<T> : IUndoRedo
    {
        #region Member
        private object _oldValue;
        private object _newValue;
        private string _property;
        private T _instance;
        #endregion

        /// <summary>
        /// Initialize a new instance of <see cref="UndoableProperty"/>.
        /// </summary>
        /// <param name="property">The name of the property.</param>
        /// <param name="instance">The instance of the property.</param>
        /// <param name="oldValue">The pre-change property.</param>
        /// <param name="newValue">The post-change property.</param>
        public UndoableProperty(string property, T instance, object oldValue, object newValue)
            : this(property, instance, oldValue, newValue, property)
        {
        }

        /// <summary>
        /// Initialize a new instance of <see cref="UndoableProperty"/>.
        /// </summary>
        /// <param name="property">The name of the property.</param>
        /// <param name="instance">The instance of the property.</param>
        /// <param name="oldValue">The pre-change property.</param>
        /// <param name="newValue">The post-change property.</param>
        /// <param name="name">The name of the undo operation.</param>
        public UndoableProperty(string property, T instance, object oldValue, object newValue, string name)
            : base()
        {
            _instance = instance;
            _property = property;
            _oldValue = oldValue;
            _newValue = newValue;

            Name = name;

            // Notify the calling application that this should be added to the undo list.
            UndoManager.Add(this);
        }

        /// <summary>
        /// The property name.
        /// </summary>
        public string Name { get; private set; }

        /// <summary>
        /// Undo the property change.
        /// </summary>
        public void Undo()
        {
            _instance.GetType().GetProperty(_property).SetValue(_instance, _oldValue, null);
        }

        public void Redo()
        {
            _instance.GetType().GetProperty(_property).SetValue(_instance, _newValue, null);
        }
    }
}

This class simply wraps a property. Whenever Undo is called, reflection is used to set the property back to it’s prechanged value. Calling Redo reverses this change. Now, that’s all well and good, but we need to keep track of these changes and apply them – and more importantly, we need to apply them across ViewModels. This is where the UndoManager comes in:

using System;
using System.Collections.Generic;
using System.Linq;

namespace UndoRedoSample
{
    /// <summary>
    /// This class is responsible for coordinating the undo/redo messages from the various view models
    /// in the application. By having a central repository, the undo/redo state is managed without the
    /// need for the VMs having to subscribe to any complex hierarchy.
    /// </summary>
    public static class UndoManager
    {
        #region Members
        private static RangeObservableCollection<IUndoRedo> _undoList;
        private static RangeObservableCollection<IUndoRedo> _redoList;
        private static int? _maxLimit;
        #endregion

        /// <summary>
        /// Add an undoable instance into the Undo list.
        /// </summary>
        /// <typeparam name="T">The type of instance this is.</typeparam>
        /// <param name="instance">The instance this undo item applies to.</param>
        public static void Add<T>(T instance) where T : IUndoRedo
        {
            if (instance == null)
                throw new ArgumentNullException("instance");

            UndoList.Add(instance);
            RedoList.Clear();

            // Ensure that the undo list does not exceed the maximum size.
            TrimUndoList();
        }

        /// <summary>
        /// Get or set the maximum size of the undo list.
        /// </summary>
        public static int? MaximumUndoLimit
        {
            get
            {
                return _maxLimit;
            }
            set
            {
                if (value.HasValue && value.Value < 0)
                {
                    throw new ArgumentOutOfRangeException("value");
                }
                _maxLimit = value;
                TrimUndoList();
            }
        }

        /// <summary>
        /// Ensure that the undo list does not get too big by
        /// checking the size of the collection against the
        /// <see cref="MaximumUndoLimit"/>
        /// </summary>
        private static void TrimUndoList()
        {
            if (_maxLimit.HasValue && _maxLimit.Value > 0)
            {
                while (_maxLimit.Value < UndoList.Count)
                {
                    UndoList.RemoveAt(0);
                }
            }
        }

        /// <summary>
        /// Actions can only be undone if there are items in the <see cref="UndoList"/>.
        /// </summary>
        public static bool CanUndo
        {
            get
            {
                return UndoList.Count > 0;
            }
        }

        /// <summary>
        /// Actions can only be redone if there are items in the <see cref="RedoList"/>.
        /// </summary>
        public static bool CanRedo
        {
            get
            {
                return RedoList.Count > 0;
            }
        }

        /// <summary>
        /// Clear all items from the list.
        /// </summary>
        public static void ClearAll()
        {
            UndoList.Clear();
            RedoList.Clear();
        }

        /// <summary>
        /// Undo the last VM change.
        /// </summary>
        public static void Undo()
        {
            if (UndoList.Count > 0)
            {
                // Extract the item from the undo list.
                IUndoRedo item = UndoList.Last();
                UndoList.RemoveAt(UndoList.Count - 1);
                List<IUndoRedo> copyRedoList = RedoList.ToList();
                copyRedoList.Add(item);
                // We need to copy the undo list here.
                List<IUndoRedo> copyUndoList = UndoList.ToList();
                item.Undo();
                // Now repopulate the undo and redo lists.
                UpdateRedoList(copyRedoList);
                UndoList.Clear();
                UndoList.AddRange(copyUndoList);
            }
        }

        /// <summary>
        /// Redo the last undone VM change.
        /// </summary>
        /// <remarks>
        /// Unlike the undo operation, we don't need to copy the undo list out
        /// because we want the item we're redoing being added back to the redo
        /// list.
        /// </remarks>
        public static void Redo()
        {
            if (RedoList.Count > 0)
            {
                // Extract the item from the redo list.
                IUndoRedo item = RedoList.Last();
                // Now, remove it from the list.
                RedoList.RemoveAt(RedoList.Count - 1);
                // Here we need to copy the redo list out because
                // we will clear the list when the Add is called and
                // the Redo is cleared there.
                List<IUndoRedo> redoList = RedoList.ToList();
                // Redo the last operation.
                item.Redo();
                // Now reset the redo list.
                UpdateRedoList(redoList);
            }
        }

        private static void UpdateRedoList(List<IUndoRedo> redoList)
        {
            RedoList.Clear();
            RedoList.AddRange(redoList);
        }

        /// <summary>
        /// Get the undo list.
        /// </summary>
        public static RangeObservableCollection<IUndoRedo> UndoList
        {
            get
            {
                if (_undoList == null)
                    _undoList = new RangeObservableCollection<IUndoRedo>();
                return _undoList;
            }
            private set
            {
                _undoList = value;
            }
        }

        /// <summary>
        /// Get the redo list.
        /// </summary>
        public static RangeObservableCollection<IUndoRedo> RedoList
        {
            get
            {
                if (_redoList == null)
                    _redoList = new RangeObservableCollection<IUndoRedo>();
                return _redoList;
            }
            private set
            {
                _redoList = value;
            }
        }
    }
}

In this code, we have two lists – the undoable list and the redoable list. These lists wrap up the IUndoRedo interface we defined earlier and will actually handle calling Undo or Redo as appropriate. There’s a little wrinkle when you call Undo – we need to copy the Redo list out to a temporary copy so that we can add it back later on. The way that the undo works, is to extract the last item from the undo stack – which then gets removed. This item is put onto the redo stack so that we can redo it later if needs be. If you notice, in the Add method, we clear the Redo stack so that we can’t perform a Redo after a new operation. As the property gets updated and triggers the Add method, we have to copy the Redo out, and add it back in after the Add has been performed.

All you need to do now, is wire your ViewModel up to the UndoManager and Robert’s your mothers brother. I’ve attached a sample application which demonstrates this in action – this application isn’t finished yet as we’re leaving room for the next installment, where we hook into an undoable observable collection. Here’s a screenshot of the application in action:

Download: The usual rules apply, the download needs to be renamed from .doc to .zip. undoredosamplezip

Logging display and WPF

A question appeared over on the Code Project forums today about binding the output from log4net into WPF. The question asked was:

“I’m trying to use Log4net to log messages within my application. I’m adding a WPF window and want to stream the messages to the window. Log4net provides a TextWriterAppender that takes a StringWriter and writes logged events to the StringWriter, flushing it after each event.I want to simply connect the output of the StringWriter as the Text property on a TextBox. When I started this, it seemed simple and obvious – now I’m less sure. Ideally, I would simply like to bind the StringWriter to the TextBox, but haven’t found the incantation.

The basic problem is that the StringWriter doesn’t provide something like the INotifyPropertyChanged event to trigger code output a new log message (unless there is something behind the scenes I haven’t found).

I’ve see many examples of binding, all of which seem to presume that I have control over the writer itself. Am I missing something simple (I hope), or is this really not that straightforward.”

This is a very good question, so I thought I’d knock together a quick sample application to demonstrate how to do this. The first thing to remember is that log4net allows you to create your own appenders and use them in your application. The second thing to remember is that you need to hook INotifyPropertyChanged into the mechanism. To that end, I created the following appender:

namespace log4netSample.Logging
{
  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
  using log4net.Appender;
  using System.ComponentModel;
  using System.IO;
  using System.Globalization;
  using log4net;
  using log4net.Core;

  /// <summary>
  /// The appender we are going to bind to for our logging.
  /// </summary>
  public class NotifyAppender : AppenderSkeleton, INotifyPropertyChanged
  {
    #region Members and events
    private static string _notification;
    private event PropertyChangedEventHandler _propertyChanged;

    public event PropertyChangedEventHandler PropertyChanged
    {
      add { _propertyChanged += value; }
      remove { _propertyChanged -= value; }
    }
    #endregion

    /// <summary>
    /// Get or set the notification message.
    /// </summary>
    public string Notification
    {
      get
      {
        return _notification; ;
      }
      set
      {
        if (_notification != value)
        {
          _notification = value;
          OnChange();
        }
      }
    }

    /// <summary>
    /// Raise the change notification.
    /// </summary>
    private void OnChange()
    {
      PropertyChangedEventHandler handler = _propertyChanged;
      if (handler != null)
      {
        handler(this, new PropertyChangedEventArgs(string.Empty));
      }
    }

    /// <summary>
    /// Get a reference to the log instance.
    /// </summary>
    public NotifyAppender Appender
    {
      get
      {
        return Log.Appender;
      }

    }

    /// <summary>
    /// Append the log information to the notification.
    /// </summary>
    /// <param name="loggingEvent">The log event.</param>
    protected override void Append(LoggingEvent loggingEvent)
    {
      StringWriter writer = new StringWriter(CultureInfo.InvariantCulture);
      Layout.Format(writer, loggingEvent);
      Notification += writer.ToString();
    }
  }
}

Whenever a new message is appended, the Notification is updated and the PropertyChangedEventHandler is called to notify the calling application that the binding has been updated. In order to use this appender, you need to hook it into your configuration:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="log4net"
      type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />
  </configSections>
  <appSettings>
    <add key="log4net.Internal.Debug" value="false"/>
  </appSettings>
  <system.diagnostics>
    <trace autoflush="true">
      <listeners>
        <add name="textWriterTraceListener"
             type="System.Diagnostics.TextWriterTraceListener"
             initializeData="C:\log4net_internal.log"/>
      </listeners>
    </trace>
  </system.diagnostics>
  <log4net>
    <appender name="NotifyAppender" type="log4netSample.Logging.NotifyAppender" >
      <layout type="log4net.Layout.PatternLayout">
        <param name="Header" value="[Header]\r\n" />
        <param name="Footer" value="[Footer]\r\n" />
        <param name="ConversionPattern" value="%d [%t] %-5p %c %m%n" />
      </layout>
    </appender>

    <root>
      <level value="ALL" />
      <appender-ref ref="NotifyAppender" />
    </root>
  </log4net>
</configuration>

Note that you might want to add the following line into your AssemblyInfo.cs file:

[assembly: log4net.Config.XmlConfigurator(Watch=true)]

I find the following class really helpful when logging:

namespace log4netSample.Logging
{
  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
  using log4net;
  using log4net.Config;
  using log4net.Appender;
  using log4net.Repository.Hierarchy;

  public enum LogLevel
  {
    Debug = 0,
    Error = 1,
    Fatal = 2,
    Info = 3,
    Warning = 4
  }
  /// <summary>
  /// Write out messages using the logging provider.
  /// </summary>
  public static class Log
  {
    #region Members
    private static readonly ILog _logger = LogManager.GetLogger(typeof(Log));
    private static Dictionary<LogLevel, Action<string>> _actions;
    #endregion

    /// <summary>
    /// Static instance of the log manager.
    /// </summary>
    static Log()
    {
      XmlConfigurator.Configure();
      _actions = new Dictionary<LogLevel, Action<string>>();
      _actions.Add(LogLevel.Debug, WriteDebug);
      _actions.Add(LogLevel.Error, WriteError);
      _actions.Add(LogLevel.Fatal, WriteFatal);
      _actions.Add(LogLevel.Info, WriteInfo);
      _actions.Add(LogLevel.Warning, WriteWarning);
    }

    /// <summary>
    /// Get the <see cref="NotifyAppender"/> log.
    /// </summary>
    /// <returns>The instance of the <see cref="NotifyAppender"/> log, if configured.
    /// Null otherwise.</returns>
    public static NotifyAppender Appender
    {
      get
      {
        foreach (ILog log in LogManager.GetCurrentLoggers())
        {
          foreach (IAppender appender in log.Logger.Repository.GetAppenders())
          {
            if (appender is NotifyAppender)
            {
              return appender as NotifyAppender;
            }
          }
        }
        return null;
      }
    }

    /// <summary>
    /// Write the message to the appropriate log based on the relevant log level.
    /// </summary>
    /// <param name="level">The log level to be used.</param>
    /// <param name="message">The message to be written.</param>
    /// <exception cref="ArgumentNullException">Thrown if the message is empty.</exception>
    public static void Write(LogLevel level, string message)
    {
      if (!string.IsNullOrEmpty(message))
      {
        if (level > LogLevel.Warning || level < LogLevel.Debug)
          throw new ArgumentOutOfRangeException("level");

        // Now call the appropriate log level message.
        _actions[level](message);
      }
    }

    #region Action methods
    private static void WriteDebug(string message)
    {
      if (_logger.IsDebugEnabled)
        _logger.Debug(message);
    }

    private static void WriteError(string message)
    {
      if (_logger.IsErrorEnabled)
        _logger.Error(message);
    }

    private static void WriteFatal(string message)
    {
      if (_logger.IsFatalEnabled)
        _logger.Fatal(message);
    }

    private static void WriteInfo(string message)
    {
      if (_logger.IsInfoEnabled)
        _logger.Info(message);
    }

    private static void WriteWarning(string message)
    {
      if (_logger.IsWarnEnabled)
        _logger.Warn(message);
    }
    #endregion
  }
}

It’s a simple matter then to do something like Log.Write(LogLevel.Info, “This is my message”);

If you download the attached sample, you’ll get to see the whole application running in all its glory, and you can see how updating the log results in the output being updated. Don’t forget to rename the .doc file to .zip when you save it.

log4netsamplezip

Textbox Drag/Drop in WPF

So last week, somebody posted a question on Code Project about why a Drag Drop into a TextBox in WPF doesn’t actually work. When you attempt to drag and drop an item into a TextBox, it refuses to cooperate and leaves the mouse cursor as the Drop denied cursor and you can’t drop into the field. (Incidentally, this behaviour also applies to RichTextBox and FlowDocument controls). The reason that you can’t drop into these fields, even if you set AllowDrop to true, is that these particular controls mark drag and drop events as handled, preventing you from handling them yourself.

Now this might seem like a big problem – it certainly makes it look like you can’t drag/drop into a textbox, and this would seem to be a huge oversight on Microsoft’s part. Fortunately, with a little bit of knowledge of how WPF handles commands, it’s actually fairly easy to come up with a workaround. Remember that I said that WPF marks these operations as handled? This is the key to being able to work around it – each particular event (such as a DragOver event), also has a corresponding Preview event which we can hook into to perform our processing. Before I show you the code though, the OP posted a follow up query:

“A specific question I have about your solution is that you get the standard mouse cursor with the plus sign inside a box when the drag operation enters the edit box. In my initial solution to the drop into a ListBox I got a mouse cursor with an empty box, not the box with the plus sign. By experimenting I determined that you achieve this with the code you have in the PreviewDrag events. What about that code gets you the cursor with the plus sign, avoiding that anemic cursor without the plus sign?”

The following filename drag/drop sample demonstrates how changing the DragDropEffects parameter changes the appearance of the drop cursor, in answer to the Original Posters followup question:

<Window x:Class="SampleDragDrop.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:sys="clr-namespace:System;assembly=mscorlib"
  Title="Window1" Height="274" Width="300">
  <Window.Resources>
    <ObjectDataProvider
        MethodName="GetValues"
        ObjectType="{x:Type sys:Enum}"
        x:Key="DragProvider">
      <ObjectDataProvider.MethodParameters>
        <x:Type TypeName="DragDropEffects" />
      </ObjectDataProvider.MethodParameters>
    </ObjectDataProvider>
    <Style TargetType="{x:Type TextBlock}">
      <Setter Property="Margin" Value="3" />
      <Setter Property="VerticalAlignment" Value="Center" />
    </Style>
  </Window.Resources>
  <Grid>
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="101*" />
      <ColumnDefinition Width="177*" />
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
      <RowDefinition Height="27*" />
      <RowDefinition Height="203.258*" />
      <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <TextBlock Text="Drag Drop Effects" Grid.Column="0" Grid.Row="0" />
    <TextBlock Text="Drop target" Grid.Column="0" Grid.Row="1" />
    <TextBlock Text="Handled" Grid.Column="0" Grid.Row="2" />
    <ComboBox
      x:Name="cboDropEffects"
      Grid.Row="0"
      Grid.Column="1"
      Margin="2"
      SelectedIndex="0"
      ItemsSource="{Binding Source={StaticResource DragProvider}}" />
    <TextBox
      Grid.Row="1"
      Grid.Column="1"
      Margin="2"
      PreviewDragEnter="TextBox_PreviewDragEnter"
      PreviewDragOver="TextBox_PreviewDragEnter"
      PreviewDrop="TextBox_PreviewDrop" />
    <CheckBox x:Name="chkHandled" IsChecked="True" Grid.Row="2" Grid.Column="1" Margin="2" />
  </Grid>
</Window>

If you look carefully at the code, you see that we bind PreviewDragEnter and the PreviewDragOver to the same event handler. The PreviewDrop event maps to a different event handler, where we actually perform the drop of the filename.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace SampleDragDrop
{
  /// <summary>
  /// Interaction logic for Window1.xaml
  /// </summary>
  public partial class Window1 : Window
  {
    public Window1()
    {
      InitializeComponent();
    }

    private void TextBox_PreviewDragEnter(object sender, DragEventArgs e)
    {
      e.Effects = (DragDropEffects)cboDropEffects.SelectedItem;
      if (chkHandled.IsChecked.HasValue)
      {
        e.Handled = chkHandled.IsChecked.Value;
      }
    }

    private void TextBox_PreviewDrop(object sender, DragEventArgs e)
    {
      object text = e.Data.GetData(DataFormats.FileDrop);
      TextBox tb = sender as TextBox;
      if (tb != null)
      {
        tb.Text = string.Format("{0}", ((string[])text)[0]);
      }
    }
  }
}

When you run the sample, play around with the Drag Drop Effects values, and setting/unsetting the Handled checkbox, to see what behaviour the textbox exhibits (and the answer to the OPs question, is that setting e.Effects to DragDropEffects.All sets the cursor to the relevant cursor).

This sample is available here: SampleDragDrop. Don’t forget to change the extension from .doc to .zip when you download it.

It’s not that hard to add the missing bits to Silverlight.

Recently, my good friend Josh Smith announced that he was putting together a set of MVVM foundation classes. In typical Josh fashion, these classes are hyper useful and hyper clever, and represent some of the great classes he’s produced over the last couple of years to help with MVVM.

Most of these classes work straight out of the box in both WPF and Silverlight, but there is a fly in the ointment. Josh recently developed a class to observe objects that implement INotifyPropertyChanged and it’s seriously good; and it’s virtually totally useless in Silverlight due to Silverlight not supporting the underlying mechanism that is used in the monitoring; the PropertyChangedEventManager class. Now, in a lesser framework than .NET, I’d be worried – but .NET gives us so much freedom to add in the missing functionality, and I wouldn’t be a WPF Disciple if I didn’t like to tinker.

Before I go any further, I will say that there is an alternative implementation in the Silverlight Toolkit in the WeakEventListener. The problem with this class (I use the word problem advisedly here) is that it requires you to use lambda expressions. It also meant that the class that Josh put together would have to be modified, so I decided to see how hard it would be to put together an implementation of PropertyChangedEventManager that works in Silverlight.

As I was working to a well defined feature set (i.e. it only had to work with the functionality in Josh’s class), I didn’t have to recreate the total functionality in the underlying framework classes. I did want it to work with the weak event pattern, however, so without further ado – here’s the functionality in Silverlight.

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.ComponentModel;
using System.Collections.Generic;

namespace System.Windows
{
  /// <summary>
  /// Provides an implementation so that you can use the
  /// "weak event listener" pattern to attach listeners
  /// for the <see cref="PropertyChanged" /> event.
  /// </summary>
  public class PropertyChangedEventManager
  {
    #region Members
    private Dictionary<string, List<WeakReference>> _list;
    private static object SyncLock = new object();
    private static PropertyChangedEventManager _manager = null;
    #endregion

    #region Public methods
    /// <summary>
    /// Adds the specified listener to the list of listeners on the specified source.
    /// </summary>
    ///
<param name="source">The object with the event.</param>
    ///
<param name="listener">The object to add as a listener.</param>
    ///
<param name="propertyName">The name of the property that exists on
    /// source upon which to listen for changes.</param>
    public static void AddListener(INotifyPropertyChanged source,
      IWeakEventListener listener,
      string propertyName)
    {
      Instance.PrivateAddListener(source, listener, propertyName);
    }

    /// <summary>
    /// Removes the specified listener from the list of listeners on the
    /// specified source.
    /// </summary>
    ///
<param name="source">The object with the event.</param>
    ///
<param name="listener">The object to remove as a listener.</param>
    ///
<param name="propertyName">The name of the property that exists
    /// on source upon which to listen for changes.</param>
    public static void RemoveListener(INotifyPropertyChanged source,
      IWeakEventListener listener,
      string propertyName)
    {
      Instance.PrivateRemoveListener(source, listener, propertyName);
    }
    #endregion

    /// <summary>
    /// Get the current instance of <see cref="PropertyChangedEventManager"/>
    /// </summary>
    private static PropertyChangedEventManager Instance
    {
      get
      {
        if (_manager == null)
          _manager = new PropertyChangedEventManager();
        return _manager;
      }
    }

    /// <summary>
    /// Begin listening for the <see cref="PropertyChanged"/> event on
    /// the provided source.
    /// </summary>
    ///
<param name="source">The object on which to start listening
    /// for <see cref="PropertyChanged"/>.</param>
    private void StartListening(INotifyPropertyChanged source)
    {
      source.PropertyChanged += new PropertyChangedEventHandler(this.PropertyChanged);
    }

    /// <summary>
    /// Stop listening for the <see cref="PropertyChanged"/> event on the
    /// provided source.
    /// </summary>
    ///
<param name="source">The object on which to start listening for
    /// <see cref="PropertyChanged"/>.</param>
    private void StopListening(INotifyPropertyChanged source)
    {
      source.PropertyChanged -= new PropertyChangedEventHandler(this.PropertyChanged);
    }

    /// <summary>
    /// The method that handles the <see cref="INotifyPropertyChanged.PropertyChanged"/> event.
    /// </summary>
    ///
<param name="sender">The source of the event.</param>
    ///
<param name="args">A <see cref="PropertyChangedEventArgs"/> that
    /// contains the event data.</param>
    private void PropertyChanged(object sender, PropertyChangedEventArgs args)
    {
      List<WeakReference> list = _list[args.PropertyName];
      if (list != null)
      {
        // We have the listeners. Deal with them
        foreach (WeakReference item in list)
        {
          IWeakEventListener eventItem = item.Target as IWeakEventListener;
          if (eventItem != null && item.IsAlive)
          {
            eventItem.ReceiveWeakEvent(this.GetType(), sender, args);
          }
        }
      }
    }

    /// <summary>
    /// Private method to add the specified listener to the list of listeners
    /// on the specified source.
    /// </summary>
    ///
<param name="source">The object with the event.</param>
    ///
<param name="listener">The object to add as a listener.</param>
    ///
<param name="propertyName">The name of the property that exists
    /// on source upon which to listen for changes.</param>
    private void PrivateAddListener(INotifyPropertyChanged source,
      IWeakEventListener listener,
      string propertyName)
    {
      if (_list == null)
      {
        _list = new Dictionary<string, List<WeakReference>>();
      }

      lock (SyncLock)
      {
        WeakReference reference = new WeakReference(listener);
        if (_list.ContainsKey(propertyName))
        {
          _list[propertyName].Add(reference);
        }
        else
        {
          List<WeakReference> list = new List<WeakReference>();
          list.Add(reference);
          _list.Add(propertyName, list);
        }
        // Now, start listening to source
        StartListening(source);
      }
    }

    /// <summary>
    /// Private method to remove the specified listener from the list of listeners
    /// on the specified source.
    /// </summary>
    ///
<param name="source">The object with the event.</param>
    ///
<param name="listener">The object to remove as a listener.</param>
    ///
<param name="propertyName">The name of the property that exists on
    /// source upon which to listen for changes.</param>
    private void PrivateRemoveListener(INotifyPropertyChanged source,
      IWeakEventListener listener,
      string propertyName)
    {
      if (_list != null)
      {
        lock (SyncLock)
        {
          if (_list.ContainsKey(propertyName))
          {
            // Stop responding to changes
            StopListening(source);
            // Remove the item from the list.
            WeakReference reference = null;
            foreach (WeakReference item in _list[propertyName])
            {
              if (item.Target.Equals(listener))
              {
                reference = item;
              }
            }
            if (reference != null)
            {
              _list[propertyName].Remove(reference);
            }
          }
        }
      }
    }
  }
}

The only thing that’s missing is the weak event listener interface (IWeakEventListener):

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace System.Windows
{
  /// <summary>
  /// Provides event listening support for classes that expect to receive events
  /// through the WeakEvent pattern and a WeakEventManager.
  /// </summary>
  public interface IWeakEventListener
  {
    /// <summary>
    /// Receives events from the centralized event manager.
    /// </summary>
    ///
<param name="managerType">The type of the WeakEventManager calling this method.</param>
    ///
<param name="sender">Object that originated the event.</param>
    ///
<param name="e">Event data.</param>
    /// <returns>true if the listener handled the event. It is considered an error by the
    /// WeakEventManager handling in WPF to register a listener for an event that the
    /// listener does not handle. Regardless, the method should return false if it receives
    /// an event that it does not recognize or handle.
    /// </returns>
    bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e);
  }
}

That’s it – that’s all you need to add in to add the “missing” functionality. Simple, isn’t it?

You can download the source here. Note that you’ll have to rename the file from .doc to .zip before you can decompress it.