Home > C#, MVVM, Windows Phone, WinRT > ScrollToBottom Behavior for ListView in MVVM based Universal Windows apps

ScrollToBottom Behavior for ListView in MVVM based Universal Windows apps

30/09/2014

In some cases, we may need to automatically scroll the content of a ListView as items are added to it (for example, in a chat-like app). To do this, we can use the ScrollIntoView method of the control.

However, if we’re following the MVVM pattern, this approach ins’t correct. We should instead use a Behavior that automatically scrolls the content when a new item is added:

public class ScrollToBottomBehavior : DependencyObject, IBehavior
{
    public DependencyObject AssociatedObject { get; private set; }

    public object ItemsSource
    {
        get { return (object)GetValue(ItemsSourceProperty); }
        set { SetValue(ItemsSourceProperty, value); }
    }

    public static readonly DependencyProperty ItemsSourceProperty =
        DependencyProperty.Register("ItemsSource", typeof(object),
        typeof(ScrollToBottomBehavior),
        new PropertyMetadata(null, ItemsSourcePropertyChanged));

    private static void ItemsSourcePropertyChanged(object sender,
        DependencyPropertyChangedEventArgs e)
    {
        var behavior = sender as ScrollToBottomBehavior;
        if (behavior.AssociatedObject == null || e.NewValue == null) return;

        var collection = behavior.ItemsSource as INotifyCollectionChanged;
        if (collection != null)
        {
            collection.CollectionChanged += (s, args) =>
            {
                var scrollViewer = behavior.AssociatedObject
                                           .GetFirstDescendantOfType<ScrollViewer>();
                scrollViewer.ChangeView(null, scrollViewer.ActualHeight, null);
            };
        }
    }

    public void Attach(DependencyObject associatedObject)
    {
        var control = associatedObject as ListView;
        if (control == null)
            throw new ArgumentException(
                "ScrollToBottomBehavior can be attached only to ListView.");

        AssociatedObject = associatedObject;
    }

    public void Detach()
    {
        AssociatedObject = null;
    }
}

The ScrollToBottomBehavior expects that the ItemsSource property is bound to an object that impletements the INotifyCollectionChanged interface (like ObservableCollection) and then registers for its CollectionChanged event (lines 22-25).

When such event occurs, we must scroll the ListView. Instead of using its ScrollIntoView method, like said before, we get the ScrollViewer that is contained in the ListView control (lines 27-28). GetFirstDescendantOfType is an extension method of the VisualTreeHelperExtensions class, part of the WinRT XAML Toolkit (you can grab only this class and add it to your project). We invoke the ChangeView method specifying the vertical offset to use: with ActualHeight, we scroll to the bottom of the control.

Now suppose we have the following ViewModel (we’re using MVVM Light Toolkit):

public class Question
{
    public string Name { get; set; }
    public string Text { get; set; }
    public DateTimeOffset Date { get; set; }
}

public class MainViewModel : ViewModelBase
{
    private ObservableCollection<Question> questions;
    public ObservableCollection<Question> Questions
    {
        get { return questions; }
        set { this.Set(ref questions, value); }
    }

    public RelayCommand AddQuestionCommand { get; set; }

    public MainViewModel()
    {
        Questions = new ObservableCollection<Question>();

        AddQuestionCommand = new RelayCommand(() =>
        {
            Questions.Add(new Question
            {
                Date = DateTime.Now,
                Name = "Name " + questions.Count,
                Text = "Question " + questions.Count
            });
        });
    }
}

We have an ObservableCollection and a Command to add items to it. We want to show the questions in a ListView that scrolls to bottom everytime a new item is added. So, we define the following XAML:

<Page
    ...
    xmlns:behaviors="using:QuestionsManager.Behaviors"
    xmlns:i="using:Microsoft.Xaml.Interactivity"
    ...>

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        ...

        <ListView ItemsSource="{Binding Questions}">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <StackPanel Margin="0,5,0,0">
                        <TextBlock Text="{Binding Date}" Margin="0,0,5,0" />
                        <StackPanel Orientation="Horizontal" Margin="0,5,0,0">
                            <TextBlock Text="{Binding Name}" FontWeight="Bold"
                                       Margin="0,0,5,0" />
                            <TextBlock Text="{Binding Text}"  />
                        </StackPanel>
                    </StackPanel>
                </DataTemplate>
            </ListView.ItemTemplate>
            <i:Interaction.Behaviors>
                <behaviors:ScrollToBottomBehavior ItemsSource="{Binding Questions}"/>
            </i:Interaction.Behaviors>
        </ListView>
        <StackPanel Grid.Row="1">
            <Button Content="Add dummy question" Command="{Binding AddQuestionCommand}" />
        </StackPanel>
    </Grid>
</Page>

At lines 23-25, we have bound the ItemsSource property of ScrollToBottomBehavior to Questions (the same of the cointainer ListView). This is all we need to do in order to activate the automatic scrolling.

Categories: C#, MVVM, Windows Phone, WinRT
%d bloggers like this: