在异步命令执行时将 ObservableCollection 绑定到 ListView 时的奇怪行为

本文关键字:ListView 绑定 命令 异步 执行 ObservableCollection | 更新日期: 2023-09-27 18:33:33

我已经从有类似问题的人那里读到了很多关于将 ObservableCollection 绑定到 ListView 的文章;但是,我还没有找到适合我的用例的解决方案。

在下面的测试应用程序中,我有一个简单的列表视图和一个按钮。启动时,初始化 ListView,即创建 2 列和 30 行,值从 0-29。30 行中的一半(即 15 行(可见。要查看剩余的 15 个项目,我必须使用滚动条向下滚动。

该按钮使用本文中的异步命令类绑定到异步命令。单击按钮(请参阅Start_Click(时,随机数将写入 ListView 的这 30 行中。这是在单独线程的无限循环中完成的(请参阅异步命令(。

现在,当我单击该按钮时,我希望所有ListView项立即更改为随机值。然而,事实并非如此。相反,只有那些不可见的项目(即滚动条之外的 15 个项目(才会更改其值。有时,任何项都不会更改其值。

下面是 XAML:

<Window x:Class="ListViewTestApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="614">
    <Grid x:Name="LayoutRoot">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="38*"/>
            <ColumnDefinition Width="9*"/>
        </Grid.ColumnDefinitions>
        <ListView HorizontalAlignment="Left" ItemsSource="{Binding MyList}" Height="261" Margin="28,24,0,0" VerticalAlignment="Top" Width="454" Grid.ColumnSpan="2">
            <ListView.View>
                <GridView>
                    <GridViewColumn Width="100" Header="Name" DisplayMemberBinding="{Binding Name}" />
                    <GridViewColumn Width="325" Header="Data" DisplayMemberBinding="{Binding Data}" />
                </GridView>
            </ListView.View>
        </ListView>
        <Button Content="Start" Command="{Binding StartCommand}" Grid.Column="1" HorizontalAlignment="Left" Margin="21.043,42,0,0" VerticalAlignment="Top" Width="75"/>        
    </Grid>
</Window>

这是我的代码(View's CodeBehind,ViewModel,Controller Logic和Model(:

/// <summary>
/// This is the CodeBehind of my View
/// </summary>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        LayoutRoot.DataContext = new ViewModel();
    }
}
/// <summary>
/// This is my ViewModel
/// </summary>
public class ViewModel : NotifyPropertyChanged
{        
    private ObservableCollection<Document> _myList;
    private Logic _logic;
    private AsynchronousCommand _startCommand;        
    public ViewModel()
    {
        _myList = new ObservableCollection<Document>();
        _logic = new Logic(this);
        _startCommand = new AsynchronousCommand(_logic.Start_Click, true);
    }
    public ObservableCollection<Document> MyList
    {
        get { return _myList; }
        set
        {
            if (_myList != value)
            {
                _myList = value;
                RaisePropertyChangedEvent("MyList");
            }
        }
    }
    public AsynchronousCommand StartCommand
    {
        get
        {
            return _startCommand;
        }
        set
        {
            _startCommand = value;
        }
    }
}
public class NotifyPropertyChanged : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected void RaisePropertyChangedEvent(string propertyName)
    {
        var handler = PropertyChanged;
        if (handler != null)
            handler(this, new PropertyChangedEventArgs(propertyName));
    }
}
/// <summary>
/// This is my Controller
/// </summary>
public class Logic
{
    private ViewModel _viewModel;
    private Random _rnd;
    public Logic(ViewModel vm)
    {
        _viewModel = vm;
        _rnd = new Random();
        for (int i = 0; i < 30; i++)
        {
            Document newDocument = new Document("Name " + i.ToString(), "Data " + i.ToString());
            _viewModel.MyList.Add(newDocument);
        } 
    }
    public void Start_Click(object obj)
    {
        while (true)
        {
            int idx = _rnd.Next(0, 29);
            _viewModel.StartCommand.ReportProgress(() =>
            {
                _viewModel.MyList[idx].Name = "New Name";
                _viewModel.MyList[idx].Data = "New Data";
            });
            System.Threading.Thread.Sleep(20);
        }
    }
}
/// <summary>
/// This is my Model
/// </summary>
public class Document
{
    public string Name { get; set; }
    public string Data { get; set; }
    public Document(string name, string data)
    {
        Name = name;
        Data = data;
    }
}

这是我的异步命令的代码,取自Dave Kerr关于CodeProject的文章:

/// <summary>
/// The ViewModelCommand class - an ICommand that can fire a function.
/// </summary>
public class Command : ICommand
{
    /// <summary>
    /// Initializes a new instance of the <see cref="Command"/> class.
    /// </summary>
    /// <param name="action">The action.</param>
    /// <param name="canExecute">if set to <c>true</c> [can execute].</param>
    public Command(Action action, bool canExecute = true)
    {
        // Set the action.
        this.action = action;
        this.canExecute = canExecute;
    }
    /// <summary>
    /// Initializes a new instance of the <see cref="Command"/> class.
    /// </summary>
    /// <param name="parameterizedAction">The parameterized action.</param>
    /// <param name="canExecute">if set to <c>true</c> [can execute].</param>
    public Command(Action<object> parameterizedAction, bool canExecute = true)
    {
        // Set the action.
        this.parameterizedAction = parameterizedAction;
        this.canExecute = canExecute;
    }
    /// <summary>
    /// Executes the command.
    /// </summary>
    /// <param name="param">The param.</param>
    public virtual void DoExecute(object param)
    {
        // Invoke the executing command, allowing the command to be cancelled.
        CancelCommandEventArgs args = new CancelCommandEventArgs() { Parameter = param, Cancel = false };
        InvokeExecuting(args);
        // If the event has been cancelled, bail now.
        if (args.Cancel)
            return;
        // Call the action or the parameterized action, whichever has been set.
        InvokeAction(param);
        // Call the executed function.
        InvokeExecuted(new CommandEventArgs() { Parameter = param });
    }
    protected void InvokeAction(object param)
    {
        Action theAction = action;
        Action<object> theParameterizedAction = parameterizedAction;
        if (theAction != null)
            theAction();
        else if (theParameterizedAction != null)
            theParameterizedAction(param);
    }
    protected void InvokeExecuted(CommandEventArgs args)
    {
        CommandEventHandler executed = Executed;
        // Call the executed event.
        if (executed != null)
            executed(this, args);
    }
    protected void InvokeExecuting(CancelCommandEventArgs args)
    {
        CancelCommandEventHandler executing = Executing;
        // Call the executed event.
        if (executing != null)
            executing(this, args);
    }
    /// <summary>
    /// The action (or parameterized action) that will be called when the command is invoked.
    /// </summary>
    protected Action action = null;
    protected Action<object> parameterizedAction = null;
    /// <summary>
    /// Bool indicating whether the command can execute.
    /// </summary>
    private bool canExecute = false;
    /// <summary>
    /// Gets or sets a value indicating whether this instance can execute.
    /// </summary>
    /// <value>
    /// <c>true</c> if this instance can execute; otherwise, <c>false</c>.
    /// </value>
    public bool CanExecute
    {
        get { return canExecute; }
        set
        {
            if (canExecute != value)
            {
                canExecute = value;
                EventHandler canExecuteChanged = CanExecuteChanged;
                if (canExecuteChanged != null)
                    canExecuteChanged(this, EventArgs.Empty);
            }
        }
    }
    #region ICommand Members
    /// <summary>
    /// Defines the method that determines whether the command can execute in its current state.
    /// </summary>
    /// <param name="parameter">Data used by the command. If the command does not require data to be passed, this object can be set to null.</param>
    /// <returns>
    /// true if this command can be executed; otherwise, false.
    /// </returns>
    /// 
    bool ICommand.CanExecute(object parameter)
    {
        return canExecute;
    }
    /// <summary>
    /// Defines the method to be called when the command is invoked.
    /// </summary>
    /// <param name="parameter">Data used by the command. If the command does not require data to be passed, this object can be set to null.</param>
    void ICommand.Execute(object parameter)
    {
        this.DoExecute(parameter);
    }
    #endregion

    /// <summary>
    /// Occurs when can execute is changed.
    /// </summary>
    public event EventHandler CanExecuteChanged;
    /// <summary>
    /// Occurs when the command is about to execute.
    /// </summary>
    public event CancelCommandEventHandler Executing;
    /// <summary>
    /// Occurs when the command executed.
    /// </summary>
    public event CommandEventHandler Executed;
}
/// <summary>
/// The CommandEventHandler delegate.
/// </summary>
public delegate void CommandEventHandler(object sender, CommandEventArgs args);
/// <summary>
/// The CancelCommandEvent delegate.
/// </summary>
public delegate void CancelCommandEventHandler(object sender, CancelCommandEventArgs args);
/// <summary>
/// CommandEventArgs - simply holds the command parameter.
/// </summary>
public class CommandEventArgs : EventArgs
{
    /// <summary>
    /// Gets or sets the parameter.
    /// </summary>
    /// <value>The parameter.</value>
    public object Parameter { get; set; }
}
/// <summary>
/// CancelCommandEventArgs - just like above but allows the event to
/// be cancelled.
/// </summary>
public class CancelCommandEventArgs : CommandEventArgs
{
    /// <summary>
    /// Gets or sets a value indicating whether this <see cref="CancelCommandEventArgs"/> command should be cancelled.
    /// </summary>
    /// <value><c>true</c> if cancel; otherwise, <c>false</c>.</value>
    public bool Cancel { get; set; }
}
/// <summary>
/// The AsynchronousCommand is a Command that runs on a thread from the thread pool.
/// </summary>
public class AsynchronousCommand : Command, INotifyPropertyChanged
{
    /// <summary>
    /// Initializes a new instance of the <see cref="AsynchronousCommand"/> class.
    /// </summary>
    /// <param name="action">The action.</param>
    /// <param name="canExecute">if set to <c>true</c> the command can execute.</param>
    public AsynchronousCommand(Action action, bool canExecute = true)
        : base(action, canExecute)
    {
        // Initialise the command.
        Initialise();
    }
    /// <summary>
    /// Initializes a new instance of the <see cref="AsynchronousCommand"/> class.
    /// </summary>
    /// <param name="parameterizedAction">The parameterized action.</param>
    /// <param name="canExecute">if set to <c>true</c> [can execute].</param>
    public AsynchronousCommand(Action<object> parameterizedAction, bool canExecute = true)
        : base(parameterizedAction, canExecute)
    {
        // Initialise the command.
        Initialise();
    }
    /// <summary>
    /// Initialises this instance.
    /// </summary>
    private void Initialise()
    {
        // Construct the cancel command.
        cancelCommand = new Command(
        () =>
        {
            // Set the Is Cancellation Requested flag.
            IsCancellationRequested = true;
        }, true);
    }
    /// <summary>
    /// Executes the command.
    /// </summary>
    /// <param name="param">The param.</param>
    public override void DoExecute(object param)
    {
        // If we are already executing, do not continue.
        if (IsExecuting)
            return;
        // Invoke the executing command, allowing the command to be cancelled.
        CancelCommandEventArgs args = new CancelCommandEventArgs() { Parameter = param, Cancel = false };
        InvokeExecuting(args);
        // If the event has been cancelled, bail now.
        if (args.Cancel)
            return;
        // We are executing.
        IsExecuting = true;
        // Store the calling dispatcher.
        callingDispatcher = Dispatcher.CurrentDispatcher;
        // Run the action on a new thread from the thread pool (this will therefore work in SL and WP7 as well).
        ThreadPool.QueueUserWorkItem(
        (state) =>
        {
            // Invoke the action.
            InvokeAction(param);
            // Fire the executed event and set the executing state.
            ReportProgress(
            () =>
            {
                // We are no longer executing.
                IsExecuting = false;
                // If we were cancelled, invoke the cancelled event - otherwise invoke executed.
                if (IsCancellationRequested)
                    InvokeCancelled(new CommandEventArgs() { Parameter = param });
                else
                    InvokeExecuted(new CommandEventArgs() { Parameter = param });
                // We are no longer requesting cancellation.
                IsCancellationRequested = false;
            }
            );
        }
        );
    }
    /// <summary>
    /// Raises the property changed event.
    /// </summary>
    /// <param name="propertyName">Name of the property.</param>
    private void NotifyPropertyChanged(string propertyName)
    {
        // Store the event handler - in case it changes between
        // the line to check it and the line to fire it.
        PropertyChangedEventHandler propertyChanged = PropertyChanged;
        // If the event has been subscribed to, fire it.
        if (propertyChanged != null)
            propertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
    /// <summary>
    /// Reports progress on the thread which invoked the command.
    /// </summary>
    /// <param name="action">The action.</param>
    public void ReportProgress(Action action)
    {
        if (IsExecuting)
        {
            if (callingDispatcher.CheckAccess())
                action();
            else
                callingDispatcher.BeginInvoke(((Action)(() => { action(); })));
        }
    }
    /// <summary>
    /// Cancels the command if requested.
    /// </summary>
    /// <returns>True if the command has been cancelled and we must return.</returns>
    public bool CancelIfRequested()
    {
        // If we haven't requested cancellation, there's nothing to do.
        if (IsCancellationRequested == false)
            return false;
        // We're done.
        return true;
    }
    /// <summary>
    /// Invokes the cancelled event.
    /// </summary>
    /// <param name="args">The <see cref="Apex.MVVM.CommandEventArgs"/> instance containing the event data.</param>
    protected void InvokeCancelled(CommandEventArgs args)
    {
        CommandEventHandler cancelled = Cancelled;
        // Call the cancelled event.
        if (cancelled != null)
            cancelled(this, args);
    }
    protected Dispatcher callingDispatcher;
    /// <summary>
    /// Flag indicating that the command is executing.
    /// </summary>
    private bool isExecuting = false;
    /// <summary>
    /// Flag indicated that cancellation has been requested.
    /// </summary>
    private bool isCancellationRequested;
    /// <summary>
    /// The cancel command.
    /// </summary>
    private Command cancelCommand;
    /// <summary>
    /// The property changed event.
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;
    /// <summary>
    /// Occurs when the command is cancelled.
    /// </summary>
    public event CommandEventHandler Cancelled;
    /// <summary>
    /// Gets or sets a value indicating whether this instance is executing.
    /// </summary>
    /// <value>
    /// <c>true</c> if this instance is executing; otherwise, <c>false</c>.
    /// </value>
    public bool IsExecuting
    {
        get
        {
            return isExecuting;
        }
        set
        {
            if (isExecuting != value)
            {
                isExecuting = value;
                NotifyPropertyChanged("IsExecuting");
            }
        }
    }
    /// <summary>
    /// Gets or sets a value indicating whether this instance is cancellation requested.
    /// </summary>
    /// <value>
    /// <c>true</c> if this instance is cancellation requested; otherwise, <c>false</c>.
    /// </value>
    public bool IsCancellationRequested
    {
        get
        {
            return isCancellationRequested;
        }
        set
        {
            if (isCancellationRequested != value)
            {
                isCancellationRequested = value;
                NotifyPropertyChanged("IsCancellationRequested");
            }
        }
    }
    /// <summary>
    /// Gets the cancel command.
    /// </summary>
    public Command CancelCommand
    {
        get { return cancelCommand; }
    }
}

在异步命令执行时将 ObservableCollection 绑定到 ListView 时的奇怪行为

Document

必须实现INotifyPropertyChanged接口,以便在属性更改后更新 UI。

为什么滚动时它有效?因为Virtualization.不可见的条目尚未被评估,因此一旦它们被评估,它们就已经收到了"新值"。

下面是有效的文档类:

/// <summary>
    /// This is my Model
    /// </summary>
    public class Document : INotifyPropertyChanged
    {
        private string _name;
        private string _data;
        public string Name
        {
            get { return _name; }
            set
            {
                _name = value;
                OnPropertyChanged();
            }
        }
        public string Data
        {
            get { return _data; }
            set
            {
                _data = value;
                OnPropertyChanged();
            }
        }
        public Document(string name, string data)
        {
            Name = name;
            Data = data;
        }
        public event PropertyChangedEventHandler PropertyChanged;
        [NotifyPropertyChangedInvocator]
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            var handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }