Archive

Archive for the ‘behavior’ Category

Keeping it focused

July 5, 2012 5 comments

Well, it’s been a while since I’ve blogged, as I’ve been a busy little bee working on CodeStash. I feel guilty about this, so I’d like to take the chance to pass on the solution to a problem that was posted on Code Project today. The scenario goes like this:

You have a ViewModel with some text that you want to be updated, and you do all the usual binding work with some TextBox elements. The thing is, you want to update your ViewModel properties when the TextBox loses focus. OK, so that’s easily achieved – in fact, it’s the default behaviour of the TextBox. However, you have a button with IsDefault set on it, so pressing Enter in the TextBox triggers the button click – but as the TextBox hasn’t lost focus, the property you are binding to doesn’t get updated. This is, of course, a problem. Fortunately, there’s an easy little trick that you can deploy to update the property, and it’s easily achieved using an attached behavior.

All you need to do is associate the behaviour to the Button you have set the IsDefault property on, and Bob is your mothers brother, the Text property updates. So, what does this behavior look like:

using System.Windows.Interactivity;
using System.Windows.Controls;
using System.Windows;
using System.Windows.Input;
using System.Windows.Data;
/// <summary>
/// Associate this behaviour with the button that you mark as IsDefault
/// to trigger the ViewModel update when the user clicks enter in a textbox
/// and the property doesn't update because the update source is set to
/// lost focus.
/// </summary>
public class DefaultButtonUpdateTextBoxBindingBehavior : Behavior<Button>
{
    /// <summary>
    /// Hook into the button click event.
    /// </summary>
    protected override void OnAttached()
    {
        AssociatedObject.Click += AssociatedObject_Click;
        base.OnAttached();
    }

    /// <summary>
    /// Unhook the button click event.
    /// </summary>
    protected override void OnDetaching()
    {
        AssociatedObject.Click -= AssociatedObject_Click;
    }

    /// <summary>
    /// The click event handler.
    /// </summary>
    void AssociatedObject_Click(object sender, System.Windows.RoutedEventArgs e)
    {
        // Get the element with the keyboard focus
        FrameworkElement el = Keyboard.FocusedElement as FrameworkElement;
        if (el != null && el is TextBox)
        {
            // Get the binding expression associated with the text property
            // for this element.
            BindingExpression expression = el.GetBindingExpression(TextBox.TextProperty);
            if (expression != null)
            {
                // Now, trigger the update.
                expression.UpdateSource();
            }
        }
    }
}

As you can see, there’s not that much code needed. Basically, we get the element that has focus, and retrieve the binding expression associated with it. Once you get the binding expression, we trigger the update.

I’ve copied this behaviour into CodeStash – the handy repository that I will be putting future snippets in for your delectation.

You see, you don’t always need code behind.

June 7, 2011 6 comments

By now you should be aware that I’m a big fan of attached behaviors. In this post, I’m going to demonstrate a simple technique to add resize and close functionality to window buttons when you want to custom draw your window chrome without having to add code behind the window. This is going to be a quick post, because it’s just so darned easy.

Note: Originally I was using Application.Current.MainWindow to retrieve the window as I only ever apply this trick to the application main window. Mike Strobel suggested using Window.GetWindow instead to retrieve the logical window for the button, just in case it was in a separate window. I’ve adjusted the code sample here to demonstrate this as it makes this more reusable. Thanks Mike.

namespace AttachedTitleButtonsSample
{
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Interactivity;

    /// <summary>
    /// Attach this behaviour to a button to enable a button to change the window state without
    /// having to write any code behind the view.
    /// </summary>
    public partial class TitleButtonBehavior : Behavior<Button>
    {
        /// <summary>
        /// The tile button action to apply.
        /// </summary>
        public enum TitleButtonAction
        {
            /// <summary>
            /// Close the application
            /// </summary>
            Close,
            /// <summary>
            /// Maximize the application
            /// </summary>
            Maximize,
            /// <summary>
            /// Minimize the application
            /// </summary>
            Minimize,
            /// <summary>
            /// Reset the application to normal
            /// </summary>
            Normal
        }

        /// <summary>
        /// Gets or sets the button behavior.
        /// </summary>
        public TitleButtonAction ButtonBehavior { get; set; }

        /// <summary>
        /// Add the click handler when this is attached.
        /// </summary>
        protected override void OnAttached()
        {
            this.AssociatedObject.Click += AssociatedObject_Click;
            base.OnAttached();
        }

        /// <summary>
        /// Remove the click handler when this is detached.
        /// </summary>
        protected override void OnDetaching()
        {
            this.AssociatedObject.Click -= AssociatedObject_Click;
            base.OnDetaching();
        }

        /// <summary>
        /// Change the window state when the button is clicked.
        /// </summary>
        void AssociatedObject_Click(object sender, System.Windows.RoutedEventArgs e)
        {
            Window window = Window.GetWindow(AssociatedObject);
            switch (ButtonBehavior)
            {
                case TitleButtonAction.Close:
                    window.Close();
                    break;
                case TitleButtonAction.Maximize:
                    window.WindowState = WindowState.Maximized;
                    break;
                case TitleButtonAction.Minimize:
                    window.WindowState = WindowState.Minimized;
                    break;
                case TitleButtonAction.Normal:
                    window.WindowState = WindowState.Normal;
                    break;
            }
        }
    }
}

Basically, all you need to do is create an attached behavior that hooks up to the Click event of the button and sets the size based on the appropriate value from the enumeration.

Sample application
I’ve attached a sample application that demonstrates this technique in action. As always, when you download the sample, you’ll need to rename it from a doc to a zip file.

AttachedTitleButtonsSampleZip

Keeping it regular

April 3, 2011 1 comment

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.

Getting control of your numbers

March 30, 2011 7 comments

[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.

February 11, 2011 Leave a comment

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

Draggable PushPins

October 10, 2010 4 comments

I must apologise that it has taken me so long to post a blog entry. I’ve been busy playing about with code contracts and working with Bing Maps in Silverlight – there’s been a lot for me to take in. Actually, the Bing maps part is the reason for this posting, and I hope that it really helps you out.

A common requirement in GIS applications is to take a point of interest from one location and move it to another by dragging it. This is behaviour that you would expect to be standard, but surprisingly enough it is not present with a default Pushpin in Silverlight Bing maps. In this post, we’ll discuss adding a Blend Behavior that provides this drag and drop functionality.

As usual, when writing a behavior, you start off by inheriting from the generic Behavior class, telling it which framework element you want to associate the behavior with.

public class DraggablePushpin : Behavior<Pushpin>
{
}

Now we want to tell it what happens when we attach the object. This is done in the OnAttached override.

/// <summary>
/// Hook the event handlers to this instance.
/// </summary>
protected override void OnAttached(){
base.OnAttached();
 AssociatedObject.MouseLeftButtonDown +=
new MouseButtonEventHandler(AssociatedObject_MouseLeftButtonDown);
}

Basically, when the left mouse button is pressed on the Pushpin, we’re going to invoke an event handler that is going to do the grunt work for handling the relevant events that we need to work with. The definition of this method looks like this:

/// <summary>
/// Called when the left mouse button is pressed on a
/// Point of Interest.
/// </summary>
void AssociatedObject_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (_map == null)
// Find the map this pushpin is attached to.
_map = FindParent<Map>(AssociatedObject);
  if (_map != null)
{
if (this.ParentMapMousePanHandler == null)
{
this.ParentMapMousePanHandler = new EventHandler<MapMouseDragEventArgs>
((s, epan) => epan.Handled = this._isdragging);
_map.MousePan += ParentMapMousePanHandler;
}
if (this.ParentMapMouseLeftButtonEventHandler == null)
{
this.ParentMapMouseLeftButtonEventHandler = new MouseButtonEventHandler
((s, args) => ConfirmMoveLocation());
_map.MouseLeftButtonUp += this.ParentMapMouseLeftButtonEventHandler;
}
if (this.ParentMapMouseMoveHandler == null)
{
// If the mouse is performing a drag operation, convert the
// underlying location based on the position of the mouse
// on the viewport to a map based location.
this.ParentMapMouseMoveHandler = new MouseEventHandler(
(s, mouseargs) =>
{
if (_isdragging)
_location = _map.ViewportPointToLocation(mouseargs.GetPosition(_map));
});
_map.MouseMove += this.ParentMapMouseMoveHandler;
}
}
_isdragging = true;
}

This method starts off by identifying the map that owns the pushpin. This means traversing the visual tree until the map is identified (I’ve done this because a common way of allocating Pushpins to a map is to host them in a DataTemplate which causes problems when attempting to find the parent map just by methods on the Pushpin). Once the map has been identified, we are going to handle the following map events:

Mouse pan
Mouse left button up
Mouse move.

When the mouse pans, the event handler uses a flag to determine whether or not a Pushpin is being dragged and sets the Handled parameter as necessary.

When the mouse moves, the event handler converts the position of the mouse on the map into a location coordinate.

