Freigeben über


More advanced attached property use: the Ramora pattern

I like to think of the technique I am about to demonstrate as the Ramora pattern - it allows you to attach a chunk of logic to any existing element that you have.

The RadialPanel example showed an example of storing information on an element using attached properties. In that example, we did not need to know when the property changed because we used the appropriate metadata to tell WPF that it affected the arrange part of the parent's layout.

What we will build now is a simple app that shows a bunch of buttons, and we will have the buttons change their background when you have the mouse over them. We will do this by just using the standard Button in WPF.

Do do this, we will create a static class called Hover which will define an attached property. First we define the class:

    static public class Hover

    {

    }

The first thing we will need is an attached property. We will define this one similar to the RadialPanel example, but this time we will have metadata telling WPF to call us whenever the property value changes on any element:

        public static readonly DependencyProperty BrushProperty = DependencyProperty.RegisterAttached(

            "Brush",

            typeof(Brush),

            typeof(Hover),

            new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnHoverBrushChanged)));

        public static Brush GetBrush(DependencyObject obj)

        {

            return (Brush)obj.GetValue(BrushProperty);

        }

        public static void SetBrush(DependencyObject obj, Brush value)

        {

            obj.SetValue(BrushProperty, value);

        }

This is where the real power of the WPF property system comes in. Not only can we attach any data we want to any DependencyObject, but WPF will tell us whenever it changes! We use this callback to do our devious Ramora work:

        private static void OnHoverBrushChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)

        {

            Control control = obj as Control;

            if (control != null)

            {

                // subscribe to mouse enter and leave events on the control

                control.MouseEnter += new MouseEventHandler(OnControlEnter);

                control.MouseLeave += new MouseEventHandler(OnControlLeave);

            }

            else

            {

                Debug.Fail("Hover only works on Control objects!");

            }

        }

As you can see, we use the fact that the Brush has changed as an opportunity to subscribe to the MouseEnter and MouseLeave events. One thing we will need to handle the mouse enter event is a place to store the Background while we override it. We can make another attached property for that:

        public static readonly DependencyProperty OriginalBrushProperty = DependencyProperty.RegisterAttached(

            "OriginalBrush",

            typeof(Brush),

            typeof(Hover));

Now we can simply handle the events. On enter we save the background on the control and set it to the hover brush:

        static void OnControlEnter(object sender, MouseEventArgs e)

        {

            Control control = (Control)e.OriginalSource;

            // remember what brush it had before we changed it

            control.SetValue(OriginalBrushProperty, control.Background);

            // set the background to the value that was set in the property

            control.Background = GetBrush(control);

        }

And on leave we can restore everything:

        static void OnControlLeave(object sender, MouseEventArgs e)

        {

            Control control = (Control)e.OriginalSource;

            // restore the old value

            control.Background = (Brush)control.GetValue(OriginalBrushProperty);

        }

Now the Hover class is done! Now we just need to use it:

<Window x:Class="AttachedBehaviorExample.Window1"

    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:local="clr-namespace:AttachedBehaviorExample"

    Title="AttachedBehaviorExample" Height="300" Width="300"

    >

  <UniformGrid Rows="6" Columns="1">

    <Button Content="Button 1" local:Hover.Brush="Red" />

    <Button Content="Button 2" local:Hover.Brush="Green" />

    <Button Content="Button 3" local:Hover.Brush="Blue" />

    <Button Content="Button 4" local:Hover.Brush="BlanchedAlmond" />

    <Button Content="Button 5" local:Hover.Brush="Thistle" />

    <Button Content="Button 6" local:Hover.Brush="SpringGreen" />

  </UniformGrid>

</Window>

Now the example is finished - if you start it up then you can see that the buttons have fancy colors when you hover over them.

Why do something like this instead of just making a new Button class? Here are a few reasons:

  • You can apply the Hover to anything, not just Buttons. You can apply it to any control.
  • The code for the behavior is all contained in one class - this gives good encapsulation of code.
  • You can make other Ramoras and apply them to your Button objects as well, in any combination. This gives some of the power of multiple inheritance, but none of the mess.

Another way of using this trick is to have the value of the attached property be an instance of the Ramora class. This allows you to make more complex behavior that stores state per attachment, rather than having the stateless design shown above. The key part of the design is the same, though.

AttachedBehaviorExample.zip

Comments

  • Anonymous
    November 08, 2007
    Hi Bencon, did you tryed to let your software work on MS Visual Studio 2008 Version 9.0.20706.1 Beta2. It seems that your software isn't able to compile, I've following errors: Error 1 Assembly 'AttachedBehaviorExample' was not found. The 'clr-namespace' URI refers to an assembly that is not referenced by the project. C:Documents and SettingsasoDesktopAttachedBehaviorExampleWindow1.xaml 4 17 AttachedBehaviorExample Error 2 The attachable property 'Brush' was not found in type 'Hover'. C:Documents and SettingsasoDesktopAttachedBehaviorExampleWindow1.xaml 8 32 AttachedBehaviorExample ... Did you experienced it? Have you any idea how to solve the trouble? Thank you very much in advance for your attention, Alfred

  • Anonymous
    November 15, 2007
    PingBack from http://wangmo.wordpress.com/2007/11/15/attached-properties-spreadsheet/

  • Anonymous
    March 24, 2008
    This looks like a very interesting technique, but how do you prevent memory leaks from the event handlers when the control goes out of scope?

  • Anonymous
    July 14, 2008
    duncan -You can unsubscribe from the events when the control is unloaded if that is an issue. I normally leave these details out of these kinds of examples, because it complicates the example and makes it harder to learn the key concept at hand.

  • Anonymous
    March 24, 2009
    According to Ben Constable, Remora Pattern allows you to attach a chunk of logic to any existing element

  • Anonymous
    October 13, 2009
    Great work! However I need to set this kind of properties from the code-behind instead of in the XAML Any ideas? I would like to do this from the code: <Button Content="Button 1" local:Hover.Brush="Red" /> You can do this by using the SetBrush static method on the Hover class, and passing the instance of the Button as the first argument.

  • Anonymous
    November 15, 2009
    Hi. "You can unsubscribe from the events when the control is unloaded if that is an issue." If you do that with a control that is reparented (like controls in a tab item), your behaviour will stop working after the first reparenting. How do you work around that?