3

I wanted to have a ListView with a footer row that doesn't scroll vertically with the rest of the items, it should be visible all the time. I've done this using templates below:

<Style x:Key="FrozenRowListView" TargetType="ListView">
    <Setter Property="SnapsToDevicePixels" Value="true" />
    <Setter Property="OverridesDefaultStyle" Value="true" />
    <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto" />
    <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" />
    <Setter Property="ScrollViewer.CanContentScroll" Value="true" />
    <Setter Property="VerticalContentAlignment" Value="Center" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ListView">
                <Border Name="Border" BorderThickness="1">
                    <ScrollViewer Style="{StaticResource FrozenRowScrollViewer}">
                        <ItemsPresenter />
                    </ScrollViewer>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<Style x:Key="FrozenRowScrollViewer" TargetType="ScrollViewer">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ScrollViewer">
                <Grid Background="{TemplateBinding Background}">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*" />
                        <ColumnDefinition Width="Auto" />
                    </Grid.ColumnDefinitions>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="*" />
                        <RowDefinition Height="Auto" />
                    </Grid.RowDefinitions>

                    <DockPanel Margin="{TemplateBinding Padding}">
                        <ScrollViewer DockPanel.Dock="Bottom"
                                        HorizontalScrollBarVisibility="Hidden"
                                          VerticalScrollBarVisibility="Hidden"
                                          Focusable="false">
                            <GridViewRowPresenter  
                                Margin="2,0,2,0"
                                Content="{Binding Path=TemplatedParent.ItemsSource, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource summaryConverter}}"
                                Columns="{Binding Path=TemplatedParent.View.Columns, RelativeSource={RelativeSource TemplatedParent}}"
                                SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
                        </ScrollViewer>
                        <ScrollViewer DockPanel.Dock="Top"
                                        HorizontalScrollBarVisibility="Hidden"
                                        VerticalScrollBarVisibility="Hidden"
                                        Focusable="false">
                            <GridViewHeaderRowPresenter DockPanel.Dock="Top"
                                    Margin="2,0,2,0"
                                    Columns="{Binding Path=TemplatedParent.View.Columns, RelativeSource={RelativeSource TemplatedParent}}"
                                    ColumnHeaderContainerStyle="{Binding Path=TemplatedParent.View.ColumnHeaderContainerStyle, RelativeSource={RelativeSource TemplatedParent}}"
                                    ColumnHeaderTemplate="{Binding Path=TemplatedParent.View.ColumnHeaderTemplate, RelativeSource={RelativeSource TemplatedParent}}"
                                    ColumnHeaderTemplateSelector="{Binding Path=TemplatedParent.View.ColumnHeaderTemplateSelector, RelativeSource={RelativeSource TemplatedParent}}"
                                    AllowsColumnReorder="{Binding Path=TemplatedParent.View.AllowsColumnReorder, RelativeSource={RelativeSource TemplatedParent}}"
                                    ColumnHeaderContextMenu="{Binding Path=TemplatedParent.View.ColumnHeaderContextMenu, RelativeSource={RelativeSource TemplatedParent}}"
                                    ColumnHeaderToolTip="{Binding Path=TemplatedParent.View.ColumnHeaderToolTip, RelativeSource={RelativeSource TemplatedParent}}"
                                    SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
                        </ScrollViewer>

                        <ScrollContentPresenter Name="PART_ScrollContentPresenter"
                                                KeyboardNavigation.DirectionalNavigation="Local"
                                                CanContentScroll="True"
                                                CanHorizontallyScroll="False"
                                                CanVerticallyScroll="False" />
                    </DockPanel>

                    <ScrollBar Name="PART_HorizontalScrollBar"
                               Orientation="Horizontal"
                               Grid.Row="1"
                               Maximum="{TemplateBinding ScrollableWidth}"
                               ViewportSize="{TemplateBinding ViewportWidth}"
                               Value="{TemplateBinding HorizontalOffset}"
                               Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}" />

                    <ScrollBar Name="PART_VerticalScrollBar"
                               Grid.Column="1"
                               Maximum="{TemplateBinding ScrollableHeight}"
                               ViewportSize="{TemplateBinding ViewportHeight}"
                               Value="{TemplateBinding VerticalOffset}"
                               Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}" />

                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

This works fine but there is one problem. The footer doesn't scroll horizontally like column headers and other items do. I've put the extra GridViewRowPresenter into a ScrollViewer exactly like GridViewHeaderRowPresenter. It seems like the header row must have some kind of hooks to follow scroll offset.

