Hacking the WPF GridView – Adding the animation

UPDATE: this article still deserves a bit of mention, but has been superseded by a revision of the code due to some issues. Please, have a look at this article instead.

In the first part of the WPF GridView hacking, I shown how to hide one or more column upon a certain boolean source.
Here I’ll show you a little complex hacking in order to achieve the same functionality, but adding an animation to get the collapsing/expanding ability more fancy.
Just to let you understand better what’s the goal, have a look at this short video:

How to approach the problem?

The animation capability is a well built-in feature of the WPF. I first considered to leverage the animation by using a StoryBoard or a simpler BeginAnimation over the GridViewColumn’s Width property, but…
The simplest way to animate a property is using the BeginAnimation, without any StoryBoard. However, this works only in code, and you must use a StoryBoard when in the XAML.
The following example is taken from the MSDN library documentation:

// Animate the button's width.
DoubleAnimation widthAnimation = new DoubleAnimation(120, 300, TimeSpan.FromSeconds(5));
widthAnimation.RepeatBehavior = RepeatBehavior.Forever;
widthAnimation.AutoReverse = true;
animatedButton.BeginAnimation(Button.WidthProperty, widthAnimation);

The above snippet indicates that a simple converter won’t help us for the animation, but we actually need some powerful tool.

Derive from a Behavior generic class.

It’s a been that the WPF added the powerful capability of adding one or more Behaviors to any DependencyObject-derived object. A Behavior is an elegant way to extend the functionality of a certain object, whereas it is not possible to directly modify it. I’d also add that even having the ability to modify it, a behavioral-pattern yields a lot of abstraction, thus a much greater component reuse.
At this point, the behavior should have to be attached to the GridViewColumn instance, and should also expose at least two properties:

  • IsVisible, of type Boolean, which aims the control of the related column’s visibility, and
  • NominalWidth, of type Double, which specifies the expanded-state width of the same column.

Once attached, the behavior should control the Width property of the owning’s column. Something like this:

    public class WidthAnimationBehavior
        : Behavior<GridViewColumn>
    {

        #region DP NominalLength

        public static readonly DependencyProperty NominalLengthProperty = DependencyProperty.Register(
            "NominalLength",
            typeof(double),
            typeof(WidthAnimationBehavior),
            new PropertyMetadata(
                double.NaN,
                (obj, args) =>
                {
                    var ctl = (WidthAnimationBehavior)obj;
                    ctl.NominalLengthChanged(args);
                }));


        /// <summary>
        /// Represent the nominal length value to be considered
        /// when the element is visible
        /// </summary>
        public double NominalLength
        {
            get { return (double)GetValue(NominalLengthProperty); }
            set { SetValue(NominalLengthProperty, value); }
        }


        private void NominalLengthChanged(DependencyPropertyChangedEventArgs args)
        {
            this.TriggerAnimation();
        }

        #endregion


        #region DP IsVisible

        public static readonly DependencyProperty IsVisibleProperty = DependencyProperty.Register(
            "IsVisible",
            typeof(bool),
            typeof(WidthAnimationBehavior),
            new PropertyMetadata(
                false,
                (obj, args) =>
                {
                    var ctl = (WidthAnimationBehavior)obj;
                    ctl.IsVisibleChanged(args);
                }));


        /// <summary>
        /// Get and set whether the element has to be considered visible.
        /// In this context, the "visibility" is meant as the element's
        /// length expanded (nominal length) or collapsed (zero).
        /// </summary>
        public bool IsVisible
        {
            get { return (bool)GetValue(IsVisibleProperty); }
            set { SetValue(IsVisibleProperty, value); }
        }


        private void IsVisibleChanged(DependencyPropertyChangedEventArgs args)
        {
            this.TriggerAnimation();
        }

        #endregion


        private void TriggerAnimation()
        {
            var targetWidth = this.IsVisible
                ? this.NominalLength
                : 0.0;

            if (targetWidth > 0.0 &&
                this.AssociatedObject.Width == 0.0)
            {
                //begin open

            }
            else if (targetWidth == 0.0 &&
                this.AssociatedObject.Width > 0.0)
            {
                //begin close
                
            }
        }
    }

