Reputation: 13
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
Upvotes: 1
Views: 5464
Reputation: 8213
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.
Upvotes: 3