xamlwinui-3winuiwindows-app-sdk

How to unify ScrollViewer Scroll across two columns?


I'm building a two-column layout in WinUI 3 using a Grid, where each column can have a different amount of content (for example, the left column might be taller than the right one, or vice versa). I want the entire page to scroll vertically using just one single scrollbar, attached to a ScrollViewer at the page level — not on each column individually.

When scrolling, both columns should scroll together. If one column has less content than the other, it should scroll only until its content is fully visible, and then stop moving, while the longer column continues scrolling as expected. Also, when scrolling back to the top, the shorter column should start scrolling again only after the scroll reaches the point where its content naturally begins (i.e., its actual height position). I have tried below approach but the UI is not smooth and I don't think using viewchanged event for this is the best. Any suggestions or workarounds would be helpful. Thanks in Advance

Attaching the code below.

<Grid>    
    <ScrollViewer x:Name="MainScrollViewer" ViewChanged="ScrollViewer_ViewChanged">
        <Grid VerticalAlignment="Stretch">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="7.25*"/>
                <ColumnDefinition Width="2.75*"/>
            </Grid.ColumnDefinitions>
            <Grid x:Name="LeftContentContainer" Grid.Column="0" VerticalAlignment="Stretch" Height="auto" Background="{ThemeResource BackgroundBrush}"> 
             <Grid.RenderTransform>
                    <TranslateTransform x:Name="LeftContentTransform"/>
                </Grid.RenderTransform>
                <!--Content Here-->
            </Grid>
            
            <Grid x:Name="RightContentContainer" Grid.Column="1" VerticalAlignment="Stretch" Height="auto" Background="{ThemeResource BackgroundBrush}">
                <Grid.RenderTransform>
                    <TranslateTransform x:Name="RightContentTransform"/>
                </Grid.RenderTransform>
                <!--Content Here-->
            </Grid>
        </Grid>
    </ScrollViewer>
</Grid>

Code behind:

private void ScrollViewer_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
{
    double offset = MainScrollViewer.VerticalOffset;

    double maxScroll0 = Math.Max(0, leftColumnHeight - viewportHeight);
    double maxScroll1 = Math.Max(0, rightColumnHeight - viewportHeight);

    double trans0 = -Math.Min(offset, maxScroll0);
    double trans1 = -Math.Min(offset, maxScroll1);

    LeftContentTransform.Y = trans0;
    RightContentTransform.Y = trans1;
}

Solution

  • I tried your approach using RenderTransform with TranslateTransform, but the UI ended up feeling a bit rough. This is what worked better for me:

    <Grid ColumnDefinitions="*,Auto">
        <Grid
            Grid.Column="0"
            ColumnDefinitions="*,*"
            PointerWheelChanged="Grid_PointerWheelChanged"
            SizeChanged="Grid_SizeChanged">
            <Grid.Resources>
                <Style BasedOn="{StaticResource DefaultScrollViewerStyle}" TargetType="ScrollViewer">
                    <Setter Property="VerticalAlignment" Value="Top" />
                    <Setter Property="HorizontalScrollBarVisibility" Value="Hidden" />
                    <Setter Property="VerticalScrollBarVisibility" Value="Hidden" />
                    <Setter Property="HorizontalScrollMode" Value="Disabled" />
                    <Setter Property="VerticalScrollMode" Value="Disabled" />
                </Style>
            </Grid.Resources>
            <ScrollViewer x:Name="LeftScrollViewer" Grid.Column="0">
                <TextBlock FontSize="2048" Text="Left" />
            </ScrollViewer>
            <ScrollViewer x:Name="RightScrollViewer" Grid.Column="1">
                <TextBlock FontSize="1024" Text="Right" />
            </ScrollViewer>
        </Grid>
        <ScrollBar x:Name="VerticalScrollBar"
            Grid.Column="1"
            IndicatorMode="MouseIndicator"
            ValueChanged="VerticalScrollBar_ValueChanged" />
    </Grid>
    
    private void Grid_SizeChanged(object sender, SizeChangedEventArgs e)
    {
        VerticalScrollBar.ViewportSize = e.NewSize.Height;
        VerticalScrollBar.Maximum = Math.Max(LeftScrollViewer.ScrollableHeight, RightScrollViewer.ScrollableHeight);
    }
    
    private void Grid_PointerWheelChanged(object sender, PointerRoutedEventArgs e)
    {
        VerticalScrollBar.Value = Math.Max(0, Math.Min(this.VerticalScrollBar.Maximum, this.VerticalScrollBar.Value - e.GetCurrentPoint(this).Properties.MouseWheelDelta));
    }
    
    private void VerticalScrollBar_ValueChanged(object sender, RangeBaseValueChangedEventArgs e)
    {
        if (e.NewValue < 0 || e.NewValue > this.VerticalScrollBar.Maximum)
            return;
    
        this.LeftScrollViewer.ScrollToVerticalOffset(e.NewValue);
        this.RightScrollViewer.ScrollToVerticalOffset(e.NewValue);
    }