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


    }


}

Comments (7)

  1. Rob Houweling with a Sketch Application (2 parts), Martin Mihaylov continuing with Shapes, Karen Corby

  2. TWiStErRob says:

    You’ve refactored the dependecy property’s explaining code, but the description after was not refactored: DPExample.Count was written instead of DPExample.Distance

  3. drelyea says:

    Hey, thanks, TWiStErRob. Corrected.

  4. dimkaz says:

    Nice sample, but don’t use IsLive property on the WeakReference it’s evil 😉

    Between your call to .IsLive and .Target the GC could have collected the object and exception will be raised

  5. juliandominguez says:

    Hi, good to see that attached behaviors are picking up… I find them very useful, and this is a very nice sample. I wanted to share an example of using attached behaviors in Silverlight to emulate the ICommand interface behavior present in WPF.

    http://blogs.southworks.net/jdominguez/2008/08/icommand-for-silverlight-with-attached-behaviors/

    You may find it useful, as the concepts are exactly the same,

    Julian