20 April 2011

Safe event detachment ‘pattern’ for behaviors

For the past few weeks I’ve been hacking away at my Windows Phone 7 game and noticed something odd. The objective is to intercept moving objects on the screen and drag/flick them to a target – but as the levels progressed interception and movement became ever more difficult and slow. At least that was reported by my #wp7nl fellow members who were kind (or crazy) enough to test drive the game into the higher levels. I won’t mention names here – yet ;-).

I followed the usual pattern for a behavior – at least, I think it is the usual pattern

using System.Windows;
using System.Windows.Interactivity;

namespace SomeNamespace
{
  public class DemoBehavior : Behavior<FrameworkElement>
  {
    protected override void OnAttached()
    {
      base.OnAttached();
      AssociatedObject.Loaded += AssociatedObjectLoaded;
    }

    void AssociatedObjectLoaded(object sender, RoutedEventArgs e)
    {
      // Hook up a bunch of events to the AssociatedObject
    }

    protected override void OnDetaching()
    {
      AssociatedObject.Loaded -= AssociatedObjectLoaded;
      // Unhook the other events from the AssociatedObject
      base.OnDetaching();
    }
  }
}

I had seen this problem before while programming in WPF and indeed – when I put a breakpoint in the first line of the OnDetaching  override it was never called. Apparently Windows Phone 7 has the same problem. So here was my game, happily creating a lot of moving objects with each 1-2 behaviors attached to it listening to a bunch of events. They were never unhooked when the objects were deleted. So within a few levels the game started eating resources like there’s no tomorrow.

I modified my behaviors to try to do a general cleanup not only when OnDetaching is called but also when the AssociatedObject is unloaded. This gave me a pattern which I think might be beneficial to everyone who is working with behaviors in general:

using System.Windows;
using System.Windows.Interactivity;

namespace SomeNamespace
{
  public class BetterDemoBehavior : Behavior<FrameworkElement>
  {
    #region Setup
    protected override void OnAttached()
    {
      base.OnAttached();
      AssociatedObject.Loaded += AssociatedObjectLoaded;
      AssociatedObject.Unloaded += AssociatedObjectUnloaded;
    }

    void AssociatedObjectLoaded(object sender, RoutedEventArgs e)
    {
      // Hook up a bunch of events to the AssociatedObject
    }
    #endregion

    #region Cleanup
    private bool _isCleanedUp;

    private void Cleanup()
    {
      if (!_isCleanedUp)
      {
        _isCleanedUp = true;
        AssociatedObject.Loaded -= AssociatedObjectLoaded;
        AssociatedObject.Unloaded -= AssociatedObjectUnloaded;
        // Unhook the other events from the AssociatedObject
      }
    }

    protected override void OnDetaching()
    {
      Cleanup();
      base.OnDetaching();
    }

    void AssociatedObjectUnloaded(object sender, RoutedEventArgs e)
    {
      Cleanup();
    }
    #endregion
  }
}

And because I am not very partial to repetitive work, I created a snippet for this piece of code.

I guess that if both and Windows Phone 7 and WPF have this issue, Silverlight will have it as well. I hope at least this will help people who are, like me, stubborn enough to use Silverlight and MVVMLight to develop games for Windows Phone 7 ;-)

8 comments:

Wim Bokkers said...

Nice solution. I wonder if you couldn't just use weak references instead.

Loc#alJoost said...

Ik wacht met spanning of het voorbeeld van het gebruik van weak references op *jouw* blog ;-)

Chris said...

This is interesting. Thanks.
However I've found out something fishy with this, namely:

Imagine scenario;

I have MainWindow, where I have Button which triggers PopupControl.

Inside PopupControl I have ListBox with items. Now I've attached behavior to each ListBoxItem.

As soon as the Popup closes, the OnUnloaded is called for behaviors. And the behavior is detached. Pretty neat right?

Yes. Besides the fact that the behavior wont be re-attached, when opening Popup again.

Currently I've made IgnoreUnLoadedEvent property to BehaviorBase. For a case like this.

But maybe you have ideas?

Joost van Schaik said...

@chris ouch. You have found an unsupported scenario - something I apparently didn't anticipate. I am open to suggestions ��

Unknown said...

I stumbled on the same scenario, though not with a popup menu but with user controls - upon reloading the control the behavior is not attached. So, any ideas so far?

Joost van Schaik said...

@unknown can you be more specific? Send sample code, for instance?

Omar Alani said...

I've used your idea and creates this Base class, which do the cleaning for you and you just to implement the hook and unhook methods.

public abstract class BehaviorBase : Behavior where T : FrameworkElement
{
#region Setup

protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.Loaded += AssociatedObjectLoaded;
AssociatedObject.Unloaded += AssociatedObjectUnloaded;
}

private void AssociatedObjectLoaded(object sender, RoutedEventArgs e)
{
OnAttachedObjectLoaded();

// Hook up a bunch of events to the AssociatedObject
OnHookEvents();
}

#endregion

#region Cleanup

private bool _isCleanedUp;

private void Cleanup()
{
if (!_isCleanedUp)
{
_isCleanedUp = true;
AssociatedObject.Loaded -= AssociatedObjectLoaded;
AssociatedObject.Unloaded -= AssociatedObjectUnloaded;

// Unhook the other events from the AssociatedObject
OnUnHookEvents();
}
}

protected override void OnDetaching()
{
Cleanup();

base.OnDetaching();
}

void AssociatedObjectUnloaded(object sender, RoutedEventArgs e)
{
OnAttachedObjectUnLoaded();

Cleanup();
}

protected virtual void OnAttachedObjectLoaded()
{
// if the derived class wants to do something when the AssociatedObject load.
}

protected virtual void OnAttachedObjectUnLoaded()
{
// if the derived class wants to do something when the AssociatedObject unload.
}

protected abstract void OnHookEvents();

protected abstract void OnUnHookEvents();

#endregion
}

you may find it useful.

Joost van Schaik said...

@Omar,

Thanks for thinking with me. But the SafeBehavior base class has been sitting in the Win7nl for some time. Have a look here http://wp7nl.codeplex.com/SourceControl/changeset/view/20956#214132