Saturday, July 25, 2015

PointerDownThemeAnimation, PointerUpThemeAnimation & Null Reference Exceptions


WinRT and UWP projects have access to PointerDownThemeAnimation and PointerUpThemeAnimation. These animations provide a consistent visual feedback experience with the OS and other apps. Unfortunately these animations have some specific implementation details and don’t always behave as expected. This is most often true when using them from code behind instead of declaring them in XAML. This usually happens when you’re building a templated control, behaviour or similar reusable component.

The most common issue that occurs is an unexpected null reference exception. To avoid this;

  • Ensure the target element (the one to be animated) has a name. The easiest way is to assign one in XAML at design time. If no name is present at runtime you need to assign one.
  • Ensure you set the TargetName property on the animation. Setting the Target property won’t work. You must set the TargetName property, and that’s why you need a name set on the target.
  • You must add the animation into the logical tree for the target. My suggestion is to add it to the resources collection for the target. If you don’t do this the system has no context to search in so it still won’t find the target. This will also result in a null reference exception.

It's a shame there isn’t a better error message. The inability to assign a target object without a name is also disappointing.

Another issue is the duration property for these animations. You can set it without receiving an error, but the duration won’t change. This is fine as you should be seeking consistency with other apps. It's just sad that this is a silent failure. It is noted on MSDN though.

Below is a draft XAML Behaviour to apply these animations to any visual element. Feel free to use it (Apache 2.0 license). I have Windows 10 UWP code based on this and it should work in WinRT for Windows 8.1 too. In both cases it requires the Behaviours SDK for WinRT

