ItemsControl 与其项目源不一致 - 使用 Dispatcher.Invoke() 时出现问题
An ItemsControl is inconsistent with its items source - Problem when using Dispatcher.Invoke()
我正在编写一个 WPF
应用程序(MVVM
模式使用 MVVM Light Toolkit
)来读取和显示我公司使用的一堆内部日志文件。目标是从多个文件中读取,从每一行中提取内容,将它们放入一个 class 对象中,并将该对象添加到一个 ObservableCollection
中。我已将 GUI
上的 DataGrid
的 ItemsSource
设置为此列表,以便它以整齐的行和列显示数据。我有一个ProgressBar
秒控window,在文件读取和显示过程中会更新进度
设置
请注意,所有这些方法都被精简为要点,删除了所有不相关的代码位。
加载按钮
当用户选择包含日志文件的目录并单击此按钮时,该过程开始。此时我打开包含 ProgressBar
的 window。我在这个过程中使用 BackgroundWorker
。
public void LoadButtonClicked()
{
_dialogService = new DialogService();
BackgroundWorker worker = new BackgroundWorker
{
WorkerReportsProgress = true
};
worker.DoWork += ProcessFiles;
worker.ProgressChanged += Worker_ProgressChanged;
worker.RunWorkerAsync();
}
ProcessFiles() 方法
这会读取所选目录中的所有个文件,并一一处理。在这里,当启动进度条 window 时,我使用 Dispatcher.Invoke()
.
private void ProcessFiles(object sender, DoWorkEventArgs e)
{
LogLineList = new ObservableCollection<LogLine>();
System.Windows.Application.Current.Dispatcher.Invoke(() =>
{
_dialogService.ShowProgressBarDialog();
});
var fileCount = 0;
foreach (string file in FileList)
{
fileCount++;
int currProgress = Convert.ToInt32(fileCount / (double)FileList.Length * 100);
ProcessOneFile(file);
(sender as BackgroundWorker).ReportProgress(currProgress);
}
}
ProcessOneFile() 方法
这个,顾名思义,读取一个文件,逐行检查,将内容转换为我的 class 对象并将它们添加到列表中。
public void ProcessOneFile(string fileName)
{
if (FileIO.OpenAndReadAllLinesInFile(fileName, out List<string> strLineList))
{
foreach (string line in strLineList)
{
if (CreateLogLine(line, out LogLine logLine))
{
if (logLine.IsRobotLog)
{
LogLineList.Add(logLine);
}
}
}
}
}
所以这工作得很好,并按我的需要显示我的日志。
问题
然而,在 显示它们之后,如果我滚动我的 DataGrid
,GUI
挂起并给出以下异常。
System.InvalidOperationException: 'An ItemsControl is inconsistent
with its items source. See the inner exception for more
information.'
在 Google 的帮助下阅读 SO 后,我发现这是因为我的 LogLineList
与 ItemsSource
不一致导致冲突。
当前解
我发现如果我将代码行放在 ProcessOneFile
中,我在第二个 Dispatcher.Invoke()
内将 class 对象添加到我的列表中,它就解决了我的问题。像这样:
if (logLine.IsRobotLog)
{
System.Windows.Application.Current.Dispatcher.Invoke(() =>
{
LogLineList.Add(logLine);
});
}
现在这又可以正常工作了,但问题是这会大大减慢处理时间。以前一个 10,000 行的日志文件大约需要 1 秒,而现在可能需要 5-10 倍的时间。
我是在做错什么,还是在预料之中?有没有更好的方法来处理这个问题?
public object SyncLock = new object();
在你的构造函数中:
BindingOperations.EnableCollectionSynchronization(LogLineList, SyncLock);
然后在你的函数中:
if (logLine.IsRobotLog)
{
lock(SyncLock)
{
LogLineList.Add(logLine);
}
}
这将使集合在您更新它的任何线程中保持同步。
好吧,observable 集合不是线程安全的。所以它以第二种方式工作,因为所有工作都是通过调度程序在 UI 线程上完成的。
您可以使用异步操作来简化此类流程。通过等待结果并根据结果更新 collection\progress,您将保持 UI 响应迅速且代码干净。
如果您不能或不想使用异步操作,请分批更新集合并在 UI 线程上进行更新。
编辑
举个例子
private async void Button_Click(object sender, RoutedEventArgs e)
{
//dir contents
var files = new string[4] { "file1", "file2", "file3", "file4" };
//progress bar for each file
Pg.Value = 0;
Pg.Maximum = files.Length;
foreach(var file in files)
{
await ProcessOneFile(file, entries =>
{
foreach(var entry in entries)
{
LogEntries.Add(entry);
}
});
Pg.Value++;
}
}
public async Task ProcessOneFile(string fileName, Action<List<string>> onEntryBatch)
{
//Get the lines
var lines = await Task.Run(() => GetRandom());
//the max amount of lines you want to update at once
var batchBuffer = new List<string>(100);
//Process lines
foreach (string line in lines)
{
//Create the line
if (CreateLogLine(line, out object logLine))
{
//do your check
if (logLine != null)
{
//add
batchBuffer.Add($"{fileName} -{logLine.ToString()}");
//check if we need to flush
if (batchBuffer.Count != batchBuffer.Capacity)
continue;
//update\flush
onEntryBatch(batchBuffer);
//clear
batchBuffer.Clear();
}
}
}
//One last flush
if(batchBuffer.Count > 0)
onEntryBatch(batchBuffer);
}
我正在编写一个 WPF
应用程序(MVVM
模式使用 MVVM Light Toolkit
)来读取和显示我公司使用的一堆内部日志文件。目标是从多个文件中读取,从每一行中提取内容,将它们放入一个 class 对象中,并将该对象添加到一个 ObservableCollection
中。我已将 GUI
上的 DataGrid
的 ItemsSource
设置为此列表,以便它以整齐的行和列显示数据。我有一个ProgressBar
秒控window,在文件读取和显示过程中会更新进度
设置
请注意,所有这些方法都被精简为要点,删除了所有不相关的代码位。
加载按钮
当用户选择包含日志文件的目录并单击此按钮时,该过程开始。此时我打开包含 ProgressBar
的 window。我在这个过程中使用 BackgroundWorker
。
public void LoadButtonClicked()
{
_dialogService = new DialogService();
BackgroundWorker worker = new BackgroundWorker
{
WorkerReportsProgress = true
};
worker.DoWork += ProcessFiles;
worker.ProgressChanged += Worker_ProgressChanged;
worker.RunWorkerAsync();
}
ProcessFiles() 方法
这会读取所选目录中的所有个文件,并一一处理。在这里,当启动进度条 window 时,我使用 Dispatcher.Invoke()
.
private void ProcessFiles(object sender, DoWorkEventArgs e)
{
LogLineList = new ObservableCollection<LogLine>();
System.Windows.Application.Current.Dispatcher.Invoke(() =>
{
_dialogService.ShowProgressBarDialog();
});
var fileCount = 0;
foreach (string file in FileList)
{
fileCount++;
int currProgress = Convert.ToInt32(fileCount / (double)FileList.Length * 100);
ProcessOneFile(file);
(sender as BackgroundWorker).ReportProgress(currProgress);
}
}
ProcessOneFile() 方法
这个,顾名思义,读取一个文件,逐行检查,将内容转换为我的 class 对象并将它们添加到列表中。
public void ProcessOneFile(string fileName)
{
if (FileIO.OpenAndReadAllLinesInFile(fileName, out List<string> strLineList))
{
foreach (string line in strLineList)
{
if (CreateLogLine(line, out LogLine logLine))
{
if (logLine.IsRobotLog)
{
LogLineList.Add(logLine);
}
}
}
}
}
所以这工作得很好,并按我的需要显示我的日志。
问题
然而,在 显示它们之后,如果我滚动我的 DataGrid
,GUI
挂起并给出以下异常。
System.InvalidOperationException: 'An ItemsControl is inconsistent with its items source. See the inner exception for more information.'
在 Google 的帮助下阅读 SO 后,我发现这是因为我的 LogLineList
与 ItemsSource
不一致导致冲突。
当前解
我发现如果我将代码行放在 ProcessOneFile
中,我在第二个 Dispatcher.Invoke()
内将 class 对象添加到我的列表中,它就解决了我的问题。像这样:
if (logLine.IsRobotLog)
{
System.Windows.Application.Current.Dispatcher.Invoke(() =>
{
LogLineList.Add(logLine);
});
}
现在这又可以正常工作了,但问题是这会大大减慢处理时间。以前一个 10,000 行的日志文件大约需要 1 秒,而现在可能需要 5-10 倍的时间。
我是在做错什么,还是在预料之中?有没有更好的方法来处理这个问题?
public object SyncLock = new object();
在你的构造函数中:
BindingOperations.EnableCollectionSynchronization(LogLineList, SyncLock);
然后在你的函数中:
if (logLine.IsRobotLog)
{
lock(SyncLock)
{
LogLineList.Add(logLine);
}
}
这将使集合在您更新它的任何线程中保持同步。
好吧,observable 集合不是线程安全的。所以它以第二种方式工作,因为所有工作都是通过调度程序在 UI 线程上完成的。
您可以使用异步操作来简化此类流程。通过等待结果并根据结果更新 collection\progress,您将保持 UI 响应迅速且代码干净。
如果您不能或不想使用异步操作,请分批更新集合并在 UI 线程上进行更新。
编辑 举个例子
private async void Button_Click(object sender, RoutedEventArgs e)
{
//dir contents
var files = new string[4] { "file1", "file2", "file3", "file4" };
//progress bar for each file
Pg.Value = 0;
Pg.Maximum = files.Length;
foreach(var file in files)
{
await ProcessOneFile(file, entries =>
{
foreach(var entry in entries)
{
LogEntries.Add(entry);
}
});
Pg.Value++;
}
}
public async Task ProcessOneFile(string fileName, Action<List<string>> onEntryBatch)
{
//Get the lines
var lines = await Task.Run(() => GetRandom());
//the max amount of lines you want to update at once
var batchBuffer = new List<string>(100);
//Process lines
foreach (string line in lines)
{
//Create the line
if (CreateLogLine(line, out object logLine))
{
//do your check
if (logLine != null)
{
//add
batchBuffer.Add($"{fileName} -{logLine.ToString()}");
//check if we need to flush
if (batchBuffer.Count != batchBuffer.Capacity)
continue;
//update\flush
onEntryBatch(batchBuffer);
//clear
batchBuffer.Clear();
}
}
}
//One last flush
if(batchBuffer.Count > 0)
onEntryBatch(batchBuffer);
}