Hacking the WPF GridView – Final revised release

This is the third (and supposed last) article on how to hack the GridView (ListView) of the WPF.

To tell the truth, this post was yes planned, but just for explaining a subtle issue still present in the past release. However, after playing a while with that source, I noticed that another severe issue: a potential memory-leak when the column manager is not disposed together with the (Grid)view.
That caused a dramatic changing of the code, but luckily it comes in a simpler way that the previous one.

The subtle issue.

Let’s have a peek at the known issue, firstly.
It’s about the columns’ header, which allows the resizing of the width by dragging the buttons separator. That’s quite normal, but the issue came out when I tried to resize the latest visible column, whereas the next was hidden. In the following video the issue is very clear:

So, why that happens, and how to solve it?
I had a little effort on inspecting the problem, because the logical tree exposes the GridViewColumn instances, but the visual tree is rather different. Moreover, the embedded WPF visualizer in the Visual Studio (even the Ultimate version) is pretty limited and does not offer enough help in such a situation.
Finally, I decided to try the XAML Spy tool. It looks astonishingly, with a very modern yet attractive look, but that isn’t a trap.

Inspecting the application with XAML Spy.

IMHO, the best feature the XAML Spy tool has is the ability to inspect any running process; of course, it has to be any of the supported technologies, but with WPF, Silverlight, Windows Phone and Windows Store, I think you’ll cover anything but the web.
Once run, the spy asks for the target application to attach to. If the troubled app is already started, it’s pretty straightforward finding it among the “running processes”. In case you don’t see it at first glance, just refresh the Spy’s page.

xamlspy-1

Press “attach to process” when the target process has been selected.

Immediately, you’ll notice two things:

  • the XAML Spy app will turn to the inspection page, where many tools are available, and
  • a special blue tag will be shown in the top of the target window’s viewport.

xamlspy-2

It’s a drop-down menu (or whatever you want to call it), which offers a series of useful functions.

xamlspy-3

I must admit that I was shocked by seeing that I can enable a basic ruler: useful for sure for many pixel-perfect artworks, of any every-day app. Anyway, it is just a delightful feature of the XAML Spy, and not what we actually need for the inspection.

xamlspy-4

In the Visual Studio tree viewer, you have to scan the visual tree “by yourself”, in the sense that there’s no easing on a certain visual element. Sometime, on third-party components, you actually don’t have any idea on what to look for. Finally, when the tree is huge, the searching is more than a challenge.

Instead, the GUI inspection has been made easy with the XAML Spy. Just move the mouse over the desired (or suspected) point, and a dashed border will highlight the related visual element. Of course you can’t do everything, but it’s surely a dramatic quicker way to find the hot spot.

xamlspy-5

So, the goal is understanding why dragging the rightmost column header separator will reveal the hidden column. The visual inspection (see below) indicates that the sizing button is still available even the column has been hidden.

xamlspy-6

At this point, the simplest way to get around this behavior would be disabling the whole column (visual) fragment, but…will be a real solution.

One time more, the XAML Spy allows to change the value of a property directly by the aim of the left pane, which is a well-known property-grid. Let’s say that only a subset of the available properties can be modified at runtime with this feature. That’s because the WPF code, which turns to “frozen” many objects (especially whose created by a template-mechanism), thus no more modifiable externally.

xamlspy-7

So, turning the GridViewColumnHeader as NOT enabled (i.e. IsEnabled = False) is trivial, as well as verifying the result in no time.

xamlspy-8

The trick can be applied, so the only task to do now is implementing the mechanism in some way.
This is not complex at all, though. A simple converter will do the job: as soon the column width is larger than some pixel, it will disable automatically the whole column itself.

Here follows the converter’s code:

    /// <summary>
    /// This converter targets a column header,
    /// in order to disable it when its width
    /// is close to zero
    /// </summary>
    public class ColumnHeaderEnableConverter
        : IValueConverter
    {
        public object Convert(
            object value, 
            Type targetType, 
            object parameter, 
            System.Globalization.CultureInfo culture)
        {
            var width = (double)value;
            return width >= 3.0;
        }


        public object ConvertBack(
            object value, 
            Type targetType, 
            object parameter, 
            System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }

    }

Then here is how to apply in the XAML:

               ...
        <ListView
            ItemsSource="{Binding Path=People, Source={x:Static local:App.Current}}"
            Grid.Row="1"
            x:Name="lvw1"
            >
            <ListView.Resources>
                <local:ColumnHeaderEnableConverter x:Key="cv1" />
                <Style TargetType="{x:Type GridViewColumnHeader}">
                    <Setter Property="IsEnabled" Value="{Binding Path=ActualWidth, RelativeSource={RelativeSource Mode=Self}, Converter={StaticResource cv1}}" />
                </Style>
            </ListView.Resources>

            <ListView.View>
               ...

