在异步命令执行时将 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; }
}
}
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));
}
}
}