ItemsControl 与其项目源不一致 - 使用 Dispatcher.Invoke() 时出现问题

An ItemsControl is inconsistent with its items source - Problem when using Dispatcher.Invoke()

我正在编写一个 WPF 应用程序(MVVM 模式使用 MVVM Light Toolkit)来读取和显示我公司使用的一堆内部日志文件。目标是从多个文件中读取,从每一行中提取内容,将它们放入一个 class 对象中,并将该对象添加到一个 ObservableCollection 中。我已将 GUI 上的 DataGridItemsSource 设置为此列表,以便它以整齐的行和列显示数据。我有一个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);
                }
            }
        }
    }
}

所以这工作得很好,并按我的需要显示我的日志。

问题

然而, 显示它们之后,如果我滚动我的 DataGridGUI 挂起并给出以下异常。

System.InvalidOperationException: 'An ItemsControl is inconsistent with its items source. See the inner exception for more information.'

在 Google 的帮助下阅读 SO 后,我发现这是因为我的 LogLineListItemsSource 不一致导致冲突。

当前解

我发现如果我将代码行放在 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);            
}