Mark Millman Ответов: 2

Принудительно динамический список WPF всегда отображает последнюю строку


У меня есть список, который отображает ObservableCollection, который растет в ответ на произвольные события. Я бы хотел, чтобы самая старая запись была вверху, и когда я добавляю новые записи, я бы хотел, чтобы список прокручивался вниз, чтобы всегда отображалась последняя строка. По умолчанию ScrollView остается вверху, а новые строки не отображаются.

Что я уже пробовал:

Мой код model () для коллекции содержит
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");
    }
}

Мой XAML содержит
<GroupBox Header="System Status">
    <ScrollViewer Name="SystemStatusSV">
        <ListBox x:Name="SystemStatusLB" ItemsSource="{Binding Model.Commands}" MaxHeight="300">
        </ListBox>
    </ScrollViewer>
<GroupBox>

Мой код xaml.cs содержит
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();
		
    }
}

2 Ответов

Рейтинг:
4

TheRealSteveJudge

Вам поможет так называемое поведение.

Предполагается, что ваше приложение называется "MyApp".
Затем вы можете реализовать желаемое поведение в подпапке под названием 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;
        }
    }
}

В вашем XAML вы должны ссылаться на "System.Окна.Интерактивность"
и пространство имен Behaviors.
<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>

Таким образом, нет необходимости заставлять список прокручиваться вниз по коду сзади.
Список автоматически прокручивается вниз до выбранного элемента.
Единственное, что нужно сделать, это установить SelectedItem на вновь добавленную запись, как это показано в этом примере:
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();
        }
    }
}

Поведение также полезно в сценариях MVVM, где вы не можете вызывать методы управления из кода позади.
Смотрите также более подробное объяснение здесь: https://www.wpftutorial.net/Behaviors.html


Mark Millman

Спасибо Вам за очень полное описание универсального решения.

TheRealSteveJudge

С наступающим Новым годом! Пожалуйста.

Рейтинг:
19

Gerry Schmitz

Прокрутите страницу до "последнего добавленного объекта". Как вы определяете "последний" , зависит только от вас.

https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.listbox.scrollintoview?view=netframework-4.8


Mark Millman

Спасибо, конечно же, это случай RTFM. Чтобы завершить решение, мне пришлось обойти ScrollIntoView, когда пользователь явно прокручивал вверх, а затем продолжить ScrollIntoView через разумный промежуток времени.