The code rewriting.

As stated, the code presented on the past article has dramatically changed, due a potential memory-leak.
Let’s say that a real memory-leak won’t happen, just because the WPF underlying code checks for the proper GridViewColumn instantiation, and throws an exception. However, the issue is severe enough to take in account, and find a workaround.

The problem faces the inability to reuse the same GridViewColumn more than once, in any GridView context. Once you insert it in a view, either you remove or that can’t be used any more. Since detaching is a bit complex, the only reliable way to consider is to prevent any GridViewColumn reusing.
Better, the old columns’ manager (GridViewColumnsManager) is no more useful and it’s substituted by a very simple ObservableCollection of GridViewColumnWrapper. Then, most of the automation hosted in the old manager has been moved into the GridViewEx.
Honestly, it’s a much more elegant solution, and safer as well. Furthermore, it offers a more abstract column-wrapper source, with just a collection.
So, the GridViewEx code is the following:

    public class GridViewEx
        : GridView
    {
        
        #region DP ColumnsSource

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


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


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

            source = args.OldValue as ObservableCollection<GridViewColumnWrapper>;
            if (source != null)
            {
                WeakEventManager<INotifyCollectionChanged, NotifyCollectionChangedEventArgs>.RemoveHandler(
                    source,
                    "CollectionChanged",
                    SourceItemsCollectionChanged
                    );
            }

            source = args.NewValue as ObservableCollection<GridViewColumnWrapper>;
            if (source != null)
            {
                WeakEventManager<INotifyCollectionChanged, NotifyCollectionChangedEventArgs>.AddHandler(
                    source,
                    "CollectionChanged",
                    SourceItemsCollectionChanged
                    );
            }

            this.Align();
        }

        #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()
        {
            if (this.ColumnsSource == null)
                return;

            int ixt = 0;
            for (int ixs = 0; ixs < this.ColumnsSource.Count; ixs++)
            {
                GridViewColumnWrapper wrapper = this.ColumnsSource[ixs];
                int pos = -1;

                if (this.Columns.Count > ixt)
                {
                    //search for the column equivalent to the current wrapper
                    pos = this.Columns.Count;
                    while (--pos >= 0 && this.Columns[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.Columns.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.Columns.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.Columns.Count > ixt)
                this.Columns.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.ColumnsSource.FirstOrDefault(_ => _.ColumnHash == hash);
            if (item != null)
            {
                //update the nominal width in the wrapper with
                //the desired one
                item.Width = col.Width;
            }
        }

    }

The above view is used as follows:

        <ListView
            ItemsSource="{Binding Path=People, Source={x:Static local:App.Current}}"
            Grid.Row="1"
            x:Name="lvw1"
            >
            <ListView.Resources>
                <local:ColumnHeaderEnableConverter x:Key="cv1" />
                <Style TargetType="{x:Type GridViewColumnHeader}">
                    <Setter Property="IsEnabled" Value="{Binding Path=ActualWidth, RelativeSource={RelativeSource Mode=Self}, Converter={StaticResource cv1}}" />
                </Style>
            </ListView.Resources>

            <ListView.View>
                <local:GridViewEx
                    AllowsColumnReorder="False"
                    ColumnsSource="{Binding Path=.}"
                    >
                </local:GridViewEx>
            </ListView.View>
        </ListView>

That’s much simplified than the past solution, because even the underlying code has got simpler:

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


        private readonly ObservableCollection<GridViewColumnWrapper> _manager = new ObservableCollection<GridViewColumnWrapper>();


        private void Button_Click(object sender, RoutedEventArgs e)
        {
            if (this._manager.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("Company", 150, true);

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

                this.AddItem("Phone", 150, false);
                //this.AddItem("Fax", 100, false);
                this.AddItem("Email", 150, false);
                //this.AddItem("Web", 180, 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.Add(mi);
        }

    }

Conclusion.

Hopefully this release will be the final one. I test this source better in the near future, and any improvement or bug will be fixed accordingly.
Really can’t miss a final word on the XAML Spy tool, which surprised me a lot for the amazing features it offers. However, as seen, the flexibility of such a tool dramatically cuts the time of debugging and inspecting apps, especially when they’re running.

Here is the source code of the app.

2 thoughts on “Hacking the WPF GridView – Final revised release

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