26 November 2011

Safe event detachment base class for Windows Phone 7 behaviors

Some time ago I blogged about the Safe Event Detachment ‘pattern’ for behaviors and even included a snippet that made implementing this pattern easier. When I started to use this pattern more often myself I observed quite some code duplication appearing and I don’t like that. What’s more – although the pattern works well under Silverlight and WPF, there are some unique situations in Windows Phone 7 that need some extra attention – particularly the situation in which the user is navigating back to a page containing such a behavior. For when the user is navigation from the page, the AssociatedObject’s Unloaded event fires and the behavior is de-activated. If the user then moves back to the page – the AssociatedObject’s OnAttached event is not fired and the behavior is not re-initialized.

So I set out to create a base class taking care of most of this initalization hooplah without bothering the developer too much. This turned out to be not so simple as I thought. But anyway – thinks worked out. The initial setup of the base class is like this:
using System;
using System.Windows;
using System.Windows.Interactivity;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;

namespace Wp7nl.Behaviors
{
  /// <summary>
  /// A base class implementing the safe event detachment pattern for behaviors.
  /// Optional re-init after page back navigation.
  /// </summary>
  /// <typeparam name="T">The framework element type this behavior attaches to</typeparam>
  public abstract class SafeBehavior<T> : Behavior<T> where T : FrameworkElement
  {
    protected SafeBehavior()
    {
      IsCleanedUp = true;
    }

    /// <summary>
    ///Setting this value to true in the constructor makes the behavior
    ///re-init after a page back event.
    /// </summary>
    protected bool ListenToPageBackEvent { get; set; }

    /// <summary>
    /// The page this behavior is on
    /// </summary>
    protected PhoneApplicationFrame ParentPage;

    /// <summary>
    /// The uri of the page this behavior is on
    /// </summary>
    private Uri pageSource;

    protected override void OnAttached()
    {
      base.OnAttached();
      InitBehavior();
    }

    /// <summary>
    /// Does the initial wiring of events
    /// </summary>
    protected void InitBehavior()
    {
      if (IsCleanedUp)
      {
        IsCleanedUp = false;
        AssociatedObject.Loaded += AssociatedObjectLoaded;
        AssociatedObject.Unloaded += AssociatedObjectUnloaded;
      }
    }
  }
}

The comments already give away which direction this is going to take: the behavior keeps track of the page it’s on and that page’s uri to track if the user is navigating back to this page. If you don’t want this behavior, do nothing. If you need to track the user navigating back to the page (and believe me, in Windows Phone 7 you want that in most of the cases), set ListenToPageBackEvent to true in the behavior’s constructor. The setting up of this tracking is done in the next method:

/// <summary>
/// Does further event wiring and initialization after load
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void AssociatedObjectLoaded(object sender, RoutedEventArgs e)
{
  // Find the page this control is on and listen to its orientation changed events
  if (ParentPage == null && ListenToPageBackEvent)
  {
    ParentPage = Application.Current.RootVisual as PhoneApplicationFrame;
    pageSource = ParentPage.CurrentSource;
    ParentPage.Navigated += ParentPageNavigated;
  }
  OnSetup();
}

/// <summary>
/// Fired whe page navigation happens
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ParentPageNavigated(object sender, NavigationEventArgs e)
{
  // Re-setup when this page is navigated BACK to
  if (IsNavigatingBackToBehaviorPage(e))
  {
    if (IsCleanedUp)
    {
      InitBehavior();
    }
  }
  OnParentPageNavigated(sender, e);
}

/// <summary>
/// Checks if the back navigation navigates back to the page
/// on which this behavior is on
/// </summary>
/// <param name="e"></param>
/// <returns></returns>
protected bool IsNavigatingBackToBehaviorPage(NavigationEventArgs e)
{
  return (e.NavigationMode == NavigationMode.Back && e.Uri.Equals(pageSource));
}

Now if you have set ListenToPageBackEvent to true, it keeps the root visual (i.e. the page on which the behavior is plonked) in ParentPage, it’s uri in pageSouce and attaches an listener to the ParentPageNavigated event of this page. Now if a navigation event happens, the IsNavigatingBackToBehaviorPage checks by comparing uri’s if the user is actually navigating back to this page.

All very interesting, but the most important is: there are two methods OnSetup and OnParentPageNavigated in this class which are called. They are basically emtpy and form your hook points into this:

/// <summary>
/// Override this to add your re-init
/// </summary>    
protected virtual void OnParentPageNavigated(object sender, NavigationEventArgs e)
{     
}

/// <summary>
/// Override this to add your own setup
/// </summary>
protected virtual void OnSetup()
{
}

So far for the setup stuff: the cleanup stuff is a lot simpler:

protected bool IsCleanedUp { get; private set; }

/// <summary>
/// Executes at OnDetaching or OnUnloaded (usually the last)
/// </summary>
private void Cleanup()
{
  if (!IsCleanedUp)
  {
    AssociatedObject.Loaded -= AssociatedObjectLoaded;
    AssociatedObject.Unloaded -= AssociatedObjectUnloaded;
    OnCleanup();
    IsCleanedUp = true;
  }
}

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

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

/// <summary>
/// Override this to add your own cleanup
/// </summary>
protected virtual void OnCleanup()
{
}

And once again you see a simple virtual Cleanup you can override.

I realize this is all very theoretical and technical, and the question you probably have now is – so what is this for and how do you use it? The usage is simple:

  • You create a MyBehavior<T> that descends from SafeBehavior<T>
  • If you want your behavior to re-init when the user navigates back set ListenToPageBackEvent  to true in the MyBehavior constructor. But beware. By its very nature the Navigated event is not detached. So basically you are leaking memory. Therefore, if you make a lot of behaviors, like in a game, don’t ever set ListenToPageBackEvent to true.Use with care and moderation.
  • You do setting up events in an override of OnSetup
  • You do cleaning up of events in an override of OnCleanup
  • And if you want to do something extra when the user is navigating back to the page do that in an override of OnParentPageNavigated.

This makes implementing the Safe Event Detachment ‘pattern‘ way more easy. This new behavior base is now included in my wp7nl library on codeplex and the source can be found here. A sample usage of this base class can be found here.

No comments: