25 February 2011

Using attached dependency properties to toggle an Application Bar Icon from viewmodel

Application Bar Icon buttons are a look-and-feel-wise very consistent and easy to understand way for every Windows Phone 7 application to give it’s users access to the most important functions. The drawback of this thing is that it’s not a real FrameworkElement, thus you cannot bind commands and other stuff to it, but as I described earlier, this has been solved by the BindableApplicationBar.

But even the BindableApplicationBar has its issues. The IconUri attribute of a BindableApplicationBarIconButton cannot be bound to – for it is no Dependecy Property. Now you can fix that, since Nicolas Humann includes the source code of his excellent solution. But I prefer to threat someone else’s code as third party component, and solved the problem using a very powerful way of extending of Silverlight/Windows Phone 7: attached dependency properties.

The problem I wanted to solve is this: I have changed my MapMania App in such a way that it now tracks your location continually if you press the “Satellite” button. But I want the user to be able to stop tracking as well, using the same button. So what used to be a simple activation button, is now a toggle button. When the user is tracking his own phone, I want the button image to change from the image of of a satellite to an image of a satellite with a big cross through it, indicating “if you press me again, I will stop tracking”. And I wanted this, of course, to be steered from the viewmodel.

This is how I solved it, with three attached dependency properties:

using System;
using System.Windows;
using Phone7.Fx.Preview;

namespace LocalJoost.Utilities
{
  /// <summary>
  /// Supports toggle of a BindableApplicationBarIconButton's icon
  /// </summary>
  public static class AppBarIconFlipper
  {
    #region IconUri
    public static readonly DependencyProperty IconUriProperty =
     DependencyProperty.RegisterAttached("IconUri",
     typeof(Uri),
     typeof(AppBarIconFlipper),
     new PropertyMetadata(IconUriPropertyChanged));

    // Called when Property is retrieved
    public static Uri GetIconUri(DependencyObject obj)
    {
      return obj.GetValue(IconUriProperty) as Uri;
    }

    // Called when Property is set
    public static void SetIconUri(
      DependencyObject obj,
      Uri value)
    {
      obj.SetValue(IconUriProperty, value);
    }

    // Called when property is changed
    private static void IconUriPropertyChanged(
     object sender,
     DependencyPropertyChangedEventArgs args)
    {
      var attachedObject = sender as BindableApplicationBarIconButton;
      if (attachedObject == null) return;
      attachedObject.IconUri = (bool)attachedObject.GetValue(ShowAlernateIconUriProperty)
                     ? (Uri)attachedObject.GetValue(AlernateIconUriProperty)
                     : (Uri)args.NewValue;
    }
    #endregion

    #region AlernateIconUri
    public static readonly DependencyProperty AlernateIconUriProperty =
     DependencyProperty.RegisterAttached("AlernateIconUri",
     typeof(Uri),
     typeof(AppBarIconFlipper),
     new PropertyMetadata(AlernateIconUriPropertyChanged));

    // Called when Property is retrieved
    public static Uri GetAlernateIconUri(DependencyObject obj)
    {
      return obj.GetValue(AlernateIconUriProperty) as Uri;
    }

    public static void SetAlernateIconUri(
      DependencyObject obj,
      Uri value)
    {
      obj.SetValue(AlernateIconUriProperty, value);
    }

    private static void AlernateIconUriPropertyChanged(
     object sender,
     DependencyPropertyChangedEventArgs args)
    {
      var attachedObject = sender as BindableApplicationBarIconButton;
      if (attachedObject == null) return;
      attachedObject.IconUri = (bool)attachedObject.GetValue(ShowAlernateIconUriProperty)
                     ? (Uri)args.NewValue
                     : (Uri)attachedObject.GetValue(IconUriProperty);
    }
    #endregion

    #region ShowAlernateIconUri
    public static readonly DependencyProperty ShowAlernateIconUriProperty =
     DependencyProperty.RegisterAttached("ShowAlernateIconUri",
     typeof(bool),
     typeof(AppBarIconFlipper),
     new PropertyMetadata(ShowAlernateIconUriPropertyChanged));

    public static bool GetShowAlernateIconUri(DependencyObject obj)
    {
      return (bool)obj.GetValue(ShowAlernateIconUriProperty);
    }

    public static void SetShowAlernateIconUri(
      DependencyObject obj,
      bool value)
    {
      obj.SetValue(ShowAlernateIconUriProperty, value);
    }

    private static void ShowAlernateIconUriPropertyChanged(
     object sender,
     DependencyPropertyChangedEventArgs args)
    {
      var attachedObject = sender as BindableApplicationBarIconButton;
      if (attachedObject == null) return;
      var value = (bool)args.NewValue;
      attachedObject.IconUri = value
                     ? (Uri)attachedObject.GetValue(AlernateIconUriProperty)
                     : (Uri)attachedObject.GetValue(IconUriProperty);
    }
    #endregion
  }
}
The only interesting code is in the “***Changed” methods, the rest is just the necessary plumbing. Anyway, in stead of setting the IconUri of the BindableApplicationBarIconButton directly, you use the attached dependency properties like this:
<Phone7Fx:BindableApplicationBarIconButton 
 Command="{Binding ShowLocation}" Text="Track" 
 LocalJoostUtils:AppBarIconFlipper.IconUri="/icons/gps.png"
 LocalJoostUtils:AppBarIconFlipper.AlernateIconUri="/icons/gps_stop.png"
 LocalJoostUtils:AppBarIconFlipper.ShowAlernateIconUri="{Binding IsTracking, Mode=TwoWay}"/>

and depending on the value of the Viewmodel property IsTracking the icon will flip from gps.png (false) to gps_stop (true) – or back.

Once again it shows Microsoft’s XAML-based platforms are as flexible as a rubber band, and wherever are holes in binding, you can almost always plug them using attached dependency properties – the super glue of MVVM, IMHO.

No comments: