Notes on Software

HeaderedScrollViewer in WPF

Posted by K. M. on November 7, 2009

I needed a scrollviewer with a header area on the top that does not scroll vertically but does scroll horizontally. It was pretty simple to derive from ScrollViewer, define a TopHeader property (and a LeftHeader property for good measure), modify the control template to contain ScrollViewers for the headers with hidden scrollbars and synchronize the horizontal and vertical offsets in code. This worked well until I put a ComboBox in the header. Suddenly the scroll position started getting reset to zero when the ComboBox was opened. I looked on the internet for a solution but didn’t find one. A little debugging revealed that when the ComboBox is opened, the ScrollViewer in the popup raises a ScrollChanged event which routes up to the ScrollViewer containing the header. To fix this problem all I needed to do was to ignore events not originating directly from the header ScrollViewers.

Here is the code

    [TemplatePart(Name = "PART_TopHeaderScrollViewer", Type = typeof(ScrollViewer))]
    [
TemplatePart(Name = "PART_LeftHeaderScrollViewer", Type = typeof(ScrollViewer))]
    
public class HeaderedScrollViewer : ScrollViewer
    {
        
static HeaderedScrollViewer()
        {
            DefaultStyleKeyProperty.OverrideMetadata(
typeof(HeaderedScrollViewer), new FrameworkPropertyMetadata(typeof(HeaderedScrollViewer)));
        }

        public object TopHeader
        {
            
get { return (object)GetValue(TopHeaderProperty); }
            
set { SetValue(TopHeaderProperty, value); }
        }
        
public static readonly DependencyProperty TopHeaderProperty =
            
DependencyProperty.Register("TopHeader", typeof(object), typeof(HeaderedScrollViewer), new UIPropertyMetadata(TopHeader_PropertyChanged));
        
private static void TopHeader_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            
HeaderedScrollViewer instance = (HeaderedScrollViewer)d;
            
if (e.OldValue != null)
                instance.RemoveLogicalChild(e.OldValue);
            
if (e.NewValue != null)
                instance.AddLogicalChild(e.NewValue);
        }

        public DataTemplate TopHeaderTemplate
        {
            
get { return (DataTemplate)GetValue(TopHeaderTemplateProperty); }
            
set { SetValue(TopHeaderTemplateProperty, value); }
        }
        
public static readonly DependencyProperty TopHeaderTemplateProperty =
            
DependencyProperty.Register("TopHeaderTemplate", typeof(DataTemplate), typeof(HeaderedScrollViewer), new UIPropertyMetadata());

        public object LeftHeader
        {
            
get { return (object)GetValue(LeftHeaderProperty); }
            
set { SetValue(LeftHeaderProperty, value); }
        }
        
public static readonly DependencyProperty LeftHeaderProperty =
            
DependencyProperty.Register("LeftHeader", typeof(object), typeof(HeaderedScrollViewer), new UIPropertyMetadata(LeftHeader_PropertyChanged));
        
private static void LeftHeader_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            
HeaderedScrollViewer instance = (HeaderedScrollViewer)d;
            
if (e.OldValue != null)
                instance.RemoveLogicalChild(e.OldValue);
            
if (e.NewValue != null)
                instance.AddLogicalChild(e.NewValue);
        }

        public DataTemplate LeftHeaderTemplate
        {
            
get { return (DataTemplate)GetValue(LeftHeaderTemplateProperty); }
            
set { SetValue(LeftHeaderTemplateProperty, value); }
        }
        
public static readonly DependencyProperty LeftHeaderTemplateProperty =
            
DependencyProperty.Register("LeftHeaderTemplate", typeof(DataTemplate), typeof(HeaderedScrollViewer), new UIPropertyMetadata());

        protected override System.Collections.IEnumerator LogicalChildren
        {
            
get
            {
                
if (TopHeader != null || LeftHeader != null)
                {

                    System.Collections.ArrayList children = new System.Collections.ArrayList();
                    System.Collections.
IEnumerator baseEnumerator = base.LogicalChildren;
                    
while (baseEnumerator.MoveNext())
                    {
                        children.Add(baseEnumerator.Current);
                    }
                    
if (TopHeader != null)
                        children.Add(TopHeader);
                    
if (LeftHeader != null)
                        children.Add(LeftHeader);
                    
return children.GetEnumerator();
                }
                
return base.LogicalChildren;
            }
        }

        private ScrollViewer topHeaderScrollViewer;

        private ScrollViewer leftHeaderScrollViewer;

        public override void OnApplyTemplate()
        {
            
base.OnApplyTemplate();
            topHeaderScrollViewer =
base.GetTemplateChild("PART_TopHeaderScrollViewer") as ScrollViewer;
            
if (topHeaderScrollViewer != null)
                topHeaderScrollViewer.ScrollChanged += topHeaderScrollViewer_ScrollChanged;
            leftHeaderScrollViewer =
base.GetTemplateChild("PART_LeftHeaderScrollViewer") as ScrollViewer;
            
if (leftHeaderScrollViewer != null)
                leftHeaderScrollViewer.ScrollChanged += leftHeaderScrollViewer_ScrollChanged;
        }

        private void leftHeaderScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
        {
            
if (e.OriginalSource == leftHeaderScrollViewer)
                ScrollToVerticalOffset(e.VerticalOffset);
        }

        private void topHeaderScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
        {
            
if (e.OriginalSource == topHeaderScrollViewer)
                ScrollToHorizontalOffset(e.HorizontalOffset);
        }

        protected override void OnScrollChanged(ScrollChangedEventArgs e)
        {
            
base.OnScrollChanged(e);
            
if (topHeaderScrollViewer != null)
                topHeaderScrollViewer.ScrollToHorizontalOffset(e.HorizontalOffset);
            
if (leftHeaderScrollViewer != null)
                leftHeaderScrollViewer.ScrollToVerticalOffset(e.VerticalOffset);
        }
    }

<Style TargetType="{x:Type local:HeaderedScrollViewer}">
    <Setter Property="IsTabStop" Value="False"/>
    <Setter Property="Focusable" Value="False"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:HeaderedScrollViewer}">
                <Grid Margin="{TemplateBinding Padding}" Background="{TemplateBinding Background}" SnapsToDevicePixels="true">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="Auto"/>
                        <ColumnDefinition Width="*"/>
                        <ColumnDefinition Width="Auto"/>
                    </Grid.ColumnDefinitions>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="*"/>
                        <RowDefinition Height="Auto"/>
                    </Grid.RowDefinitions>
                    <ScrollViewer Name="PART_TopHeaderScrollViewer" Grid.Column="1"
                                     HorizontalScrollBarVisibility="Hidden"
                                     VerticalScrollBarVisibility="Hidden"
                                     Focusable="false">
                        <ContentPresenter Content="{TemplateBinding TopHeader}" MinWidth="{TemplateBinding ExtentWidth}"
                                         ContentTemplate="{TemplateBinding TopHeaderTemplate}"/>
                    </ScrollViewer>
                    <ScrollViewer Name="PART_LeftHeaderScrollViewer" Grid.Row="1"
                                     HorizontalScrollBarVisibility="Hidden"
                                     VerticalScrollBarVisibility="Hidden"
                                     Focusable="false">
                        <ContentPresenter Content="{TemplateBinding LeftHeader}" MinHeight="{TemplateBinding ExtentHeight}"
                                         ContentTemplate="{TemplateBinding LeftHeaderTemplate}"/>
                    </ScrollViewer>
                    <AdornerDecorator x:Name="test" Grid.Row="1" Grid.Column="1">
                        <ScrollContentPresenter Name="PART_ScrollContentPresenter"
                                               KeyboardNavigation.DirectionalNavigation="Local"
                                               Content="{TemplateBinding Content}"
                                               ContentTemplate="{TemplateBinding ContentTemplate}"
                                               CanContentScroll="{TemplateBinding CanContentScroll}"
                                               SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                    </AdornerDecorator>
                    <ScrollBar Name="PART_HorizontalScrollBar"
                               Orientation="Horizontal"
                               Grid.Row="2" Grid.ColumnSpan="2"
                               Minimum="0.0"
                               Maximum="{TemplateBinding ScrollableWidth}"
                               ViewportSize="{TemplateBinding ViewportWidth}"
                               Value="{Binding Path=HorizontalOffset,RelativeSource={RelativeSource TemplatedParent},Mode=OneWay}"
                               Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}"
                               Cursor="Arrow"/>
                    <ScrollBar Name="PART_VerticalScrollBar"
                               Orientation="Vertical"
                               Grid.Column="2" Grid.RowSpan="2"
                               Minimum="0.0"
                               Maximum="{TemplateBinding ScrollableHeight}"
                               ViewportSize="{TemplateBinding ViewportHeight}"
                               Value="{Binding Path=VerticalOffset,RelativeSource={RelativeSource TemplatedParent},Mode=OneWay}"
                               Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"
                               Cursor="Arrow"/>
                    <DockPanel Grid.Column="2"
                               Grid.Row="2"
                               Background="{Binding Path=Background,ElementName=PART_VerticalScrollBar}"
                               LastChildFill="false">
                        <Rectangle DockPanel.Dock="Left"
                                   Width="1"
                                   Fill="White"
                                   Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"/>
                        <Rectangle DockPanel.Dock="Top"
                                   Height="1"
                                   Fill="White"
                                   Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}"/>
                    </DockPanel>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Advertisements

5 Responses to “HeaderedScrollViewer in WPF”

  1. hello i have modify you class for adding topleftheader
    just for adding another part
    if you want i can send you the projet
    semd me you mail for it

    best regards
    LETRESTE Bruno
    DSW

    • Emilien Guilmineau said

      Hello Bruno,

      I’m interested by your project, can you send it to me?

      Thank you.

      Best regards,
      GUILMINEAU Emilien

  2. Howdy are using WordPress for your site platform? I’m new to the blog world but I’m
    trying to get started and create my own. Do you require any coding expertise to make your own blog?
    Any help would be really appreciated!

    • K. M. said

      If you host on a platform like wordpress.com or blogger, you don’t need any coding expertise, but you don’t get your own domain either. You can choose to host your blog on your own domain by installing the wordpress.org open source blog software on your server. Coding experience would definitely help if you intend to do this.

  3. Bouraine said

    Hi,
    Could you tell us how to use it by giving a small example ?
    Thank you.

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

 
%d bloggers like this: