Click here to Skip to main content
15,891,645 members
Please Sign up or sign in to vote.
0.00/5 (No votes)
See more:
I have a ListBox which displays an ObservableCollection that grows in response to arbitrary events. I would like the oldest entry to be at the top and as I add new entries I would like the ListBox to scroll down so that the last row is always displayed. The default behavior is for the ScrollView to remain at the top and new rows to not be displayed.

What I have tried:

My model () code for the collection contains
public class MyModel : Notifiable, INotifyPropertyChanged {
    public ObservableCollection<string> Commands { get; set; } = new ObservableCollection<string>();
    public void UpdateCommands(string msg) {
        Commands.Insert(0, msg);
        OnPropertyChanged("SystemStatusLB");
    }
}

My XAML contains
<GroupBox Header="System Status">
    <ScrollViewer Name="SystemStatusSV">
        <ListBox x:Name="SystemStatusLB" ItemsSource="{Binding Model.Commands}" MaxHeight="300">
        </ListBox>
    </ScrollViewer>
<GroupBox>

My xaml.cs code contains
public partial class MyView : UserControl {
    public MyModel Model { get; set; } = MyModel.Instance();
	protected DispatcherTimer UpdateTimer { get; set; }
	
	public MyView() {
        InitializeComponent();
	    UpdateTimer = new DispatcherTimer {Interval = new TimeSpan(0, 0, 1) };
		UpdateTimer.Tick += new EventHandler(UpdateTimer_Tick);
        UpdateTimer.Start();
	}
	public void UpdateTimer_Tick(object sender, EventArgs e) {
	    // I have tried various techniques here to update the ScrollView to display the last row without success
		
		// The following IF statement never resolves to true so the contents are not executed
        if (SystemStatusLB.ItemContainerGenerator.ContainerFromIndex(Model.Commands.Count-1) is FrameworkElement container) {
            var transform = container.TransformToVisual(SystemStatusSV);
            var elementLocation = transform.Transform(new Point(0, 0));
            double newVerticalOffset = elementLocation.Y + SystemStatusSV.VerticalOffset;
            SystemStatusSV.ScrollToVerticalOffset(newVerticalOffset);
        }
		
		// I have also tried calling
		SystemStatusSV.ScrollToVerticalOffset(280); // MaxHeight less one row of pixels
		SystemStatusLB.MoveCurrentToLast();
		
    }
}
Posted
Updated 18-Dec-19 21:26pm

Scroll to the "last object" that was added. How you determine "last" is up to you.

https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.listbox.scrollintoview?view=netframework-4.8
 
Share this answer
 
Comments
Mark Millman 20-Dec-19 12:52pm    
Thank you, surely a case of RTFM. To complete the solution I did have to bypass the ScrollIntoView when the user explicitly scrolled up and then continue the ScrollIntoView after a reasonable period of time.
A so-called behavior will help you.

It is assumed that your application is called "MyApp".
Then you could implement the wished behavior in a subfolder called Behaviors.
using System.Windows.Controls;
using System.Windows.Interactivity;

namespace MyApp.Behaviors
{
    public class ListBoxScrollIntoViewBehavior : Behavior<ListBox>
    {
        protected override void OnAttached()
        {
            base.OnAttached();

            AssociatedObject.SelectionChanged += AssociatedObject_SelectionChanged;
        }

        private void AssociatedObject_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            var listBox = sender as ListBox;

            if (listBox?.SelectedItem != null)
            {
                listBox.Dispatcher.Invoke(() =>
                {
                    listBox.UpdateLayout();
                    listBox.ScrollIntoView(listBox.SelectedItem);
                });
            }
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();

            AssociatedObject.SelectionChanged -= AssociatedObject_SelectionChanged;
        }
    }
}

In your XAML you must reference "System.Windows.Interactivity"
and the Behaviors namespace.
<Window x:Class="MyApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:behaviors="clr-namespace:MyApp.Behaviors"
        xmlns:local="clr-namespace:MyApp"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">

    <Grid>
        <ListBox Name="ListBoxItems">
            <i:Interaction.Behaviors>
                <behaviors:ListBoxScrollIntoViewBehavior/>
            </i:Interaction.Behaviors>
        </ListBox>
    </Grid>
</Window>

That way there is no need to force the ListBox to scroll down by code behind.
The ListBox will scroll down automatically to the SelectedItem.
The only thing is to set the SelectedItem to the newly added entry as it is shown in this example:
C#
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;

namespace MyApp
{
    public partial class MainWindow
    {
        private ObservableCollection<string> items;

        public MainWindow()
        {
            Loaded += MainWindow_Loaded;

            InitializeComponent();
        }

        private void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            items = new ObservableCollection<string>
            {
                "A",
                "B",
                "B",
                "C",
                "D"
            };

            ListBoxItems.ItemsSource = items;
            ListBoxItems.SelectedItem = items.LastOrDefault();
        }
    }
}

Behaviors are also useful in MVVM scenarios where you cannot call control methods from code behind.
See also a more detailed explanation here: https://www.wpftutorial.net/Behaviors.html
 
Share this answer
 
v2
Comments
Mark Millman 20-Dec-19 12:53pm    
Thank you for a very complete description of the generic solution.
TheRealSteveJudge 6-Jan-20 4:44am    
Happy new year! You're welcome.

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900