29 March 2012

Attached behaviors for Windows 8 Metro Style XAML

This post was updated substantially at March 31, 2012

Regular readers of my blog know that there are some recurring themes: MVVM, maps and behaviors. I am a big fan of using behaviors ever since I learned how to use this rooting trough the sources of MVVMLight. When I saw the BUILD videos I was elated. I saw a great merger of the best things of Windows and  Windows Phone 7 styles and I knew I was going to get on board too. Five months later, I found myself being an MVP and on the Microsoft Campus of all places, and got a pretty unpleasant surprise: a lady presenting the new Expression Blend version said there would be no behaviors in Windows 8 Metro Style XAML. I was quite disappointed at the time. I still think it’s is quite an omission, but then again, when it’s not your deadline it’s always easy to criticize others.

And then for some reason, this week, I remembered a single line from a presentation by Laurent Bugnion on the 2012 Microsoft Techdays in The Hague. “You can’t use behaviors but you can use attached dependency properties”. It kept reverbing trough my brain for a few moments.

“Use the Force, Luke” ;-)

And the result is this. It’s crude, it’s clumsy, it has no Blend support, but it works, more or less – I have been able to port my DragFlickBehavior to Windows 8 and it bloody works, too. This blog post will be split in two parts: in this part, I will show how to make a behavior in Windows 8 XAML in general, and in a next one I will specifically show the DragFlickBehavior itself.

First of all, the behavior class itself:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.UI.Xaml;

namespace WinRtBehaviors
{
  public abstract class Behavior : DependencyObject
  {
    private FrameworkElement associatedObject;
    public FrameworkElement AssociatedObject
    {
      get
      {
        return associatedObject;
      }
      set
      {
        if (associatedObject != null)
        {
          OnDetaching();
        }
        associatedObject = value;
        if (associatedObject != null)
        {
          OnAttached();
        }
      }
    }

    protected virtual void OnAttached()
    {
      AssociatedObject.Unloaded += AssociatedObjectUnloaded;
    }

    protected virtual void OnDetaching()
    {
      AssociatedObject.Unloaded -= AssociatedObjectUnloaded;
    }

    void AssociatedObjectUnloaded(object sender, RoutedEventArgs e)
    {
      OnDetaching();
    }
  }
}

This is partially ‘borrowed’ from the Windows Phone System.Windows.Interactivity.dll, courtesy of Reflector.  I don’t have a real ‘detached’ event so I’ve decided to call the “OnDetaching’ method when the FrameworkElement is unloaded. Gotta use what’s available, right? The next class, which is the typed version of AttachedBehavior, is also courtesy of Reflector:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.UI.Xaml;

namespace WinRtBehaviors
{
  public abstract class Behavior<T> : Behavior 
    where T : FrameworkElement
  {
    protected Behavior()
    {
    }

    public T AssociatedObject
    {
      get
      {
        return (T)base.AssociatedObject;
      }
      set
      {
        base.AssociatedObject = value;
      }
    }
  }
}

I’ve closely followed naming conventions as used in Windows Phone and Silverlight, but I took a different root namespace “WinRtBehaviors”. Should the Windows 8 team decide to add behaviors to the API in the future, removing this classes and changing the namespaces should do the trick

Finally there is this pretty crazy piece of code, which is basically a giant Attached Dependency property. This connects the ‘behaviors’ to the FrameworkElements:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.UI.Xaml;
using Windows.ApplicationModel;

namespace WinRtBehaviors
{
  /// <summary>
  /// Attached dependency property storing 'behaviors'
  /// </summary>
  public static class Interaction
  {
    public static readonly DependencyProperty BehaviorsProperty =
       DependencyProperty.RegisterAttached("Behaviors",
       typeof(ObservableCollection<Behavior>),
       typeof(Interaction),
       new PropertyMetadata(
         DesignMode.DesignModeEnabled ? new ObservableCollection<Behavior>() : null,         
       BehaviorsChanged));


    /// <summary>
    /// Called when Property is retrieved
    /// </summary>
    /// <param name="obj"></param>
    /// <returns></returns>
    public static ObservableCollection<Behavior> GetBehaviors(DependencyObject obj)
    {
      var associatedObject = obj as FrameworkElement;
      var behaviors = obj.GetValue(BehaviorsProperty) as ObservableCollection<Behavior>;
      if (behaviors == null)
      {
        behaviors = new ObservableCollection<Behavior>();
        SetBehaviors(obj, behaviors);
      }

      return behaviors;
    }

    /// <summary>
    /// Called when Property is retrieved
    /// </summary>
    /// <param name="obj"></param>
    /// <param name="value"></param>
    public static void SetBehaviors(
       DependencyObject obj,
       ObservableCollection<Behavior> value)
    {
      obj.SetValue(BehaviorsProperty, value);
    }

    /// <summary>
    /// Called when the property changes
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="args"></param>
    private static void BehaviorsChanged(
     object sender,
     DependencyPropertyChangedEventArgs args)
    {
      var associatedObject = sender as FrameworkElement;
      if (associatedObject != null)
      {
        var oldList = args.OldValue as ObservableCollection<Behavior>;
        if (oldList != null)
        {
          foreach (var behavior in oldList)
          {
            behavior.AssociatedObject = null;
          }
        }

        var newList = args.NewValue as ObservableCollection<Behavior>;
        if (newList != null)
        {
          foreach (var behavior in newList)
          {
            behavior.AssociatedObject = sender as FrameworkElement;
          }
          newList.CollectionChanged += (collectionSender, collectionArgs) =>
          {
            switch (collectionArgs.Action)
            {
              case NotifyCollectionChangedAction.Add:
                {
                  foreach (Behavior behavior in collectionArgs.NewItems)
                  {
                    behavior.AssociatedObject = associatedObject;
                  }
                  break;
                }
              case NotifyCollectionChangedAction.Reset:
              case NotifyCollectionChangedAction.Remove:
                {
                  foreach (Behavior behavior in collectionArgs.NewItems)
                  {
                    behavior.AssociatedObject = null;
                  }
                  break;
                }
            }
          };
        }
      }
    }
  }
}

So what do we have here? On top, a pretty standard way of registering an attached dependency property – an ObservableCollection of Behavior. Notice the fact the initial value is null in runtime, but an empty collection in design time. This is because of the next part, the mandatory GetBehaviors method. This is normally ‘just a getter’, but it checks if the collection is null first. And then something interesting happens:

  • If it is null, it creates a new empty collection and initializes the attached dependency property itself with it.
  • That, in turn, fires BehaviorsChanged
  • BehaviorsChanged attaches an internal anonymous method to the ObservableCollectionChanged event of the behavior collection.
  • That anonymous method basically rams the FrameworkElement to which this ObservableCollection is attached in the AssociatedObject property of every new behavior that’s added the list.
  • This will fire the overrideable OnAttached method in the bavhior and boom – your behavior is ready to go.

The SetBehaviors method then is pretty standard. The basic pattern of a behavior is then something like this:

namespace Win8nl.Behaviors
{
  public class DragFlickBehavior : AttachedBehavior<FrameworkElement>
  {
    protected override void OnAttached()
    {
      // Do something
      base.OnAttached();
    }
    protected override void OnDetaching()
    {
      // Do something
      base.OnDetaching();
    }
  }
}

Which, not entirely by accident, looks quite a lot like an behavior looks in Windows Phone or Silverlight. And you call it in XAML like this:

<Page
  x:Class="Catchit8.BlankPage"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="using:Catchit8"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  xmlns:Win8nl_Behaviors="using:Win8nl.Behaviors"
  xmlns:WinRtBehaviors="using:WinRtBehaviors"
  mc:Ignorable="d">

  <Grid Background="{StaticResource ApplicationPageBackgroundBrush}">
    <TextBlock HorizontalAlignment="Left" Margin="503,213,0,0" TextWrapping="Wrap" 
   VerticalAlignment="Top" FontSize="18" Text="Drag me">
      <WinRtBehaviors:Interaction.Behaviors>
         <Win8nl_Behaviors:DragFlickBehavior BrakeSpeed ="5"/>
      </WinRtBehaviors:Interaction.Behaviors>
    </TextBlock>
    <Button Content="Drag me too!" HorizontalAlignment="Left" Margin="315,269,0,0" 
   VerticalAlignment="Top" >
      <WinRtBehaviors:Interaction.Behaviors>
          <Win8nl_Behaviors:DragFlickBehavior BrakeSpeed ="5"/>
      </WinRtBehaviors:Interaction.Behaviors>
     </Button>

  </Grid>
