代码之家  ›  专栏  ›  技术社区  ›  Sach

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

  •  0
  • Sach  · 技术社区  · 6 年前

    我在写一篇文章 WPF 应用( MVVM 模式使用 MVVM Light Toolkit )读取和显示我公司使用的一系列内部日志文件。其目的是从多个文件中读取,从每行中提取内容,将它们放入类对象中,并将所述对象添加到 ObservableCollection . 我已经设定了 ItemsSource A的 DataGrid 在我身上 GUI 以使其以整洁的行和列显示数据。我有一个 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);
                    }
                }
            }
        }
    }
    

    所以这很好,并按我想要的方式显示我的日志。

    问题

    然而, 之后 显示它们,如果我滚动 数据网格 , the GUI 挂起并给出以下异常。

    System.InvalidOperationException:“itemsControl不一致 及其项源。有关更多信息,请参阅内部异常 信息。

    在阅读了这篇文章之后,在谷歌的帮助下,我发现这是因为 LogLineList 与…不一致 项目源 导致冲突。

    当前解决方案

    我发现如果我输入代码行 ProcessOneFile 一秒钟内将类对象添加到列表中 调度器.invoke()。 它解决了我的问题。像这样:

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

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

    我是做错了什么事,还是应该这样做?有更好的方法来处理这个吗?

    2 回复  |  直到 6 年前
        1
  •  1
  •   ryanthedev    6 年前

    可观察的集合不是线程安全的。所以它是第二种工作方式,因为所有工作都是通过调度程序在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);            
    }
    
        2
  •  0
  •   Kevin Cook    6 年前
    public object SyncLock = new object();
    

    在您的构造函数中:

    BindingOperations.EnableCollectionSynchronization(LogLineList, SyncLock);
    

    那么在你的职能范围内:

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

    这将使集合保持同步,在同步过程中,您可以从中更新它。