【问题标题】:An ItemsControl is inconsistent with its items source - Problem when using Dispatcher.Invoke()ItemsControl 与其项目源不一致 - 使用 Dispatcher.Invoke() 时出现问题
【发布时间】:2018-10-02 18:38:23
【问题描述】:

我正在编写一个WPF 应用程序(MVVM 模式使用MVVM Light Toolkit)来读取和显示我公司使用的一堆内部日志文件。目标是从多个文件中读取,从每一行中提取内容,将它们放在一个类对象中并将所述对象添加到ObservableCollection。我已将GUI 上的DataGridItemsSource 设置到此列表中,以便它以整齐的行和列显示数据。我在第二个窗口中有一个ProgressBar 控件,在文件读取和显示过程中会更新进度。

设置

请注意,所有这些方法都被简化为删除所有不相关代码位的基本要素。

加载按钮

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

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

ProcessFiles() 方法

这会读取所选目录中的所有文件,并一一处理。这里,在启动进度条窗口时,我使用的是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() 方法

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

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: '一个 ItemsControl 不一致 及其物品来源。有关更多信息,请参阅内部异常 信息。'

在 SO 上阅读此内容并在 Google 的帮助下,我发现这是因为我的 LogLineListItemsSource 不一致,从而导致冲突。

当前解决方案

我发现如果我将代码行放在ProcessOneFile 中,我在第二个Dispatcher.Invoke() 内将一个类对象添加到我的列表中,它可以解决我的问题。像这样:

if (logLine.IsRobotLog)
{
    System.Windows.Application.Current.Dispatcher.Invoke(() =>
    {
        LogLineList.Add(logLine);
    });                                
}

现在这又可以正常工作了,但问题是这会大大减慢处理时间。以前一个包含 10,000 行的日志文件大约需要 1 秒,而现在可能需要 5-10 倍的时间。

我做错了什么,还是可以预料到的?有没有更好的方法来处理这个问题?

【问题讨论】:

  • 如果你可以批量更新而不是实时更新,那么创建一个单独的集合(非 obs)并添加日志,每 5 秒 - 将项目源更改为从创建的新 obs 集合非 obs 集合
  • LoadButtonClick方法位于View代码后面?
  • @shat90 我试试看是否可行。
  • @SirRufo,不,我使用的是MVVM,所以它位于主窗口的ViewModel 中。该方法附加到按钮的单击事件。
  • @Sach 那么,您应该在 ViewModel 中定义一个 ICommand 属性并将其绑定到按钮的 Command 属性。方法分配不是正确的 MVVM 绑定方式。仅供参考

标签: c# wpf dispatcher itemscontrol


【解决方案1】:

可观察的集合不是线程安全的。所以它是第二种方式,因为所有工作都是通过调度程序在 UI 线程上完成的。

您可以使用异步操作来简化这种类型的流程。通过等待结果并更新结果的集合\进度,您将保持您的 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);            
}

【讨论】:

  • 通过等待结果并更新结果的集合\进度,你将保持你的 UI 响应和代码干净。你的意思是我应该await CreateLogLine() 方法并更新集合?
  • 更多内容是您希望等待流程文件并使用操作回调批量更新集合。我用一个例子更新了我的答案。
【解决方案2】:
public object SyncLock = new object();

在你的构造函数中:

BindingOperations.EnableCollectionSynchronization(LogLineList, SyncLock);

然后在你的函数中:

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

这将使集合在您更新它的任何线程中保持同步。

【讨论】:

  • 这没有帮助。我还是有这个问题。
  • 你是说速度问题还是同步问题?至于速度,您是否尝试将锁移到循环之外?
  • 不是速度问题,每当我尝试滚动(或任何操作,例如最大化窗口导致重绘DataGrid)时,它仍然会在加载后继续挂起。
猜你喜欢
  • 1970-01-01
  • 2013-10-25
  • 1970-01-01
  • 1970-01-01
  • 2018-05-14
  • 2020-01-19
  • 2021-05-27
  • 1970-01-01
  • 2021-04-09
相关资源
最近更新 更多