The actual problem is that the BeginAnimation method is declared in the Animatable class, but the GridViewColumn class does not derive from it.
Let’s continue digging…

Use a timer instead…

Of course there are many ways to manage an animation: I believe the most straightforward is using a normal timer as a clock. Better, a DispatcherTimer, since the goal is dealing heavily with the UI thread, and a specific timer will surely lead a better result.
The above behavior class gets a bit more complex, but still offers a decent functionality without messing the code too much.
Here is the revised class:

    public class WidthAnimationBehavior
        : Behavior<GridViewColumn>
    {
        /// <summary>
        /// Define how long takes the animation
        /// </summary>
        /// <remarks>
        /// The value is expressed as clock interval units
        /// </remarks>
        private const int StepCount = 10;


        public WidthAnimationBehavior()
        {
            //create the clock used for the animation
            this._clock = new DispatcherTimer(DispatcherPriority.Render);
            this._clock.Interval = TimeSpan.FromMilliseconds(20);
            this._clock.Tick += _clock_Tick;
        }


        private DispatcherTimer _clock;
        private int _animationStep;
        private double _fromLength;
        private double _toLength;


        #region DP NominalLength

        public static readonly DependencyProperty NominalLengthProperty = DependencyProperty.Register(
            "NominalLength",
            typeof(double),
            typeof(WidthAnimationBehavior),
            new PropertyMetadata(
                double.NaN,
                (obj, args) =>
                {
                    var ctl = (WidthAnimationBehavior)obj;
                    ctl.NominalLengthChanged(args);
                }));


        /// <summary>
        /// Represent the nominal length value to be considered
        /// when the element is visible
        /// </summary>
        public double NominalLength
        {
            get { return (double)GetValue(NominalLengthProperty); }
            set { SetValue(NominalLengthProperty, value); }
        }


        private void NominalLengthChanged(DependencyPropertyChangedEventArgs args)
        {
            this.TriggerAnimation();
        }

        #endregion


        #region DP IsVisible

        public static readonly DependencyProperty IsVisibleProperty = DependencyProperty.Register(
            "IsVisible",
            typeof(bool),
            typeof(WidthAnimationBehavior),
            new PropertyMetadata(
                false,
                (obj, args) =>
                {
                    var ctl = (WidthAnimationBehavior)obj;
                    ctl.IsVisibleChanged(args);
                }));


        /// <summary>
        /// Get and set whether the element has to be considered visible.
        /// In this context, the "visibility" is meant as the element's
        /// length expanded (nominal length) or collapsed (zero).
        /// </summary>
        public bool IsVisible
        {
            get { return (bool)GetValue(IsVisibleProperty); }
            set { SetValue(IsVisibleProperty, value); }
        }


        private void IsVisibleChanged(DependencyPropertyChangedEventArgs args)
        {
            this.TriggerAnimation();
        }

        #endregion


        private void TriggerAnimation()
        {
            this._animationStep = StepCount;
            this._clock.IsEnabled = true;
        }


        /// <summary>
        /// Clock ticker, mainly used for the animation
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void _clock_Tick(object sender, EventArgs e)
        {
            if (this.AssociatedObject != null)
            {
                if (this._animationStep-- == StepCount)
                {
                    //calculates the from/to values to be used for the animation
                    this._fromLength = double.IsNaN(this.AssociatedObject.Width) ? 0.0 : this.AssociatedObject.Width;
                    this._toLength = this.NominalLength * (this.IsVisible ? 1.0 : 0.0);

                    if (Math.Abs(this._toLength - this._fromLength) < 0.1)
                    {
                        //the points match, thus there's no needs to animate
                        this._animationStep = 0;
                        this._clock.Stop();
                    }
                }

                if (this._clock.IsEnabled)
                {
                    //applies the easing function, whereas defined
                    double relative = (StepCount - this._animationStep) / (double)StepCount;
                    double value = this._fromLength + relative * (this._toLength - this._fromLength);

                    this.AssociatedObject.Width = value;
                }

                if (this._animationStep <= 0)
                {
                    //the animation is over: stop the clock
                    this._animationStep = 0;
                    this._clock.Stop();
                }
            }
            else
            {
                //no animation or no target: stop the clock immediately
                this._animationStep = 0;
                this._clock.Stop();
            }
        }

    }

On the XAML side, the documenti will show as follows:

<ListView
    ItemsSource="{Binding Path=People, Source={x:Static local:App.Current}}"
    Grid.Row="1"
    x:Name="lvw1"
    >
    <ListView.View>
        <GridView
            AllowsColumnReorder="False"
            >
            <GridViewColumn Header="FirstName" Width="100" DisplayMemberBinding="{Binding Path=FirstName}" />
            <GridViewColumn Header="LastName" Width="100" DisplayMemberBinding="{Binding Path=LastName}" />

            <GridViewColumn Header="Address" DisplayMemberBinding="{Binding Path=Address}">
                <i:Interaction.Behaviors>
                    <local:WidthAnimationBehavior NominalLength="200" IsVisible="{Binding Path=IsChecked, ElementName=ChkLoc}" />
                </i:Interaction.Behaviors>
            </GridViewColumn>

            <GridViewColumn Header="City" Width="120" DisplayMemberBinding="{Binding Path=City}" />
            <GridViewColumn Header="State" Width="50" DisplayMemberBinding="{Binding Path=State}" />
            <GridViewColumn Header="ZIP" Width="60" DisplayMemberBinding="{Binding Path=ZIP}" />
                    
            <GridViewColumn Header="Phone" Width="150" DisplayMemberBinding="{Binding Path=Phone}" />
            <GridViewColumn Header="Email" Width="150" DisplayMemberBinding="{Binding Path=Email}" />
        </GridView>
    </ListView.View>
</ListView>

NOTE: for readiness reasons, the XAML snippet shown is just the ListView. Also, the behavior has been applied only to the “Address” column, but it could have be applied to any other column.

Again, there’s no code behind, and that’s a good news. The usage in the XAML context is not much complex than using a normal converter. Most of the uncommon tag structure is due by the attached collection, which holds the real behavior instance.
How is the result now? This video says more than thousand words!

So, everything seems fine!…Well, not at all yet.
When the column is expanded (i.e. visible) I should be able to resize, whereas this is a desired feature. By the way, I can do it, but the new width is not stored anywhere, and a new collapse/expansion will lose the desired setting.

A dramatic new approach.

Okay, I need also another feature: the ability to add/remove the columns at runtime. That’s because our LOB app for the timber drying regulation (Cet Electronics), can’t rely on a prefixed set of columns, and the real set depends on the regulator model/state.
Moreover, the animation behavior runs fine, but…why not rethink that class in order to use for many more double-value animations?
That’s for saying that the above trick is valuable for many applications, yet not enough for a pretty flexible usage in a professional context.
So, I approached the GridViewColumns management via a proxy, where each column is mirrored by a view-model instance. I don’t know if the term “view-model” is appropriate in this case, because the GridViewColumn is actually a kind of view-model for the real elements hosted in the visual tree.
Anyway, the deal is hosting this “proxy” in some place, where the business layer would adjust the virtual columns as it wants. At that point the view (i.e. a ListView+GridView) may bind safely to this proxy, thus the visual result should match the expectations.

As for “safely”, I mean without any kind of memory-leak.

For the final solution the XAML is amazingly clean, but it’s also obvious because most of the work is done in the code behind.

<Window 
    x:Class="ListViewHacking.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:ListViewHacking"
    Title="ListView hacking demo" 
    Height="480" Width="900"
    WindowStartupLocation="CenterOwner"
    FontSize="14"
    Background="{StaticResource BG}"
    >
    
        
    <Grid
        Margin="50,40"
        >
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        
        <StackPanel
            Orientation="Horizontal"
            Grid.Row="0"
            Margin="20,8"
            >
            <CheckBox Content="Show location columns" x:Name="ChkLoc" Click="ChkLoc_Click" Margin="20,0" />
            <CheckBox Content="Show contact columns" x:Name="ChkCont" Click="ChkCont_Click" Margin="20,0" />
        </StackPanel>
        
        <ListView
            ItemsSource="{Binding Path=People, Source={x:Static local:App.Current}}"
            Grid.Row="1"
            x:Name="lvw1"
            >
            <ListView.View>
                <local:GridViewEx
                    AllowsColumnReorder="False"
                    ColumnsSource="{Binding Path=TargetCollection}"
                    >
                </local:GridViewEx>
            </ListView.View>
        </ListView>
        
    </Grid>