</Page>

Like I said: crude, clumsy and no Blend support. There are a few issues with it. The anonymous method doing all the work, is never detached. I wonder how much memory leaks this will produce. But at least I can move forward now porting a lot of stuff I made for Windows Phone to Windows 8. Unfortunately, contrary to what I hoped, data binding to dependency properties of the behavior itself does not seem to work yet (thanks to Filip Skakun for pointing that out in a reaction to that this post) [It does now, see below]. I hope people smarter than me can improve this to a possible better solution. I will soon post a demo solution with the DragFlickBehavior in working condition in it, after I have traced back how I got it working it the first place.

In the mean time, I’ve started an CodePlex project that will be the home of this stuff. I was initially planning of including it in my Win8nl CodePlex library (coming soon) but after careful consideration and advice, I decided to make a separate library. After all, if people would like to go ahead and expand this, adding triggers and whatnot, it would probably interfere with my own ‘hobby’ library and vice versa.

So Metro, meet behaviors, at “WinRtBehaviors”.

Very much thanks to Geert van Horrik for the suggestions that led to the improvement of my first version.  He is as of now registered as a developer on WinRtBehaviors.

Update 04-04-2012 The WinRtBehaviors library is adapted: a) it no longer introduces memory leaks as described, (thanks to Geert van Horrik), and b) Filip Skakun provided me with code to actually enable data binding.

9 comments:

xyzzer said...

I am wondering - you are saying you should be able to data bind to your behaviors, but do you ever set their DataContext to inherit it from the object you are attaching the behaviors to? I think when they get attached - you should be binding their DataContext to the DataContext of the host object. Then again - I am not sure if you will continue to be able to set their DataContext explicitly, but I would assume that is of less importance...

Joost van Schaik said...

@xyzzer you are right. I assumed a bit too much. Data binding to the dependency properties of the behavior does not seem to work. I fear I will have to resort to attached dependency properties till I found out what I need to do to get this working.

xyzzer said...

I think it might not be much difference between using Attached DPs or your version of behaviors.

I tried to port reverse engineered versions of these myself and gave up after I found myself drowning in layers of internal implementations doing some things not available in WinRT.

Anyways - if you just do something like this when you set your AssociatedObject in the Behavior - it should start working:

this.SetBinding(DataContextProperty, new Binding { Path = new PropertyPath("DataContext"), Source = associatedObject });

wilco_ said...

Hi,
I'm trying to attach your DragFlickBehavior to a button.

But I only get this error message:

Failed to assign to property 'WinRtBehaviors.Interaction.Behaviors'

Any idea?

Joost van Schaik said...

@wilco_ unfortunately not unless you give me some more code. Have you been able to run the sample?

Anargyros Tomaras said...

Use WeakEventListener if possible to avoid the MemLeak or closure a WeakRef inside the lambda instead of implicitly closuring the instance.

Also i dont think it is correct to detach on Unloaded.An element can be removed and readded to the UI and then your behaviours will stop working.Detach and Attach are called when the Behaviour is *added* or *removed* from the collection.

Joost van Schaik said...

@Anargyros You might want to check out the current state of affairs of WinRTBehaviors. It indeed uses weak references now. Have a look at the current source code and if you have any suggestions to made, please feel free to e-mail me. I might even add you to the dev crew if you like ;-)

Herman van der Blom said...

I tried to use this source and other source to compile native for Windows Phone 8. The compilation goes well only if I use it in an XAML file the Interactivity class is not recognized. Any idea why? May be the referenced Phone8 dll's are not compatible with this, I just gave it a try but it failed.

Joost van Schaik said...

@Herman I am afraid I don't get it. Why would you want to try this on Windows Phone 8? The Windows Phone XAML stack descends from Silverlight and has EventToCommand included. You don't need my behavior at all!