Hacking the WPF GridView – Two more features

This is another post about the hacking of the standard WPF GridView typically used with(in) the ListView. If you missed anything about my previous three article on it, please have a look here.
By using the hacked component in some real projects, I discovered that something was missing, and something else could be improved. In particular, here are a couple of features making my GridViewEx almost complete (well…not perfect yet!)

XAML-defined columns.

The GridViewEx approach was based on a GridViewColumn wrapper. The host application should add as many wrappers as needed, and the real columns will be created (and synchronized) accordingly. This approach is basically the MVVM pattern, and yields the ability to manipulate the visual columns with ease and flexibility.
However, that’s not always an acceptable usage: the XAML-way to define templates and styles is far better than the classic code. Well, I wished to find a solution as a compromise between the MVVM flexibility and the XAML power.
The solution I chose is pretty easy: an extra columns “back” synchronization, which operates when there’s no ambiguity between the XAML and the MVVM collections content. The legacy MVVM columns management is still alive and full compatible. However, when the MVVM is bound “empty” the XAML columns definition will be taken in account.
Here follows the code added to the GridViewEx class.

        /// <summary>
        /// Provides the backward-alignment from the XAML-defined columns
        /// to the wrapper collection source.
        /// That should happen only when the source collection is empty,
        /// and there are XAML-column defined in the view.
        /// </summary>
        /// <param name="source"></param>
        private void BackAlign(Collection<GridViewColumnWrapper> source)
        {
            if (source.Count == 0 &&
                this.Columns.Count > 0)
            {
                foreach (GridViewColumn column in this.Columns)
                {
                    var wrapper = new GridViewColumnWrapper();
                    wrapper.Header = column.Header;
                    wrapper.Width = column.Width;
                    wrapper.IsVisible = true;
                    wrapper.ColumnHash = column.GetHashCode();
                    source.Add(wrapper);

                    //setup the column
                    this.SetupColumn(wrapper, column);
                }
            }
        }

The above method is called once only, at the moment of the binding to a new wrapper collection. As the code shows, if there’s no wrappers, the defined XAML GridViewColumn are automatically converted to wrappers. Hereinafter the game plays as usual.

The column’s setup has been moved off the existent Align method, to a dedicated one in order to be used both in the forward- and in the back-alignment of the columns’ collection.

        /// <summary>
        /// Prepares the column binding it to the wrapper
        /// </summary>
        /// <param name="wrapper"></param>
        /// <param name="col"></param>
        private void SetupColumn(
            GridViewColumnWrapper wrapper,
            GridViewColumn 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;
        }

To test the new feature, the window’s XAML page has been simplified as follows.

<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}"
    >
    
    
    <Window.Resources>
        
        <DataTemplate x:Key="dtpl1">
            <TextBlock
                Text="{Binding Path=FirstName}"
                />
        </DataTemplate>


        <DataTemplate x:Key="dtpl2">
            <TextBlock
                Text="{Binding Path=LastName}"
                />
        </DataTemplate>


        <DataTemplate x:Key="dtpl3">
            <TextBlock
                Text="{Binding Path=City}"
                />
        </DataTemplate>

    </Window.Resources>
    
        
    <Grid
        Margin="50,40"
        >
        <ListView
            ItemsSource="{Binding Path=People, Source={x:Static local:App.Current}}"
            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=.}"
                    >
                    <GridViewColumn Header="FirstName" Width="150" CellTemplate="{StaticResource dtpl1}" />
                    <GridViewColumn Header="LastName" Width="150" CellTemplate="{StaticResource dtpl2}" />
                    <GridViewColumn Header="City" Width="150" CellTemplate="{StaticResource dtpl3}" />
                </local:GridViewEx>
            </ListView.View>
        </ListView>
        
    </Grid>
</Window>

Now, let’s switch to the trickier problem.

Per-column defined cell content alignment.

Sometime is useful to define the alignment (horizontal and vertical) of a cell content. Unfortunately the native GridView offers no help about this feature, and the only way to control the cell behavior is via the ListViewItem styling.
To be clear, none of the below approaches will align the text content to the right edge of the respective cells.

        <DataTemplate x:Key="dtpl1">
            <TextBlock
                Text="{Binding Path=FirstName}"
                Background="Yellow"
                HorizontalAlignment="Right"
                />
        </DataTemplate>
        
        <DataTemplate x:Key="dtpl2">
            <TextBlock
                Text="{Binding Path=LastName}"
                Background="Yellow"
                TextAlignment="Right"
                />
        </DataTemplate>

lh5-1

That’s because the template-created content is actually hosted in a ContentPresenter object. The GridView creates as many ContentPresenters as the columns are, and the user’s control over these components is very limited. Please note that the ContentPresenters are created always but when you bind the cell’s content via a simpler DisplayMemberBinding. In that case, a simple TextBlock is created instead.
As said, the only way to control the alignment of the hosted ContentPresenters is by settings the HorizontalContentAlignment (or VerticalContentAlignment) in the ListViewItem style. In other words, you could style the rows as follows:

        <ListView
            ItemsSource="{Binding Path=People, Source={x:Static local:App.Current}}"
            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>

                <!-- alter the items (rows) implicit style -->
                <Style TargetType="ListViewItem">
                    <Setter Property="HorizontalContentAlignment" Value="Right" />
                </Style>
            </ListView.Resources>

            <ListView.View>
                <local:GridViewEx
                    AllowsColumnReorder="False"
                    ColumnsSource="{Binding Path=.}"
                    >
                    <GridViewColumn Header="FirstName" Width="150" CellTemplate="{StaticResource dtpl1}" />
                    <GridViewColumn Header="LastName" Width="150" CellTemplate="{StaticResource dtpl2}" />
                    <GridViewColumn Header="City" Width="150" CellTemplate="{StaticResource dtpl3}" />
                </local:GridViewEx>
            </ListView.View>
        </ListView>

The result is as expected, but involves all the cells, without any control on a specific column.

lh5-2

This behavior is predefined in the GridViewRowPresenter, which is part of the default rows styling. The most straightforward way to take the control over the row’s cell presenters is creating (or deriving) a custom GridViewRowPresenter, then re-styling the ListViewItem. However, I would avoid to touch any of the Windows’ default styles.

The solution I used takes advantage of a couple of Attachable DependencyProperty: one for each alignment orientation.
The attached DP comes in aid because it can be stuck anywhere in the logical tree, and that’s helping to reach the hidden visual element. The basic idea is right on the attaching event of the DP: at that time the visual tree is walked up, up to the “original” GridViewRowPresenter. The immediate-child of that presenter is the sought ContentPresenter, and the alignment properties are fully exposed.

        #region DPA HorizontalContainerAlignment

        public static readonly DependencyProperty HorizontalContainerAlignmentProperty = DependencyProperty.RegisterAttached(
            "HorizontalContainerAlignment",
            typeof(HorizontalAlignment),
            typeof(GridViewEx),
            new UIPropertyMetadata(
                default(HorizontalAlignment),
                HorizontalContainerAlignmentChanged
                ));


        public static HorizontalAlignment GetHorizontalContainerAlignment(DependencyObject obj)
        {
            return (HorizontalAlignment)obj.GetValue(HorizontalContainerAlignmentProperty);
        }


        public static void SetHorizontalContainerAlignment(DependencyObject obj, HorizontalAlignment value)
        {
            obj.SetValue(HorizontalContainerAlignmentProperty, value);
        }


        private static void HorizontalContainerAlignmentChanged(object sender, DependencyPropertyChangedEventArgs args)
        {
            var current = sender as DependencyObject;
            int levels = 10;

            while (current != null && levels-- > 0)
            {
                var next = VisualTreeHelper.GetParent(current);
                if (next is GridViewRowPresenter)
                {
                    var cp = current as ContentPresenter;
                    if (cp != null)
                    {
                        cp.HorizontalAlignment = (HorizontalAlignment)args.NewValue;
                    }
                    break;
                }

                current = next;
            }
        }

        #endregion

And here is how to apply it:

        <DataTemplate x:Key="dtpl1">
            <TextBlock
                Text="{Binding Path=FirstName}"
                Background="Yellow"
                local:GridViewEx.HorizontalContainerAlignment="Stretch"
                />
        </DataTemplate>


        <DataTemplate x:Key="dtpl2">
            <TextBlock
                Text="{Binding Path=LastName}"
                Background="Yellow"
                local:GridViewEx.HorizontalContainerAlignment="Right"
                />
        </DataTemplate>


        <DataTemplate x:Key="dtpl3">
            <TextBlock
                Text="{Binding Path=City}"
                Background="Yellow"
                />
        </DataTemplate>

Finally, the visual result is the following.
lh5-3

Conclusions.

Hacking. Far funnier than digging around for hours looking for a ready-to-go solution, which (we do know) does not exist.
I loved this series of hacking around the GridView, because it showed me that even a very simple component as the (ListView plus the) GridView is, yields a lot of power by just learning a bit on how its core works.

Feel free to download the demo source here.

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