</Window>

However, there’s a minimal handling for the checkboxes’ events:

    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
        }


        private void ChkLoc_Click(object sender, RoutedEventArgs e)
        {
            var mirror = (GridViewColumnManager)this.DataContext;
            var isVisible = this.ChkLoc.IsChecked == true;

            //manage the visibility for the specified columns
            for (int i = 2; i <= 5; i++)
            {
                mirror.SourceItems[i].IsVisible = isVisible;
            }
        }


        private void ChkCont_Click(object sender, RoutedEventArgs e)
        {
            var mirror = (GridViewColumnManager)this.DataContext;
            var isVisible = this.ChkCont.IsChecked == true;

            //manage the visibility for the specified columns
            for (int i = 6; i <= 7; i++)
            {
                mirror.SourceItems[i].IsVisible = isVisible;
            }
        }

    }

Now, the columns’ configuration is fully done in the code behind, specifically in the main window:

    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }


        private GridViewColumnManager _manager = new GridViewColumnManager();


        private void Button_Click(object sender, RoutedEventArgs e)
        {
            if (this._manager.SourceItems.Count == 0)
            {
                //the very first time, the manager should be
                //filled up with the desired columns
                this.AddItem("FirstName", 100, true);
                this.AddItem("LastName", 100, true);

                this.AddItem("Address", 200, false);
                this.AddItem("City", 120, false);
                this.AddItem("State", 50, false);
                this.AddItem("ZIP", 60, false);

                this.AddItem("Phone", 150, false);
                this.AddItem("Email", 150, false);
            }

            //create then show the secondary window,
            //containing the grid
            var win = new Window1();
            win.Owner = this;
            win.DataContext = this._manager;
            win.ShowDialog();
        }


        //just a helper for creating a column wrapper
        private void AddItem(string caption, double width, bool isVisible)
        {
            var mi = new GridViewColumnWrapper();
            mi.Header = caption;
            mi.Width = width;
            mi.IsVisible = isVisible;

            //here is the opportunity to set the cell content:
            //either a direct data or even a more useful data-binding
            mi.Initializer = (sender, gvc) => gvc.DisplayMemberBinding = new Binding(caption);
            
            this._manager.SourceItems.Add(mi);
        }

    }

Here is the final version of the behavior:

    /// <summary>
    /// Perform a length animation overtime, without using data binding.
    /// It is a normal behavior that can be attached to any <see cref="System.Windows.DependencyObject"/>
    /// </summary>
    /// <remarks>
    /// Currently only the <see cref="System.Double"/> type is supported
    /// </remarks>
    public class LengthAnimationBehavior
        : Behavior<DependencyObject>
    {
        /// <summary>
        /// Define how long is the delay time before actually starting the animation
        /// </summary>
        /// <remarks>
        /// The value is expressed as clock interval units
        /// </remarks>
        private const int DelayCount = 10;

        /// <summary>
        /// Define how long takes the animation
        /// </summary>
        /// <remarks>
        /// The value is expressed as clock interval units
        /// </remarks>
        private const int StepCount = 10;


        /// <summary>
        /// Create the instance and specify what's
        /// the target property to animate
        /// </summary>
        /// <param name="dp"></param>
        public LengthAnimationBehavior(DependencyProperty dp)
        {
            this._dp = dp;

            //create the clock used for the animation
            this._clock = new DispatcherTimer(DispatcherPriority.Render);
            this._clock.Interval = TimeSpan.FromMilliseconds(20);
            this._clock.Tick += _clock_Tick;

            //see: http://wpf-animation.googlecode.com/svn/trunk/src/WPF/Animation/PennerDoubleAnimation.cs
            this.EasingFunction = (t, b, c, d) =>
            {
                //a quintic easing function
                if ((t /= d / 2) < 1)
                    return c / 2 * t * t * t * t * t + b;
                else
                    return c / 2 * ((t -= 2) * t * t * t * t + 2) + b;
            };
        }


        private DependencyProperty _dp;

        private DispatcherTimer _clock;
        private int _animationStep;
        private double _fromLength;
        private double _toLength;

        /// <summary>
        /// Get and set the easing function to be used for the animation
        /// </summary>
        public Func<double, double, double, double, double> EasingFunction { get; set; }


        #region DP NominalLength

        public static readonly DependencyProperty NominalLengthProperty = DependencyProperty.Register(
            "NominalLength",
            typeof(double),
            typeof(LengthAnimationBehavior),
            new PropertyMetadata(
                double.NaN,
                (obj, args) =>
                {
                    var ctl = (LengthAnimationBehavior)obj;
                    ctl.NominalLengthChanged(args);
                }));


        /// <summary>
        /// Represent the nominal length value to be considered
        /// when the element is visible
        /// </summary>
        public double NominalLength
        {
            get { return (double)GetValue(NominalLengthProperty); }
            set { SetValue(NominalLengthProperty, value); }
        }


        private void NominalLengthChanged(DependencyPropertyChangedEventArgs args)
        {
            if (this.IsAnimationEnabled)
            {
                this._animationStep = DelayCount + StepCount;
                this._clock.IsEnabled = true;
            }
            else
            {
                this.SetImmediately();
            }
        }

        #endregion


        #region DP TargetValue

        private static readonly DependencyProperty TargetValueProperty = DependencyProperty.Register(
            "TargetValue",
            typeof(object),
            typeof(LengthAnimationBehavior),
            new PropertyMetadata(
                null,
                (obj, args) =>
                {
                    var ctl = (LengthAnimationBehavior)obj;
                    ctl.TargetValueChanged(args);
                }));


        /// <summary>
        /// Used as mirror of the target property value.
        /// It's a simple way to be notified of any value change
        /// </summary>
        /// <remarks>
        /// Please, note that's everything private
        /// </remarks>
        private object TargetValue
        {
            get { return (object)GetValue(TargetValueProperty); }
            set { SetValue(TargetValueProperty, value); }
        }


        private void TargetValueChanged(DependencyPropertyChangedEventArgs args)
        {
            if (this.IsVisible &&
                (this._animationStep <= 0 || this._animationStep > StepCount))
            {
                //fire the related event
                this.OnControlledValueChanged(this.AssociatedObject);
            }
        }

        #endregion


        #region DP IsVisible

        public static readonly DependencyProperty IsVisibleProperty = DependencyProperty.Register(
            "IsVisible",
            typeof(bool),
            typeof(LengthAnimationBehavior),
            new PropertyMetadata(
                false,
                (obj, args) =>
                {
                    var ctl = (LengthAnimationBehavior)obj;
                    ctl.IsVisibleChanged(args);
                }));


        /// <summary>
        /// Get and set whether the element has to be considered visible.
        /// In this context, the "visibility" is meant as the element's
        /// length expanded (nominal length) or collapsed (zero).
        /// </summary>
        public bool IsVisible
        {
            get { return (bool)GetValue(IsVisibleProperty); }
            set { SetValue(IsVisibleProperty, value); }
        }


        private void IsVisibleChanged(DependencyPropertyChangedEventArgs args)
        {
            if (this.IsAnimationEnabled)
            {
                this._animationStep = DelayCount + StepCount;
                this._clock.IsEnabled = true;
            }
            else
            {
                this.SetImmediately();
            }
        }

        #endregion


        #region DP IsAnimationEnabled

        public static readonly DependencyProperty IsAnimationEnabledProperty = DependencyProperty.Register(
            "IsAnimationEnabled",
            typeof(bool),
            typeof(LengthAnimationBehavior),
            new PropertyMetadata(
                false,
                (obj, args) =>
                {
                    var ctl = (LengthAnimationBehavior)obj;
                    ctl.IsAnimationEnabledChanged(args);
                }));


        /// <summary>
        /// Get or set whether the animation should run or not.
        /// When disabled, any setting will take place immediately
        /// </summary>
        public bool IsAnimationEnabled
        {
            get { return (bool)GetValue(IsAnimationEnabledProperty); }
            set { SetValue(IsAnimationEnabledProperty, value); }
        }


        private void IsAnimationEnabledChanged(DependencyPropertyChangedEventArgs args)
        {
            if ((bool)args.NewValue == false)
            {
                this._animationStep = 0;
                this._clock.Stop();
            }
        }

        #endregion


        /// <summary>
        /// Allow to set the new target length immediately,
        /// without any animation or delay
        /// </summary>
        private void SetImmediately()
        {
            if (this.AssociatedObject != null)
            {
                this.AssociatedObject.SetValue(
                    this._dp,
                    this.NominalLength * (this.IsVisible ? 1.0 : 0.0)
                    );
            }
        }


        /// <summary>
        /// Clock ticker, mainly used for the animation
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void _clock_Tick(object sender, EventArgs e)
        {
            if (this.IsAnimationEnabled &&
                this.AssociatedObject != null)
            {
                //check the initial delay
                if (--this._animationStep > StepCount)
                    return;

                //when the delay expires...
                if (this._animationStep == StepCount)
                {
                    //...calculates the from/to values to be used for the animation
                    this._fromLength = (double)this.TargetValue;
                    this._toLength = this.NominalLength * (this.IsVisible ? 1.0 : 0.0);

                    if (Math.Abs(this._toLength - this._fromLength) < 0.1)
                    {
                        //the points match, thus there's no needs to animate
                        this._animationStep = 0;
                        this._clock.Stop();
                    }
                }

                if (this._clock.IsEnabled)
                {
                    //applies the easing function, whereas defined
                    double value = this.EasingFunction(
                        StepCount - this._animationStep,
                        this._fromLength,
                        this._toLength - this._fromLength,
                        StepCount
                        );

                    this.AssociatedObject.SetValue(
                        this._dp,
                        value
                        );
                }

                if (this._animationStep <= 0)
                {
                    //the animation is over: stop the clock
                    this._animationStep = 0;
                    this._clock.Stop();
                }
            }
            else
            {
                //no animation or no target: stop the clock immediately
                this._animationStep = 0;
                this._clock.Stop();
            }
        }


        /// <summary>
        /// The behavior has just been attached to the object
        /// </summary>
        protected override void OnAttached()
        {
            base.OnAttached();

            BindingOperations.SetBinding(
                this,
                LengthAnimationBehavior.TargetValueProperty,
                new Binding()
                {
                    Path = new PropertyPath(this._dp),
                    Source = this.AssociatedObject,
                    Mode = BindingMode.OneWay,
                });
        }


        /// <summary>
        /// The behavior has just been detached to the object
        /// </summary>
        protected override void OnDetaching()
        {
            BindingOperations.ClearBinding(
                this,
                LengthAnimationBehavior.TargetValueProperty
                );

            base.OnDetaching();
        }


        #region EVT ControlledValueChanged

        /// <summary>
        /// Provide the notification of any change
        /// of the target property value, when the animation
        /// is not active
        /// </summary>
        public event EventHandler<ControlledValueChangedEventArgs> ControlledValueChanged;


        private void OnControlledValueChanged(DependencyObject associated)
        {
            var handler = this.ControlledValueChanged;

            if (handler != null)
            {
                handler(
                    this,
                    new ControlledValueChangedEventArgs(associated)
                    );
            }
        }

        #endregion

    }


    /// <summary>
    /// Event arguments for the notification of any change
    /// of the target property value, when the animation
    /// is not active
    /// </summary>
    public class ControlledValueChangedEventArgs
        : EventArgs
    {
        public ControlledValueChangedEventArgs(DependencyObject associated)
        {
            this.AssociatedObject = associated;
        }

        public DependencyObject AssociatedObject { get; private set; }
    }

Here is the proxy manager and a minimal realization of the column mirror model:

    /// <summary>
    /// Proxy for the columns collection used in a grid-view
    /// </summary>
    public class GridViewColumnManager
    {
        public GridViewColumnManager()
        {
            //create the source items collection instance
            this._sourceItems = new ObservableCollection<GridViewColumnWrapper>();
            this._sourceItems.CollectionChanged += SourceItemsCollectionChanged;

            //create the target columns collection instance
            this._targetCollection = new ObservableCollection<GridViewColumn>();
        }


        #region PROP SourceItems

        private readonly ObservableCollection<GridViewColumnWrapper> _sourceItems;

        /// <summary>
        /// Collection reference for the column wrapper items
        /// </summary>
        public ObservableCollection<GridViewColumnWrapper> SourceItems 
        {
            get { return this._sourceItems; }
        }

        #endregion


        #region PROP TargetCollection

        private readonly ObservableCollection<GridViewColumn> _targetCollection;

        /// <summary>
        /// Columns collection reference for the grid-view
        /// </summary>
        public IEnumerable<GridViewColumn> TargetCollection
        {
            get { return this._targetCollection; }
        }

        #endregion


        void SourceItemsCollectionChanged(
            object sender,
            System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            this.Align();
        }


        /// <summary>
        /// Provides to align the target collection by the source's one.
        /// The default implementation is a simple positional one-to-one mirroring.
        /// </summary>
        /// <remarks>
        /// The wrapper and the actual column instances are compared by leveraging
        /// the column's hash code, which is stored privately inside the wrapper
        /// </remarks>
        protected virtual void Align()
        {
            int ixt = 0;
            for (int ixs = 0; ixs < this._sourceItems.Count; ixs++)
            {
                GridViewColumnWrapper wrapper = this._sourceItems[ixs];
                int pos = -1;

                if (this._targetCollection.Count > ixt)
                {
                    //search for the column equivalent to the current wrapper
                    pos = this._targetCollection.Count;
                    while (--pos >= 0 && this._targetCollection[pos].GetHashCode() != wrapper.ColumnHash) ;
                }

                if (pos >= 0)
                {
                    //the column was found, but adjust its position only
                    //when is not already correct
                    if (pos != ixt)
                        this._targetCollection.Move(pos, ixt);
                }
                else
                {
                    //the column was not found, so create a new one
                    var col = new GridViewColumn();
                    wrapper.ColumnHash = col.GetHashCode();

                    //simple copy of the header, so a further binding is also possible
                    col.Header = wrapper.Header;

                    //sets the initial (nominal) width of the column
                    col.Width = wrapper.Width;

                    //yields a column initialization, whereas available
                    if (wrapper.Initializer != null)
                    {
                        wrapper.Initializer(wrapper, col);
                    }

                    this._targetCollection.Insert(ixt, col);

                    //creates the behavior for the length animation
                    var bvr = new LengthAnimationBehavior(GridViewColumn.WidthProperty);
                    Interaction.GetBehaviors(col).Add(bvr);
                    bvr.ControlledValueChanged += bvr_ControlledValueChanged;

                    //binds the nominal width of the column to the behavior
                    BindingOperations.SetBinding(
                        bvr,
                        LengthAnimationBehavior.NominalLengthProperty,
                        new Binding("Width")
                        {
                            Source = wrapper,
                        });

                    //also binds the visibility to the behavior
                    BindingOperations.SetBinding(
                        bvr,
                        LengthAnimationBehavior.IsVisibleProperty,
                        new Binding("IsVisible")
                        {
                            Source = wrapper,
                        });

                    //now finally enables the animation
                    bvr.IsAnimationEnabled = true;
                }

                ixt++;
            }

            //removes any no further useful column
            while (this._targetCollection.Count > ixt)
                this._targetCollection.RemoveAt(ixt);
        }


        /// <summary>
        /// Event handler for the actual column's width changing
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        /// <remarks>
        /// This is very useful for keeping track of the manual resizing
        /// of any grid-view column. Every width changing off the animation,
        /// will be notified here.
        /// </remarks>
        void bvr_ControlledValueChanged(object sender, ControlledValueChangedEventArgs e)
        {
            var col = (GridViewColumn)e.AssociatedObject;
            var hash = col.GetHashCode();
            var item = this._sourceItems.FirstOrDefault(_ => _.ColumnHash == hash);
            if (item != null)
            {
                //update the nominal width in the wrapper with
                //the desired one
                item.Width = col.Width;
            }
        }

    }


    public class GridViewColumnWrapper
        : INotifyPropertyChanged
    {

        internal int ColumnHash;

        public string Name { get; set; }
        public Action<GridViewColumnWrapper, GridViewColumn> Initializer { get; set; }


        #region PROP Header

        private object _header;

        public object Header
        {
            get { return this._header; }
            set
            {
                if (this._header != value)
                {
                    this._header = value;
                    this.OnPropertyChanged("Header");
                }
            }
        }

        #endregion


        #region PROP Width

        private double _width;

        public double Width
        {
            get { return this._width; }
            set
            {
                if (this._width != value)
                {
                    this._width = value;
                    this.OnPropertyChanged("Width");
                }
            }
        }

        #endregion


        #region PROP IsVisible

        private bool _isVisible;

        public bool IsVisible
        {
            get { return this._isVisible; }
            set
            {
                if (this._isVisible != value)
                {
                    this._isVisible = value;
                    this.OnPropertyChanged("IsVisible");
                }
            }
        }

        #endregion


        #region EVT PropertyChanged

        public event PropertyChangedEventHandler PropertyChanged;


        protected virtual void OnPropertyChanged(string propertyName)
        {
            var handler = this.PropertyChanged;

            if (handler != null)
            {
                handler(
                    this,
                    new PropertyChangedEventArgs(propertyName));
            }
        }

        #endregion

    }

Finally, a derivation of the native GridView, because we need the ability to bind its column collection, but it is not available.

    public class GridViewEx
        : GridView
    {

        #region DP ColumnsSource

        public static readonly DependencyProperty ColumnsSourceProperty = DependencyProperty.Register(
            "ColumnsSource",
            typeof(ObservableCollection<GridViewColumn>),
            typeof(GridViewEx),
            new PropertyMetadata(
                null,
                (obj, args) =>
                {
                    var ctl = (GridViewEx)obj;
                    ctl.ColumnsSourceChanged(args);
                }));


        public ObservableCollection<GridViewColumn> ColumnsSource
        {
            get { return (ObservableCollection<GridViewColumn>)GetValue(ColumnsSourceProperty); }
            set { SetValue(ColumnsSourceProperty, value); }
        }


        private void ColumnsSourceChanged(DependencyPropertyChangedEventArgs args)
        {
            ObservableCollection<GridViewColumn> source;

            source = args.OldValue as ObservableCollection<GridViewColumn>;
            if (source != null)
            {
                source.CollectionChanged -= source_CollectionChanged;
            }

            this.Columns.Clear();

            source = args.NewValue as ObservableCollection<GridViewColumn>;
            if (source != null)
            {
                foreach (var col in source)
                {
                    this.Columns.Add(col);
                }

                source.CollectionChanged += source_CollectionChanged;
            }
        }

        #endregion


        void source_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    this.Columns.Add((GridViewColumn)e.NewItems[0]);
                    break;

                case NotifyCollectionChangedAction.Remove:
                    this.Columns.Remove((GridViewColumn)e.OldItems[0]);
                    break;

                case NotifyCollectionChangedAction.Move:
                    this.Columns.Move(e.OldStartingIndex, e.NewStartingIndex);
                    break;

                case NotifyCollectionChangedAction.Replace:
                    this.Columns[e.NewStartingIndex] = (GridViewColumn)e.NewItems[0];
                    break;

                case NotifyCollectionChangedAction.Reset:
                    this.Columns.Clear();
                    break;
            }
        }

    }

If you noticed, in the demo video the grid is hosted in a separate window than the main one. That’s for two reasons:

  • verify that the closure of the child windows won’t lead to any leak, and
  • verify that any manual width change on the columns has to be preserved even when the window is destroyed.

Conclusion.

I know, the final version is pretty complex when compared to the solution seen till now. However, the benefits are noticeable: here are briefly summarized:

  • Of course, the primary target is fully available: each column can be hidden or shown, via a simple bool setting;
  • the columns’ configuration is totally controlled by the back view-model;
  • ease of save/load (persist) the user’s size setting;
  • the animation behavior is now a more generic “animation of a length” (of type Double);
  • there was an effort to avoid any modification of the style of the ListView, so that all the functionality should not have any impact with the user’s style;

Next time, we’ll see that even this final release is not perfect, and it has a subtle issue. We’ll learn how to fix it by leveraging the right tool!

By clicking here, you may download the basic (simplified) demo application. Click here to download the final release, instead.

One thought on “Hacking the WPF GridView – Adding the animation

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s