Visually Located

XAML and GIS

Synching the scroll position of two LongListSelectors

I was looking at Stackoverflow and found a question asking about how to sync two LongListSelectors so that their scroll position was always the same. I thought this was so cool that it was worth sharing it with the masses.

First create a new class called MyLongListSelector. Unlike the ListBox, the LLS does not use a ScrollViewer to scroll the content. Instead, it uses a ViewportControl. We need to override the OnApplyTemplate and hook into the ViewportChanged event of the ViewportControl .

public class MyLongListSelector : LongListSelector
{
    private ViewportControl _viewport;
 
    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
 
        _viewport = (ViewportControl)GetTemplateChild("ViewportControl");
        _viewport.ViewportChanged += OnViewportChanged;
    }
}

Within the event handler for the ViewportChanged event, we’ll set a DependencyProperty that represent the top of the scroll position.

private void OnViewportChanged(object sender, ViewportChangedEventArgs args)
{
    ScrollPosition = _viewport.Viewport.Top;
}

The ScrollPosition property will represent the top of our own scroll position. We’ll create a DependencyProperty so that other MyLLS controls can bind to it and set it from xaml.

public double ScrollPosition
{
    get { return (double)GetValue(ViewPortProperty); }
    set { SetValue(ViewPortProperty, value); }
}
 
public static readonly DependencyProperty ViewPortProperty = DependencyProperty.Register(
    "ScrollPosition", 
    typeof(double), 
    typeof(MyLongListSelector), 
    new PropertyMetadata(0d, OnViewPortChanged));

When our own ScrollPosition changes, we’ll attempt to change the Viewport of the ViewportControl. We’ll do this because the value could be changing from xaml, where another control is setting it based on it’s value. We cannot set the Viewport directly, and we cannot set the top of the Viewport. Luckily the ViewportControl does have a SetViewportOrigin method that allows us to set the top of the scroll.

private static void OnViewPortChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    var lls = (MyLongListSelector) d;
    
    if (lls._viewport.Viewport.Top.Equals(lls.ScrollPosition)) return;
 
    lls._viewport.SetViewportOrigin(new Point(0, lls.ScrollPosition));
}

Now we can place two MyLLS controls within a Grid in our page. I used the default DataboundApp from the Windows Phone 8 template and duplicated the existing LLS.

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    <dataBoundApp1:MyLongListSelector x:Name="MainLongListSelector" Margin="0,0,-12,0" ItemsSource="{Binding Items}" SelectionChanged="MainLongListSelector_SelectionChanged"
                                      ScrollPosition="{Binding ScrollPosition, ElementName=MainLongListSelector2, Mode=TwoWay}">
        <dataBoundApp1:MyLongListSelector.ItemTemplate>
            <DataTemplate>
                <StackPanel Margin="0,0,0,17">
                    <TextBlock Text="{Binding LineOne}" TextWrapping="Wrap" Style="{StaticResource PhoneTextExtraLargeStyle}"/>
                    <TextBlock Text="{Binding LineTwo}" TextWrapping="Wrap" Margin="12,-6,12,0" Style="{StaticResource PhoneTextSubtleStyle}"/>
                </StackPanel>
            </DataTemplate>
        </dataBoundApp1:MyLongListSelector.ItemTemplate>
    </dataBoundApp1:MyLongListSelector>
    <dataBoundApp1:MyLongListSelector x:Name="MainLongListSelector2" Grid.Column="1" Margin="0,0,-12,0" ItemsSource="{Binding Items}" 
                                      ScrollPosition="{Binding ScrollPosition, ElementName=MainLongListSelector, Mode=TwoWay}">
        <dataBoundApp1:MyLongListSelector.ItemTemplate>
            <DataTemplate>
                <StackPanel Margin="0,0,0,17">
                    <TextBlock Text="{Binding LineOne}" TextWrapping="Wrap" Style="{StaticResource PhoneTextExtraLargeStyle}"/>
                    <TextBlock Text="{Binding LineTwo}" TextWrapping="Wrap" Margin="12,-6,12,0" Style="{StaticResource PhoneTextSubtleStyle}"/>
                </StackPanel>
            </DataTemplate>
        </dataBoundApp1:MyLongListSelector.ItemTemplate>
    </dataBoundApp1:MyLongListSelector>
</Grid>

Notice that the two LLS are binding to each others ScrollPosition property and are using TwoWay binding. This allows each control to set the other controls position.

This approach unfortunately causes some choppy scrolling and causes the scrolling to abruptly stop. We can help control that by checking the ManipulationState of the ViewportControl before we attempt to change the value of the Viewport. If the ManipulationState is Idle, we can be pretty sure that we should set the value because the ViewportControl is not the one being animated. We want to make sure that we do not set the Viewport for the LLS that is being animated.

private static void OnViewPortChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    var lls = (MyLongListSelector) d;
 
    if (lls._viewport.Viewport.Top.Equals(lls.ScrollPosition)) return;
 
    if (lls._viewport.ManipulationState == ManipulationState.Idle)
    {
        lls._viewport.SetViewportOrigin(new Point(0, lls.ScrollPosition));
    }
}
With this approach we have a much smoother experience. Download a working sample.

blog comments powered by Disqus