Using an Attached DependencyProperty to Implement Pixel Snapping as an Attached Behavior

In a previous post, I introduced the Snapper element, which is a UserControl subclass that snaps its Content to an integer pixel. Now I'll show how to implement snapping as an attached behavior using a custom attached DependencyProperty.

To use the Snapper element, you put it into your tree, and "wrap" the element that you want to snap, like this:

<local:Snapper>
    <Rectangle Height="40" Width="40" Stroke="Black" Margin="2"/>
</local:Snapper>

 

That's not bad, but there's a cooler way to do it. We can add pixel snapping to any element by using an attached property to attach a behavior, like this:

<Rectangle Height="40" Width="40" Stroke="Black" Margin="2" local:PixelSnapBehavior.PixelSnap="Closest"/>

 

Attached Properties 

Using XAML's attached properties, it is possible to put a property on an element that doesn't know about the property at compile time. In other words, "normal" properties have to be implemented on an element's class or base classes. Attached properties do not have this restriction. Some examples of attached properties include Canvas.Left, Canvas.Top, Canvas.ZIndex, Grid.Row, Grid.Column, etc. (If you have to put a "dot" in the property name, it is an attached property.) You can also create your own attached properties, and get and set their values on other objects. Before describing how to create your own attached properties, let's review regular DependencyProperties.

Dependency Properties

A "regular" DependencyProperty is a property that you define for a given class, and it is only valid on that class, like a CLR property. You register the DependencyProperty, and define a CLR property for it. A change notification callback is optional--you can pass null instead of the PropertyMetadata. Here are the elements needed to define a DependencyProperty. Note that the property and the change notification are static. Also note that the class you define the property on must descend from DependencyObject. In this case, I'm using a UserControl.

public class DPExample : UserControl

{

static DependencyProperty DistanceProperty = DependencyProperty.Register("Distance", typeof(double), typeof(DPExample), new PropertyMetadata(OnDistanceChanged));

static private void OnDistanceChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)

{

Debug.WriteLine("Distance property changed from {0} to {1}", e.OldValue, e.NewValue);

}

public double Distance

{

get { return (double)GetValue(DistanceProperty); }

set { SetValue(DistanceProperty, value); }

}

}

You use the DPExample.Distance property like you would use any CLR property, but the underlying storage is provided by the Silverlight property system. You can set the property in code, in XAML, etc. and you will get change notification. You can also animate it if it is of a type that can be animated.

 

Attached DependencyProperties

An attached DependencyProperty is defined on one class, but made to by used (mostly) on instances of other classes. It is not necessary for the other classes to know about this property at compile time, so you can set an attached DependencyProperty on any object that descends from DependencyObject--even objects provided in the Silverlight framework. The property is now registered with RegisterAttached (the parameters are the same.) The property get has been replaced by a public static void Get<propertyName> method, and the property setter has been replaced by a public static <type> Set<propertyName> method.

 

static DependencyProperty MassProperty = DependencyProperty.RegisterAttached("Mass", typeof(double), typeof(DPExample), new PropertyMetadata(OnMassChanged));

static private void OnMassChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)

{

    Debug.WriteLine("Mass property changed from {0} to {1}", e.OldValue, e.NewValue);

}

public static void SetMass(DependencyObject obj, double value)

{

    obj.SetValue(MassProperty, value);

}

public static double GetMass(DependencyObject obj)

{

    object result = obj.GetValue(MassProperty);

    return result != null ? (double)result : DefaultMass;

}

public const double DefaultMass = 1;

You will notice that when the GetMass method calls GetValue, it does not immediately cast to a double. This is because if the property has not been set on the instance passed in by the obj parameter, GetValue will return null. In this case, it is typical to return a default value (as above) or some value that signals "not set". This attached DependencyProperty can be set in code by calling the SetMass method, set in XAML, and animated.

 

Attached Behaviors

 

If we define a behavior as "doing something" then when we attach a behavior to an element, we are getting it to do something that it could not do before. This is typically done by attaching an event handler or handlers to an elements events. The behavior is in the event handler, which is defined elsewhere. This is obviously easy enough to do in code, but it can also be done in XAML (and code) by leveraging attached DependencyProperties. When an attached DependencyProperty is set on an instance, the property changed notification method is called. This is where you can hook into the element's events.

 

The Code

 

Here is a class that implements pixel snapping as an attached behavior. 

 

using System;

using System.Collections.Generic;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Media;

using System.Diagnostics;

namespace CustomAttachedDP