When the left button is released, the behavior calls a method to prompt the user whether or not they wish to move the location and updates the position accordingly.
Finally, we have methods available to detach the events. The whole behaviour is shown here:

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.Windows.Interactivity;
using Microsoft.Maps.MapControl;
using Microsoft.Maps.MapControl.Core;
namespace Pointilist.Behaviors{
/// <summary>
/// This behaviour adds drag capabilities to a Bing Maps <see cref="Pushpin"/> control.
/// </summary>
public class DraggablePushpin : Behavior<Pushpin>
{
#region Members
private bool _isdragging = false;
private MapBase _map = null;
private EventHandler<MapMouseDragEventArgs> ParentMapMousePanHandler;
private MouseButtonEventHandler ParentMapMouseLeftButtonEventHandler;
private MouseEventHandler ParentMapMouseMoveHandler;
private Location _location;
#endregion
    /// <summary>
/// Hook the event handlers to this instance.
/// </summary>
protected override void OnAttached()
{
base.OnAttached();
      AssociatedObject.MouseLeftButtonDown +=
new MouseButtonEventHandler(AssociatedObject_MouseLeftButtonDown);
    }
    /// <summary>
/// Unhook the events when this is detaching.
/// </summary>
protected override void OnDetaching()
{
base.OnDetaching();
      AssociatedObject.MouseLeftButtonDown -=
new MouseButtonEventHandler(AssociatedObject_MouseLeftButtonDown);
      DetachEvents();
}

    /// <summary>
/// Make sure that no mouse events are left dangling.
/// </summary>
private void DetachEvents()
{
if (_map == null) return;
if (this.ParentMapMousePanHandler != null)
{
_map.MousePan -= ParentMapMousePanHandler;
ParentMapMousePanHandler = null;
}
if (ParentMapMouseLeftButtonEventHandler != null)
{
_map.MouseLeftButtonUp -= ParentMapMouseLeftButtonEventHandler;
ParentMapMouseLeftButtonEventHandler = null;
}
if (ParentMapMouseMoveHandler != null)
{
_map.MouseMove -= ParentMapMouseMoveHandler;
ParentMapMouseMoveHandler = null;
}
}
    /// <summary>
/// Only move the poi if the user accepts the change
/// (at which point, it's saved to the database).
/// </summary>
private void ConfirmMoveLocation()
{
_isdragging = false;
if (_location == null) return;

      DetachEvents();
      MessageBoxResult result = MessageBox.Show("Are you sure you want to move here?",
"Move location?", MessageBoxButton.OKCancel);
if (result == MessageBoxResult.OK)
{
AssociatedObject.Location = _location;
}
      _location = null;
}
    /// <summary>
/// Called when the left mouse button is pressed on a
/// Point of Interest.
/// </summary>
void AssociatedObject_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (_map == null)
// Find the map this pushpin is attached to.
_map = FindParent<Map>(AssociatedObject);
      if (_map != null)
{
if (this.ParentMapMousePanHandler == null)
{
this.ParentMapMousePanHandler = new EventHandler<MapMouseDragEventArgs>
((s, epan) => epan.Handled = this._isdragging);
_map.MousePan += ParentMapMousePanHandler;
}
if (this.ParentMapMouseLeftButtonEventHandler == null)
{
this.ParentMapMouseLeftButtonEventHandler = new MouseButtonEventHandler
((s, args) => ConfirmMoveLocation());
_map.MouseLeftButtonUp += this.ParentMapMouseLeftButtonEventHandler;
}
if (this.ParentMapMouseMoveHandler == null)
{
// If the mouse is performing a drag operation, convert the
// underlying location based on the position of the mouse
// on the viewport to a map based location.
this.ParentMapMouseMoveHandler = new MouseEventHandler(
(s, mouseargs) =>
{
if (_isdragging)
_location = _map.ViewportPointToLocation(mouseargs.GetPosition(_map));
});
_map.MouseMove += this.ParentMapMouseMoveHandler;
}
}
_isdragging = true;
}
    /// <summary>
/// Find the relevant parent item for a particular dependency object.
/// </summary>
/// <typeparam name="T">The type of object to search for.</typeparam>
/// <param name="child">The <see cref="DependencyObject"/> to start searching from.</param>
/// <returns>The parent object if found, null otherwise.</returns>
public static T FindParent<T>(DependencyObject child) where T : FrameworkElement
{
DependencyObject parentObject = VisualTreeHelper.GetParent(child);
      // If parentObject is null, we've reached the top of the tree without finding the item we were looking for.
if (parentObject == null) return null;
      T parent = parentObject as T;
// If parent is null, recursively call this method.
if (parent == null)
return FindParent<T>(parentObject);
      // If we reach this point, we have found the parent item we are looking for.
return parent;
}
}
}

Now, it’s a simple matter to hook it up to a Pushpin.

<m:Pushpin m:MapLayer.Position="{Binding Location, Mode=TwoWay}"
Visibility="{Binding IsVisible, Converter={StaticResource VisibilityConverter}}"
PositionOrigin="Center" ToolTipService.ToolTip="{Binding Address}">
<interact:Interaction.Behaviors>
<behavior:DraggablePushpin />
</interact:Interaction.Behaviors>
</m:Pushpin>

There you have it – a draggable Pushpin. I hope that this helps you as much as it’s helping me.

I love behaving

June 8, 2010 2 comments

