ListBox Scroll Into View with MVVM

2019-03-19 05:11发布

问题:

I have what is a pretty simple problem, but I can't figure out how to crack it using MVVM.

I have a ListBox that is bound to an ObservableCollection<string>.

I run a process that will add a whole bunch of items to the collection and they are therefore shown in the ListBox.

The problem is that as the items are added to the list box... the scroll bar just grows, but I can't seem to figure out how to make it ScrollIntoView for each item added to the collection.

This sample code illustrates the problem perfectly.

XAML

<Window x:Class="Stack.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:vm="clr-namespace:Stack"
    Title="MainWindow"
    Height="350"
    Width="525">
<Window.DataContext>
    <vm:MainWindowViewModel />
</Window.DataContext>
<StackPanel>
    <ListBox Margin="10" Height="150"
             ItemsSource="{Binding Path=MyValue}" />
    <Button Margin="10"
            Height="25"
            Content="Generate"
            Command="{Binding Path=CommandName}" />
</StackPanel>
</Window>

View Model

namespace Stack
{
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Windows.Input;
using GalaSoft.MvvmLight.Command;

/// <summary>
/// TODO: Update summary.
/// </summary>
public class MainWindowViewModel : INotifyPropertyChanged
{
    private readonly BackgroundWorker _worker;

    private ICommand _commandName;

    private ObservableCollection<string> _myValue = new ObservableCollection<string>();

    /// <summary>
    /// Initializes a new instance of the <see cref="MainWindowViewModel" /> class.
    /// </summary>
    public MainWindowViewModel()
    {
        this._worker = new BackgroundWorker();
        this._worker.DoWork += new DoWorkEventHandler(DoWork);
        this._worker.ProgressChanged += new ProgressChangedEventHandler(ProgressChanged);
        this._worker.RunWorkerCompleted += delegate(object sender, RunWorkerCompletedEventArgs e)
        {
            CommandManager.InvalidateRequerySuggested();
        };
    }

    /// <summary>
    /// Occurs when a property value changes.
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;

    public ICommand CommandName
    {
        get
        {
            if (this._commandName == null)
            {
                this._commandName = new RelayCommand(() => this.CommandMethod());
            }
            return this._commandName;
        }
    }

    /// <summary>
    /// Gets or sets my value.
    /// </summary>
    /// <value>My value.</value>
    public ObservableCollection<string> MyValue
    {
        get
        {
            return this._myValue;
        }
        set
        {
            this._myValue = value;
            this.NotifyPropertyChange("MyValue");
        }
    }

    /// <summary>
    /// Notifies the property change.
    /// </summary>
    /// <param name="propName">Name of the prop.</param>
    internal void NotifyPropertyChange(string propName)
    {
        if (this.PropertyChanged != null)
        {
            this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
        }
    }

    /// <summary>
    /// Commands the method.
    /// </summary>
    private void CommandMethod()
    {
        this.MyValue.Clear();
        this._worker.RunWorkerAsync();
        this._worker.WorkerReportsProgress = true;
    }

    /// <summary>
    /// Does the work.
    /// </summary>
    /// <param name="sender">The sender.</param>
    /// <param name="e">The <see cref="System.ComponentModel.DoWorkEventArgs" /> instance containing the event data.</param>
    private void DoWork(object sender, DoWorkEventArgs e)
    {
        this.Populate();
    }

    /// <summary>
    /// Populates this instance.
    /// </summary>
    private void Populate()
    {
        for (int index = 0; index < 100; index++)
        {
            System.Threading.Thread.Sleep(10);
            this._worker.ReportProgress(index);
        }
    }

    /// <summary>
    /// Progresses the changed.
    /// </summary>
    /// <param name="sender">The sender.</param>
    /// <param name="e">The <see cref="System.ComponentModel.ProgressChangedEventArgs" /> instance containing the event data.</param>
    private void ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        this.MyValue.Add(e.ProgressPercentage.ToString());
    }
}

}

回答1:

You could create a DependencyProperty or simply extend the ListBox control and use your new control instead.

public class ScrollingListBox : ListBox
{
    protected override void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        int newItemCount = e.NewItems.Count; 

        if(newItemCount > 0) 
            this.ScrollIntoView(e.NewItems[newItemCount - 1]);

        base.OnItemsChanged(e);
    } 
}

In your XAML, add the class's namespace:

xmlns:custom="clr-namespace:ScrollingListBoxNamespace"

and swap out your standard ListBox with your custom one:

<custom:ScrollingListBox Margin="10" Height="150"
                         ItemsSource="{Binding Path=MyValue}" />


回答2:

You can also add a Behavior:

xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" 
.
.
.
<ListBox Margin="10" Height="150" ItemsSource="{Binding Path=MyValue}" >
 <i:Interaction.Behaviors>
     <bhv:ScrollIntoViewBehavior/>
 </i:Interaction.Behaviors>
</ListBox>

And implement the behavior:

using System.Windows.Interactivity;

public class ScrollIntoViewBehavior : Behavior<ListBox>
{
    protected override void OnAttached()
    {
        ListBox listBox = AssociatedObject;
        ((INotifyCollectionChanged)listBox.Items).CollectionChanged += OnListBox_CollectionChanged;
    }

    protected override void OnDetaching()
    {
        ListBox listBox = AssociatedObject;
        ((INotifyCollectionChanged)listBox.Items).CollectionChanged -= OnListBox_CollectionChanged;
    }

    private void OnListBox_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        ListBox listBox = AssociatedObject;
        if (e.Action == NotifyCollectionChangedAction.Add)
        {
            // scroll the new item into view   
            listBox.ScrollIntoView(e.NewItems[0]);
        }
    }
}


标签: c# wpf xaml mvvm