Friday, May 4, 2012

Silverlight Double Animation Helper

Often we need to quickly animate something on the screen without initializing storyboards, animations, setting targets etc.

Also sometimes we need to be able to change animation direction or target value.

All this is about double animation.

Suppose we need to move a scroll viewer vertical offset with some speed but depending on user actions we may change speed and even direction of scrolling.

So I wrote a helper class named DoubleAnimationHelper.

You can use it as simple as:

image

Now if while this animation is in progress you call this code with different offset – the same instance of DoubleAnimationHelper class will be returned by Get factory method – and the previous animation will be graciously changed to new one (it will have the effect of continuing from current point).

Here is the code

public class DoubleAnimationHelper : DependencyObject
{
    #region Class level variables

    private DependencyObject mOwner;
    private DependencyProperty mProperty;
    private Storyboard mStoryboard = new Storyboard();
    private DoubleAnimation mAnimation = new DoubleAnimation();
    private Action mCallback = null;
    private bool mInitialized = false;

    #endregion

    #region Constructors

    private DoubleAnimationHelper(DependencyObject owner, DependencyProperty property)
    {
        mOwner = owner;
        mProperty = property;

        IsManual = true;

        CurrentValue = TargetValue = (double)owner.GetValue(property);

        mAnimation.AutoReverse = false;

        Storyboard.SetTargetProperty(mAnimation, new PropertyPath("CurrentValue"));
        Storyboard.SetTarget(mAnimation, this);

        mStoryboard.Children.Add(mAnimation);
        mStoryboard.Completed += mStoryboard_Completed;

        mInitialized = true;
    }

    #endregion

    #region FactoryMethods

    public static DoubleAnimationHelper Get(DependencyObject owner, DependencyProperty property)
    {
        var dictionary = GetAnimationHelper(owner);
        if (dictionary == null)
        {
            dictionary = new Dictionary<DependencyProperty, DoubleAnimationHelper>();
            SetAnimationHelper(owner, dictionary);
        }
        if (!dictionary.ContainsKey(property))
        {
            dictionary.Add(property, new DoubleAnimationHelper(owner, property));
        }
        return dictionary[property];
    }

    #endregion

    #region Properties

    public bool IsManual { get; set; }

    #endregion

    #region Event handlers

    private void mStoryboard_Completed(object sender, EventArgs e)
    {
        if (mCallback != null)
        {
            mCallback();
            mCallback = null;
        }
    }

    #endregion

    #region Methods

    public void Animate()
    {
        Animate(null);
    }

    public void Animate(Action callback)
    {
        if (mInitialized)
        {
            mStoryboard.Stop();

            mAnimation.From = CurrentValue;
            mAnimation.To = TargetValue;
            mAnimation.Duration = Duration;
            mAnimation.EasingFunction = EasingFunction;
            mCallback = callback;

            mStoryboard.Begin();
        }
    }

    public void Stop()
    {
        if (mInitialized)
        {
            mStoryboard.Pause();
        }
    }

    #endregion

    #region Overrides

    #endregion

    #region Dependency properties

    #region AnimationHelper

    internal static Dictionary<DependencyProperty, DoubleAnimationHelper> GetAnimationHelper(DependencyObject element)
    {
        if (element == null)
        {
            throw new ArgumentNullException("element");
        }
        return (Dictionary<DependencyProperty, DoubleAnimationHelper>)element.GetValue(AnimationHelperProperty);
    }

    internal static void SetAnimationHelper(DependencyObject element, Dictionary<DependencyProperty, DoubleAnimationHelper> value)
    {
        if (element == null)
        {
            throw new ArgumentNullException("element");
        }
        element.SetValue(AnimationHelperProperty, value);
    }

    internal static readonly DependencyProperty AnimationHelperProperty =
        DependencyProperty.RegisterAttached(
            "AnimationHelper",
            typeof(Dictionary<DependencyProperty, DoubleAnimationHelper>),
            typeof(DoubleAnimationHelper),
            new PropertyMetadata(null, OnAnimationHelperPropertyChanged));

    private static void OnAnimationHelperPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
            
    }

    #endregion

    #region TargetValue

    public double TargetValue
    {
        get { return (double)GetValue(TargetValueProperty); }
        set { SetValue(TargetValueProperty, value); }
    }

    public static readonly DependencyProperty TargetValueProperty =
        DependencyProperty.Register(
            "TargetValue",
            typeof(double),
            typeof(DoubleAnimationHelper),
            new PropertyMetadata((double)0, OnTargetValuePropertyChanged));

    private static void OnTargetValuePropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        var control = sender as DoubleAnimationHelper;
        if (control != null)
        {
            control.TargetValueUpdated();
        }
    }

    private void TargetValueUpdated()
    {
        if (!IsManual)
        {
            Animate();
        }
    }

    #endregion

    #region CurrentValue

    public double CurrentValue
    {
        get { return (double)GetValue(CurrentValueProperty); }
        set { SetValue(CurrentValueProperty, value); }
    }

    public static readonly DependencyProperty CurrentValueProperty =
        DependencyProperty.Register(
            "CurrentValue",
            typeof(double),
            typeof(DoubleAnimationHelper),
            new PropertyMetadata((double)0, OnCurrentValuePropertyChanged));

    private static void OnCurrentValuePropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        var control = sender as DoubleAnimationHelper;
        if (control != null)
        {
            control.CurrentValueUpdated();
        }
    }

    private void CurrentValueUpdated()
    {
        if (mProperty == ScrollViewer.VerticalOffsetProperty)
        {
            ((ScrollViewer)mOwner).ScrollToVerticalOffset(CurrentValue);
        }
        else if (mProperty == ScrollViewer.HorizontalOffsetProperty)
        {
            ((ScrollViewer)mOwner).ScrollToHorizontalOffset(CurrentValue);
        }
        else
        {
            mOwner.SetValue(mProperty, CurrentValue);
        }
    }

    #endregion

    #region Duration

    public Duration Duration
    {
        get { return (Duration)GetValue(DurationProperty); }
        set { SetValue(DurationProperty, value); }
    }

    public static readonly DependencyProperty DurationProperty =
        DependencyProperty.Register(
            "Duration",
            typeof(Duration),
            typeof(DoubleAnimationHelper),
            new PropertyMetadata(new Duration(TimeSpan.FromMilliseconds(200)), OnDurationPropertyChanged));

    private static void OnDurationPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        var control = sender as DoubleAnimationHelper;
        if (control != null)
        {
            control.DurationUpdated();
        }
    }

    private void DurationUpdated()
    {
        if (!IsManual)
        {
            Animate();
        }
    }

    #endregion

    #region EasingFunction

    public IEasingFunction EasingFunction
    {
        get { return (IEasingFunction)GetValue(EasingFunctionProperty); }
        set { SetValue(EasingFunctionProperty, value); }
    }

    public static readonly DependencyProperty EasingFunctionProperty =
        DependencyProperty.Register(
            "EasingFunction",
            typeof(IEasingFunction),
            typeof(DoubleAnimationHelper),
            new PropertyMetadata(null, OnEasingFunctionPropertyChanged));

    private static void OnEasingFunctionPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        var control = sender as DoubleAnimationHelper;
        if (control != null)
        {
            control.EasingFunctionUpdated();
        }
    }

    private void EasingFunctionUpdated()
    {
        if (!IsManual)
        {
            Animate();
        }
    }

    #endregion

    #endregion
}

 

Happy coding

Wednesday, April 25, 2012

Don’t use non indexed “Item” CLR property in binding

Today I found rather strange problem.

I was getting strange exception from MS internal code related to bindings.

The exception was raised when code was trying to set SelectedItem of DataGrid control.

Stack Trace is very large but most important lines are as follows:

Message: Object reference not set to an instance of an object.
ExceptionType: System.NullReferenceException
Details
   at MS.Internal.Data.PropertyPathWorker.DetermineWhetherDBNullIsValid(Object item)
   at MS.Internal.Data.PropertyPathWorker.DetermineWhetherDBNullIsValid()
   at MS.Internal.Data.PropertyPathWorker.get_IsDBNullValidForUpdate()
   at MS.Internal.Data.ClrBindingWorker.get_IsDBNullValidForUpdate()
   at System.Windows.Data.BindingExpression.ConvertProposedValue(Object value)
   at System.Windows.Data.BindingExpressionBase.UpdateValue()
   at System.Windows.Data.BindingExpression.UpdateOverride()
   at System.Windows.Data.BindingExpressionBase.Update()
   at System.Windows.Data.BindingExpressionBase.Dirty()
   at System.Windows.Data.BindingExpressionBase.SetValue(DependencyObject d, DependencyProperty dp, Object value)
   at System.Windows.DependencyObject.SetValueCommon(DependencyProperty dp, Object value, PropertyMetadata metadata, Boolean coerceWithDeferredReference, Boolean coerceWithCurrentValue, OperationType operationType, Boolean isInternal)
   at System.Windows.DependencyObject.SetValue(DependencyProperty dp, Object value)
   at MyNamespace.BusinessDataGrid.set_SelectedItem(Object value)

I could not understand the reason so I started to dig inside MS code using Reflector.

After some time I noticed the following piece of code inside method causing error:

private bool DetermineWhetherDBNullIsValid(object item)
{
    PropertyInfo info;
    PropertyDescriptor descriptor;
    DependencyProperty property;
    DynamicPropertyAccessor accessor;
    this.SetPropertyInfo(this._arySVS[this.Length - 1].info, out info, out descriptor, out property, out accessor);
    string columnName = (descriptor != null) ? descriptor.Name : ((info != null) ? info.Name : null);
    object arg = ((columnName == "Item") && (info != null)) ? this._arySVS[this.Length - 1].args[0] : null;
    return SystemDataHelper.DetermineWhetherDBNullIsValid(item, columnName, arg);
}

I assumed this could be problem – args of PropertyInfo where null.

It seems MS have hardcoded name “Item” as indexed property and assumed no one will ever name CLR property with “Item”.

After that I found the binding which was causing this code to execute:

image

Once commented everything worked fine.

To replicate the problem use this code:

public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            SetBinding(TagProperty, new Binding("Item")
            {
                RelativeSource = RelativeSource.Self,
                Mode = BindingMode.TwoWay,
            });

            Tag = 1;
            Tag = null;
        }

        public object Item { get; set; }
    }

 

To summarize – the rule to keep in mind – don’t name CLR property with name “Item” if you may use it in binding

 

Happy Coding

Kirill