{

    public class PixelSnapBehavior

    {

      // Define the attached DependencyProperty

        public static DependencyProperty PixelSnapProperty = DependencyProperty.RegisterAttached(

            "PixelSnap", typeof(PixelSnapType), typeof(PixelSnapBehavior), new PropertyMetadata(SnapPropertyChanged));

        // In the property changed notification method, we will add the element to a list of objects that will

        // be snapped when we get a LayoutUpdated event. We have to do a bunch of fancy stuff with weak references,

    // our own list, etc. because there is no Unloaded event.

        public static void SnapPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)

        {

            PixelSnapType newSnap = (PixelSnapType)e.NewValue;

            int index = 0;

            while (index < _objects.Count)

            {

                if (_objects[index].Target == d)

                    break;

                ++index;

            }

            if (index < _objects.Count)

            {

                if (newSnap == PixelSnapType.None)

                {

                    if (_objects[index].IsAlive)

                    {

                        Debug.WriteLine("Removing RenderTransform");

                        ((FrameworkElement)_objects[index].Target).RenderTransform = null;

                    }

                    _objects.RemoveAt(index);

                }

            }

            else if (newSnap != PixelSnapType.None)

            {

                _objects.Add(new WeakReference(d));

            }

            if (!_attached && _objects.Count > 0)

            {

                FrameworkElement element = d as FrameworkElement;

                if (element != null)

                {

                    element.LayoutUpdated += new EventHandler(LayoutUpdated);

                    _attached = true;

                }

            }

            else if (_attached && _objects.Count == 0)

            {

                FrameworkElement element = d as FrameworkElement;

                if (element != null)

                {

                    element.LayoutUpdated -= new EventHandler(LayoutUpdated);

                    _attached = false;

                }

            }

        }

        // The attached DependencyProperty setter

        public static void SetPixelSnap(DependencyObject obj, PixelSnapType value)

        {

            obj.SetValue(PixelSnapProperty, value);

        }

        // The attached DependencyProperty getter

        public static PixelSnapType GetPixelSnap(DependencyObject obj)

        {

            object result = obj.GetValue(PixelSnapProperty);

            return result == null ? PixelSnapType.None : (PixelSnapType)result;

        }

        // A utility method to remove all snapped objects from the list.

        public static void RemoveAll()

        {

            while (_objects.Count > 0)

            {

                if (_objects[0].IsAlive)

                {

                    SetPixelSnap((DependencyObject)_objects[0].Target, PixelSnapType.None);

   }

                else

                {

                    _objects.RemoveAt(0);

                }

            }

        }

        // The event handler for the LayoutUpdated event. It will snap everything that it thinks is

        // still alive. This is not 100% bulletproof but it should work in most scenarios.

        private static void LayoutUpdated(object sender, EventArgs e)

        {

            int index = 0;

            while (index < _objects.Count)

            {

                if (_objects[index].IsAlive == false)

                {

                    _objects.RemoveAt(index);

                }

                else

                {

                    Snap(_objects[index].Target as FrameworkElement);

                    ++index;

                }

            }

        }

        // Try to align an element on an integer pixel

        private static void Snap(FrameworkElement target)

        {

            if (target == null)

                return;

            PixelSnapType snap = PixelSnapBehavior.GetPixelSnap(target);

            // Remove existing transform

            TranslateTransform savedTransform = target.RenderTransform as TranslateTransform;

            if (savedTransform != null)

     {

                target.RenderTransform = null;

            }

            // Calculate actual location

            MatrixTransform globalTransform = target.TransformToVisual(Application.Current.RootVisual) as MatrixTransform;

            Point p = globalTransform.Matrix.Transform(_zero);

            double deltaX = snap == PixelSnapType.Closest ? Math.Round(p.X) - p.X : (int)p.X - p.X;

            double deltaY = snap == PixelSnapType.Closest ? Math.Round(p.Y) - p.Y : (int)p.Y - p.Y;

            // Set new transform

            if (deltaX != 0 || deltaY != 0)

            {

                if (savedTransform == null)

                    savedTransform = new TranslateTransform();

                target.RenderTransform = savedTransform;

                savedTransform.X = deltaX;

                savedTransform.Y = deltaY;

            }

        }

        private static readonly Point _zero = new Point(0, 0);

        private static List<WeakReference> _objects = new List<WeakReference>();

        private static bool _attached = false;

    }

    public enum PixelSnapType

    {

        None,

        Closest,

        TopLeft

    }

}