简体   繁体   中英

Autoscroll ListView in WPF using MVVM

I try to have a list that automatically scrolls to the end when adding a new line.

Here is a simple MVVM example in which I want to integrate :

There is a button that adds lines to the list when clicked.

Model code :

public class Student
{
    public string Lastname {get; set;}
    public string Firstname {get; set;}

    public Student(string lastname, string firstname) {
        this.Lastname = lastname;
        this.Firstname = firstname;

    }
}

public class StudentsModel: ObservableCollection<Student>
{
    private static object _threadLock = new Object();
    private static StudentsModel current = null;

    public static StudentsModel Current {
        get {
            lock (_threadLock)
            if (current == null)
                current = new StudentsModel();

            return current;
        }
    }

    private StudentsModel() {

        for (int i = 1; i <= 50; i++)
        {
            Student aStudent = new Student("Student " + i.ToString(), "Student " + i.ToString());
            Add(aStudent);
        }
    }

    public void AddAStudent(String lastname, string firstname) {
        Student aNewStudent = new Student(lastname, firstname);
        Add(aNewStudent);
    }
}

ViewModel code :

public class MainViewModel : ViewModelBase
{

    public StudentsModel Students { get; set; }

    public MainViewModel()
    {
        Students = StudentsModel.Current;
    }

    private ICommand _AddStudent;
    public ICommand AddStudent
    {
        get
        {
            if (_AddStudent == null)
            {
                _AddStudent = new DelegateCommand(delegate()
                {
                    Students.AddAStudent("New Student lastname", "New Student firstname");
                });
            }

            return _AddStudent;
        }
    }

View code :

<Window x:Class="demo.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="clr-namespace:demo.Commands">
<Grid>
    <ListView  Grid.Row="2" BorderBrush="White" ItemsSource="{Binding Path=Students}"
               HorizontalAlignment="Stretch">
        <ListView.View>
            <GridView>
                <GridViewColumn Header="Lastname" DisplayMemberBinding="{Binding Path=Lastname}" />
                <GridViewColumn Header="Firstname" DisplayMemberBinding="{Binding Path=Firstname}" />

            </GridView>
        </ListView.View>
    </ListView >
    <Button Content="Add" Command="{Binding AddStudent}" Margin="601.94,36.866,96.567,419.403" />
</Grid>

Thank you

I wrote a simple AttachedProperty that I use to auto-scroll to the bottom when a bound ObservableCollection changes:

public class AutoScroller : Behavior<ScrollViewer>
{
    public object AutoScrollTrigger 
    {
        get { return (object)GetValue( AutoScrollTriggerProperty ); }
        set { SetValue( AutoScrollTriggerProperty, value ); }
    }

    public static readonly DependencyProperty AutoScrollTriggerProperty = 
        DependencyProperty.Register(  
            "AutoScrollTrigger", 
            typeof( object ), 
            typeof( AutoScroller ), 
            new PropertyMetadata( null, ASTPropertyChanged ) );

    private static void ASTPropertyChanged( DependencyObject d, DependencyPropertyChangedEventArgs args )
    {
        var ts = d as AutoScroller;            
        if( ts == null )
            return;

        // must be attached to a ScrollViewer
        var sv = ts.AssociatedObject as ScrollViewer;

        // check if we are attached to an ObservableCollection, in which case we
        // will subscribe to CollectionChanged so that we scroll when stuff is added/removed
        var ncol = args.NewValue as INotifyCollectionChanged;
        // new event handler
        if( ncol != null )
            ncol.CollectionChanged += ts.OnCollectionChanged;

        // remove old eventhandler
        var ocol = args.OldValue as INotifyCollectionChanged;
        if( ocol != null )
            ocol.CollectionChanged -= ts.OnCollectionChanged;


        // also scroll to bottom when the bound object itself changes
        if( sv != null && ts.AutoScroll )
            sv.ScrollToBottom();
    }

    private void OnCollectionChanged(object sender, EventArgs args)
    {
        App.Current.Dispatcher.Invoke(delegate {
            (this.AssociatedObject as ScrollViewer).ScrollToBottom();
        });
    }
}

Note: I use Rx to subscribe to the CollectionChanged event, but that can be done with normal .NET event handling plus Dispatcher.Invoke to get the .ScrollToBottom() call on the UI thread.

Also Note: This attached property is in a Behavior class called TouchScroller, which does other stuff too, but it can be simplified to just a simple attached property.

EDIT:

To use it, in the xaml you would simply bind the property from the viewmodel:

<ScrollViewer ...>
    <i:Interaction.Behaviors>
        <util:TouchScroller AutoScrollTrigger="{Binding Students}" />
    </i:Interaction.Behaviors>
    ...
</ScrollViewer>

EDIT2:

I edited the code to contain a full Behavior. I use a Behavior instead of simpley a static class with an attached behavior because it gives access to .AssociatedObject , which is the ScrollViewer that you need to call .ScrollToBottom() on. Without using a behavior, you would have to keep track of these objects manually. I also removed the Rx subscription and added simple event handlers and Dispatcher.Invoke.

This is a stripped down and modified version of what I use, but I haven't tested this version.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM