Pivot grid in WPF

By Mirek on (tags: pivot grid, WPF, categories: code)

Today I am going to show you how easy to achieve a pivot scrolling in wpf using scroll viewers and scroll bars.

What we are going to achieve is presented on following picture

image

The center and top part can be scrolled horizontally using bottom scroll bar (red rectangle). From the other side the vertical scroll bar affects center and left side part (orange rectangle). So when we scroll vertically the top part doesn’t move and when we scroll horizontally the left part doesn’t move. The xaml of the window consists of three scroll viewers and two scroll bars.

<Grid>
       ...
       <ScrollViewer x:Name="topScrollViewer" 
                         Grid.Row="0"
                         Grid.Column="1"
                         HorizontalScrollBarVisibility="Hidden"
                         VerticalScrollBarVisibility="Disabled">
           ...
       </ScrollViewer>
       <ScrollViewer x:Name="leftScrollViewer" 
                         Grid.Row="1"
                         Grid.Column="0"
                         HorizontalScrollBarVisibility="Disabled"
                         VerticalScrollBarVisibility="Hidden">
           ...
       </:ScrollViewer>
       <ScrollViewer x:Name="centerScrollViewer"
                         Grid.Row="1"
                         Grid.Column="1"
                         VerticalScrollBarVisibility="Hidden">
           ...
       </ScrollViewer>
       <ScrollBar x:Name="horizontalScrollBar"
                  Grid.Row="2"
                  Grid.Column="1"
                  HorizontalAlignment="Stretch"
                  VerticalAlignment="Top"
                  Orientation="Horizontal"/>
       <ScrollBar x:Name="verticalScrollBar"
                  Grid.Row="1"
                  Grid.Column="2"
                  HorizontalAlignment="Left"
                  VerticalAlignment="Stretch"
                  Orientation="Vertical" />
   </Grid>

Now the goal is to have the scroll viewers react on scroll bars change and from the other side scrolling on scroll viewer should adjust the value of proper scroll bar. Let’s do it by binding. The ScrollBar has a dependency property Value which we can bind to. The ScrollViewer has HorizontalOffset and VerticalOffset which both represents the scroll value of the viewer, however both those are not dependency property, so we cannot bind to them.
The solution is to extend the ScrollViewer and expose horizontal and vertical offsets as dependency properties.

public class ScrollViewerEx : ScrollViewer
{
    public double VerticalOffsetBindable
    {
        get { return (double)GetValue(VerticalOffsetBindableProperty); }
        set { SetValue(VerticalOffsetBindableProperty, value); }
    }
 
    public static readonly DependencyProperty VerticalOffsetBindableProperty =
        DependencyProperty.Register("VerticalOffsetBindable", typeof(double), typeof(ScrollViewerEx),
        new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, VerticalOffsetBindablePropertyChanged));
    
    public double HorizontalOffsetBindable
    {
        get { return (double)GetValue(HorizontalOffsetBindableProperty); }
        set { SetValue(HorizontalOffsetBindableProperty, value); }
    }
    
    private static void VerticalOffsetBindablePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var viewer = d as ScrollViewer;
        viewer.ScrollToVerticalOffset((double)e.NewValue);
    }
 
    public static readonly DependencyProperty HorizontalOffsetBindableProperty =
        DependencyProperty.Register("HorizontalOffsetBindable", typeof(double), typeof(ScrollViewerEx),
        new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, HorizontalOffsetBindablePropertyChanged));
 
    private static void HorizontalOffsetBindablePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var viewer = d as ScrollViewer;
        viewer.ScrollToHorizontalOffset((double)e.NewValue);
    }
    
    protected override void OnScrollChanged(ScrollChangedEventArgs e)
    {
        HorizontalOffsetBindable = e.HorizontalOffset;
        VerticalOffsetBindable = e.VerticalOffset;
        base.OnScrollChanged(e);
    }
}
 

Having that we can bind our scroll bar’s Value to the HorizontalOffsetBindable and VerticalOffsetBindable. For instance for the center scroll viewer we must bind both scroll bars, the vertical and horizontal one.

<h:ScrollViewerEx x:Name="centerScrollViewer"
                          Grid.Row="1"
                          Grid.Column="1"
                          HorizontalScrollBarVisibility="Hidden"
                          VerticalScrollBarVisibility="Hidden"
                          VerticalOffsetBindable="{Binding Value, ElementName=verticalScrollBar}"
                          HorizontalOffsetBindable="{Binding Value, ElementName=horizontalScrollBar}">
 ...
</h:ScrollViewerEx>

The dependency properties defined in ScrollViewerEx are set to be TwoWay by default. This makes the scrolling working from both ways. Either we scroll on the viewer with use for instance mouse wheel or we drag and move the scroll bar, the content of the viewer is scrolled.  Additionally to have the scroll bars reflect the vieport size we must set its Maximum and ViewportSize properties to corresponding properties of the center scroll viewer

<ScrollBar x:Name="horizontalScrollBar"
           Grid.Row="2"
           Grid.Column="1"
           HorizontalAlignment="Stretch"
           VerticalAlignment="Top"
           Orientation="Horizontal"
           Maximum="{Binding ElementName=centerScrollViewer, Path=ScrollableWidth}"
           ViewportSize="{Binding ElementName=centerScrollViewer, Path=ViewportWidth}" />
<ScrollBar x:Name="verticalScrollBar"
           Grid.Row="1"
           Grid.Column="2"
           HorizontalAlignment="Left"
           VerticalAlignment="Stretch"
           Orientation="Vertical"
           Maximum="{Binding ElementName=centerScrollViewer, Path=ScrollableHeight}"
           ViewportSize="{Binding ElementName=centerScrollViewer, Path=ViewportHeight}" />

 

And we are done. The complete solution can be found in attachement.

Download attachement - 5 KB