ItemsControl与它的项目来源不一致-使用Dispatcher.Invoke()

时间:2018-10-02 18:38:24

标签: c# wpf dispatcher itemscontrol

我正在编写一个WPF应用程序(使用MVVM的{​​{1}}模式)来读取和显示公司使用的一堆内部日志文件。目的是读取多个文件,从每一行提取内容,将其放入类对象中,然后将所述对象添加到MVVM Light Toolkit中。我已经将ObservableCollectionItemsSource的{​​{1}}设置为此列表,以便它以整齐的行和列显示数据。我在第二个窗口中有一个DataGrid控件,该控件在文件读取和显示过程中将更新进度。

设置

请注意,所有这些方法都精简到删除所有无关代码位的要点。

加载按钮

当用户选择包含日志文件的目录并单击此按钮时,该过程开始。此时,我打开包含GUI的窗口。我在此过程中使用了ProgressBar

ProgressBar

ProcessFiles()方法

这会读取所选目录中的所有文件,并逐一处理它们。在这里,启动进度条窗口时,我使用的是BackgroundWorker

public void LoadButtonClicked()
{
    _dialogService = new DialogService();
    BackgroundWorker worker = new BackgroundWorker
    {
        WorkerReportsProgress = true
    };
    worker.DoWork += ProcessFiles;
    worker.ProgressChanged += Worker_ProgressChanged;
    worker.RunWorkerAsync();
}

ProcessOneFile()方法

顾名思义,它读取一个文件,逐行处理,将内容转换为我的类对象,然后将它们添加到列表中。

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); } } 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); } } } } } 会挂起并给出以下异常。

  

System.InvalidOperationException:'ItemsControl不一致   及其项目来源。有关更多信息,请参见内部异常   信息。

在SO上对此进行了阅读之后,我发现这是因为我的DataGridGUI不一致导致冲突。

当前解决方案

我发现,如果将代码行放在LogLineList中,在那里我在第二个ItemsSource中将类对象添加到列表中,则可以解决我的问题。像这样:

ProcessOneFile

现在这再次可以正常工作,但是问题是这极大地减慢了处理时间。以前包含10,000行的日志文件大约需要1秒钟,而现在所需的时间可能是原来的5-10倍。

我是在做错什么,还是可以预料的?有更好的方法来解决这个问题吗?

2 个答案:

答案 0 :(得分:1)

可观察的集合不是线程安全的。因此,它是第二种方法,因为所有工作都是通过分派器在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);            
}

答案 1 :(得分:0)

public object SyncLock = new object();

在您的构造函数中:

BindingOperations.EnableCollectionSynchronization(LogLineList, SyncLock);

然后在您的函数中:

if (logLine.IsRobotLog)
{
    lock(SyncLock)
    {
        LogLineList.Add(logLine);
    }                               
}

这将使集合保持同步,无论您在哪个线程中对其进行更新。