Some poor chap posted a question on Code Project asking how to convert a textbox to be lower or upper case. My initial reaction to this (as with so much else), is to use MVVM and just handle the property change. I nearly posted this, but then I decided not to – instead, in this post I’d like to demonstrate the use of attached behaviours (or behaviors for those who don’t like the u).

An attached behaviour allows us to do things without having to depend on other items, such as a trigger or actions. Now, there’s a handy class added to the System.Windows.Interactivity dll called Behavior. We derive from this to add the behaviour to a particular control type. In this example, we’re going to hook into a TextBox and add the functionality that we want to automatically handle the upper/lower casing as appropriate.

In the first part, we’re going to add a DependencyProperty that will accept the boolean to indicate whether or not it should be all upper or lower case (yes, I know that it would be better to use an enumeration here, but this is just a sample to demonstrate the functionality).

public static readonly DependencyProperty UpperLowerMaskProperty =
  DependencyProperty.Register("UpperLowerMask", typeof(bool), typeof(UpperOrLowerBehavior), null);
public bool UpperLowerMask
{
   get { return (bool)GetValue(UpperLowerMaskProperty); }
   set { SetValue(UpperLowerMaskProperty, value); }
}

As you can see, there’s nothing particularly frightening about the code so far. It’s a DP like any other that we’ve created before. Now, we’ve derived from Behavior, so we need to override the OnAttached and OnDetached methods to hook/unregister the events that we’re interested in from the TextBox. The code to do this is shown here:

protected override void OnAttached()
{
  base.OnAttached();
  AssociatedObject.PreviewKeyDown += new KeyEventHandler(AssociatedObject_PreviewKeyDown);
  AssociatedObject.TextChanged += new TextChangedEventHandler(AssociatedObject_TextChanged);
  DataObject.AddPastingHandler(AssociatedObject, PastingHandler);
}
protected override void OnDetaching()
{
  base.OnDetaching();
  AssociatedObject.PreviewKeyDown -= new KeyEventHandler(AssociatedObject_PreviewKeyDown);
  AssociatedObject.TextChanged -= new TextChangedEventHandler(AssociatedObject_TextChanged);
  DataObject.RemovePastingHandler(AssociatedObject, PastingHandler);
}
As you can see, there are three things that we are interested in; the PreviewKeyDown, TextChanged events, and the paste handler. PreviewKeyDown is used to identify whether or not we want to handle the text changing, and retrieving the cursor position at that point in time. TextChanged is used to handle the text being changed.

The code for these looks a lot like this:

void AssociatedObject_TextChanged(object sender, TextChangedEventArgs e)
{
  if (!_shouldBeCaptured) return;
  if (UpperLowerMask)
  {
    AssociatedObject.Text = AssociatedObject.Text.ToUpperInvariant();
  }
  else
  {
    AssociatedObject.Text = AssociatedObject.Text.ToLowerInvariant();
  }
  AssociatedObject.SelectionStart = _keyPos + 1;
}
void AssociatedObject_PreviewKeyDown(object sender, KeyEventArgs e)
{
  char key;
  char.TryParse(e.Key.ToString(), out key);
  _shouldBeCaptured = Keyboard.Modifiers == ModifierKeys.None && Char.IsLetter(key) ;
  _keyPos = AssociatedObject.SelectionStart;
}

Again, as you can see, there’s nothing particularly scary here. The AssociatedObject identifies the TextBox that we’re working on. Now that we have the behaviour, we need to use it. This means that we need to hook it into the XAML; here’s the page that we’re working with:

<Window x:Class="AllLowerOrUpper.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   xmlns:behavior="clr-namespace:AllLowerOrUpper"
   xmlns:interact="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
   Title="Upper/Lower Behavior" Height="350" Width="525">
   <StackPanel>
     <TextBox Width="500" >
       <interact:Interaction.Behaviors>
         <behavior:UpperOrLowerBehavior UpperLowerMask="true" />
       </interact:Interaction.Behaviors>
     </TextBox>
     <TextBox Width="500" >
       <interact:Interaction.Behaviors>
          <behavior:UpperOrLowerBehavior UpperLowerMask="false" />
       </interact:Interaction.Behaviors>
     </TextBox>
   </StackPanel>
</Window>

And that’s it – a simple attached behavior to cope with the upper/lower functionality. It’s not perfect code, it’s intended to demonstrate the use of attached behaviors, so please add whatever functionality you need. Feel free to download the related sample (and don’t forget to rename the .doc file to .zip to get round the WordPress allowed files issue).

Categories: behavior Tags:
The Canny Coder

Java 8 Functional Programming with Lambda Expressions

pihole.org

Adventures in theoretical computer science, with your host, chaiguy1337

Confessions of a coder

Confessions of a WPF lover

Follow

Get every new post delivered to your Inbox.

Join 39 other followers