This custom behaviour adds both the animation and a command binding. If specified the bound command will execute after the animation completes. If the command reports as not executable, neither the animation nor the command executes.

   1:      public class BehaviourBase<T> : DependencyObject, IBehavior where T : DependencyObject
   2:      {
   3:   
   4:          private T _AssociatedObject;
   5:   
   6:          public DependencyObject AssociatedObject
   7:          {
   8:              get
   9:              {
  10:                  return _AssociatedObject;
  11:          }
  12:          }
  13:   
  14:          public T TypedAssociatedObject
  15:          {
  16:              get { return _AssociatedObject; }
  17:          }
  18:   
  19:          public void Attach(DependencyObject associatedObject)
  20:          {
  21:              if (associatedObject == null) throw new ArgumentNullException("associatedObject");
  22:   
  23:              _AssociatedObject = (T)associatedObject;
  24:              Attached(_AssociatedObject);
  25:          }
  26:   
  27:          public virtual void Attached(T associatedObject)
  28:          {
  29:          }
  30:   
  31:          public void Detach()
  32:          {
  33:              if (_AssociatedObject != null)
  34:              {
  35:                  Detatching();
  36:                  _AssociatedObject = null;
  37:              }
  38:          }
  39:   
  40:          public virtual void Detatching()
  41:          {
  42:          }
  43:   
  44:      }
  45:   
  46:      public class VisualTapBehavior : BehaviourBase<UIElement>
  47:      {
  48:   
  49:          public static readonly DependencyProperty CommandProperty = DependencyProperty.Register("Command", typeof(ICommand), typeof(VisualTapBehavior), new PropertyMetadata(null));
  50:          public static readonly DependencyProperty CommandParameterProperty = DependencyProperty.Register("CommandParameter", typeof(object), typeof(VisualTapBehavior), new PropertyMetadata(null));
  51:   
  52:          private Storyboard _PointerDownStoryboard;
  53:          private Storyboard _PointerUpStoryboard;
  54:   
  55:          public Storyboard Storyboard { get; internal set; }
  56:   
  57:          public ICommand Command
  58:          {
  59:              get { return (ICommand)this.GetValue(CommandProperty); }
  60:              set
  61:              {
  62:                  this.SetValue(CommandProperty, value);
  63:              }
  64:          }
  65:   
  66:          public object CommandParameter
  67:          {
  68:              get { return this.GetValue(CommandParameterProperty); }
  69:              set
  70:              {
  71:                  this.SetValue(CommandParameterProperty, value);
  72:              }
  73:          }
  74:   
  75:          public override void Attached(UIElement associatedObject)
  76:          {
  77:              base.Attached(associatedObject);
  78:   
  79:              //Stupidly, these theme animations require the object to have a name :(
  80:              var fe = ((FrameworkElement)associatedObject);
  81:              var name = fe.Name;
  82:              if (String.IsNullOrEmpty(name))
  83:              {
  84:                  name = Guid.NewGuid().ToString();
  85:                  fe.Name = name;
  86:              }
  87:   
  88:              if (fe.Resources.ContainsKey("VisualTapDownAnimation"))
  89:                  _PointerDownStoryboard = (Storyboard)fe.Resources["VisualTapDownAnimation"];
  90:              else
  91:              {
  92:                  var pointerDownStoryboard = new Storyboard();
  93:                  var downAnimation = new PointerDownThemeAnimation();
  94:                  Storyboard.SetTargetName(downAnimation, name);
  95:                  pointerDownStoryboard.Children.Add(downAnimation);
  96:                  _PointerDownStoryboard = pointerDownStoryboard;
  97:                  fe.Resources.Add(new KeyValuePair<object, object>("VisualTapDownAnimation", pointerDownStoryboard));
  98:              }
  99:   
 100:              if (fe.Resources.ContainsKey("VisualTapUpAnimation"))
 101:                  _PointerUpStoryboard = (Storyboard)fe.Resources["VisualTapUpAnimation"];
 102:              else
 103:              {
 104:                  var pointerUpStoryboard = new Storyboard();
 105:                  var upAnimation = new PointerUpThemeAnimation();
 106:                  Storyboard.SetTargetName(upAnimation, name);
 107:                  pointerUpStoryboard.Children.Add(upAnimation);
 108:                  pointerUpStoryboard.Completed += PointerUpStoryboard_Completed;
 109:                  _PointerUpStoryboard = pointerUpStoryboard;
 110:                  ((FrameworkElement)associatedObject).Resources.Add(new KeyValuePair<object, object>("VisualTapUpAnimation", pointerUpStoryboard));
 111:              }
 112:   
 113:              associatedObject.PointerPressed += AssociatedObject_PointerPressed;
 114:              associatedObject.PointerReleased += AssociatedObject_PointerReleased;
 115:          }
 116:   
 117:          private async void PointerUpStoryboard_Completed(object sender, object e)
 118:          {
 119:              await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal,
 120:                  () =>
 121:                  {
 122:                      var command = this.Command;
 123:                      var commandParameter = this.CommandParameter;
 124:                      if (command != null && command.CanExecute(commandParameter))
 125:                          command.Execute(commandParameter);
 126:                  });
 127:          }
 128:   
 129:          public override void Detatching()
 130:          {
 131:              base.Detatching();
 132:   
 133:              TypedAssociatedObject.PointerPressed -= this.AssociatedObject_PointerPressed;
 134:              TypedAssociatedObject.PointerReleased -= this.AssociatedObject_PointerReleased;
 135:   
 136:              _PointerDownStoryboard = null;
 137:          }
 138:   
 139:          private void AssociatedObject_PointerPressed(object sender, Windows.UI.Xaml.Input.PointerRoutedEventArgs e)
 140:          {
 141:              ((FrameworkElement)sender).CapturePointer(e.Pointer);
 142:   
 143:              var command = this.Command;
 144:              var commandParameter = this.CommandParameter;
 145:              if (command == null || command.CanExecute(CommandParameter))
 146:                  RunStoryboardIfNotNull(_PointerDownStoryboard);
 147:          }
 148:   
 149:          private void AssociatedObject_PointerReleased(object sender, Windows.UI.Xaml.Input.PointerRoutedEventArgs e)
 150:          {
 151:              ((FrameworkElement)sender).ReleasePointerCapture(e.Pointer);
 152:              RunStoryboardIfNotNull(_PointerUpStoryboard);
 153:          }
 154:   
 155:          private void RunStoryboardIfNotNull(Storyboard storyboard)
 156:          {
 157:              if (storyboard != null)
 158:              {
 159:                  storyboard.Stop();
 160:                  storyboard.Begin();
 161:              }
 162:          }
 163:      }

No comments:

Post a Comment