How to make this work? And how does GridViewHeaderRowPresenter handle scrolling?

serine
  • 1,338
  • 14
  • 24

1 Answers1

2

You have to synchronize your footer row ScrollViewer.HorizontalOffset with PART_HorizontalScrollBar.Value. But here a solution like this won't work. I suggest to use an attached property. Looks complicated but it isn't.

public static class ScrollViewerBinding
{
    public static double GetHorizontalOffset(DependencyObject depObj)
    {
        return (double)depObj.GetValue(HorizontalOffsetProperty);
    }

    public static void SetHorizontalOffset(DependencyObject depObj, double value)
    {
        depObj.SetValue(HorizontalOffsetProperty, value);
    }

    public static readonly DependencyProperty HorizontalOffsetProperty =
        DependencyProperty.RegisterAttached("HorizontalOffset",
                                            typeof(double),
                                            typeof(ScrollViewerBinding),
                                            new PropertyMetadata(OnHorizontalOffsetPropertyChanged));

    private static void OnHorizontalOffsetPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ScrollViewer sv = d as ScrollViewer;
        if (sv != null)
        {
            sv.ScrollToHorizontalOffset((double)e.NewValue);
        }
    }
}

Then you can bind like this

<ScrollViewer DockPanel.Dock="Bottom"
              local:ScrollViewerBinding.HorizontalOffset="{Binding Value, ElementName=PART_HorizontalScrollBar}"

Now to your second question: How GridViewHeaderRowPresenter does this?

You can find the magic in RenewEvents() method in GridViewHeaderRowPresenter.cs (from line 946 on).

// find needed elements and hook up events
private void RenewEvents()
{
    ScrollViewer oldHeaderSV = _headerSV;
    _headerSV = Parent as ScrollViewer;
    if (oldHeaderSV != _headerSV)
    {
        if (oldHeaderSV != null)
        {
            oldHeaderSV.ScrollChanged -= new ScrollChangedEventHandler(OnHeaderScrollChanged);
        }
        if (_headerSV != null)
        {
            _headerSV.ScrollChanged += new ScrollChangedEventHandler(OnHeaderScrollChanged);
        }
    }

    ScrollViewer oldSV = _mainSV; // backup the old value
    _mainSV = TemplatedParent as ScrollViewer;

    if (oldSV != _mainSV)
    {
        if (oldSV != null)
        {
            oldSV.ScrollChanged -= new ScrollChangedEventHandler(OnMasterScrollChanged);
        }

        if (_mainSV != null)
        {
            _mainSV.ScrollChanged += new ScrollChangedEventHandler(OnMasterScrollChanged);
        }
    }
    ...

As you can see GridViewHeaderRowPresenter hooks up ScrollChanged event of Parent (the HeaderScrollViewer) and TemplatedParent (the MainScrollViewer) and then handles the events with ScrollToHorizontalOffset method too (from line 1034 on).

// The following two scroll changed methods will not be called recursively and lead to dead loop.
// When scrolling _masterSV, OnMasterScrollChanged will be called, so _headerSV also scrolled
// to the same offset. Then, OnHeaderScrollChanged be called, and try to scroll _masterSV, but
// it's already scrolled to that offset, so OnMasterScrollChanged will not be called.

// When master scroll viewer changed its offset, change header scroll viewer accordingly
private void OnMasterScrollChanged(object sender, ScrollChangedEventArgs e)
{
    if (_headerSV != null && _mainSV == e.OriginalSource)
    {
        _headerSV.ScrollToHorizontalOffset(e.HorizontalOffset);
    }
}

// When header scroll viewer changed its offset, change master scroll viewer accordingly
private void OnHeaderScrollChanged(object sender, ScrollChangedEventArgs e)
{
    if (_mainSV != null && _headerSV == e.OriginalSource)
    {
        _mainSV.ScrollToHorizontalOffset(e.HorizontalOffset);
    }
}
Community
  • 1
  • 1
LPL
  • 16,827
  • 6
  • 51
  • 95
  • wow, thanks so much, somehow couldn't figure it out, it works! but I'm still curious how do the column headers scroll horizontally even though there is no explicit binding? – serine Aug 03 '13 at 15:26
  • GridViewHeaderRowPresenter has an advantage, both ScrollViewers can be reached easily. Please see my edit. – LPL Aug 03 '13 at 17:52