ItemsControl несовместим с источником своих элементов - проблема при использовании Dispatcher.Invoke () - PullRequest
0 голосов
/ 02 октября 2018

Я пишу приложение WPF (шаблон MVVM с использованием MVVM Light Toolkit) для чтения и отображения набора внутренних файлов журнала, используемых моей компанией.Цель состоит в том, чтобы читать из нескольких файлов, извлекать содержимое из каждой строки, помещать их в объект класса и добавлять указанный объект в ObservableCollection.Я установил ItemsSource из DataGrid в моем GUI в этот список, чтобы он отображал данные в аккуратных строках и столбцах.У меня есть элемент управления 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);
                }
            }
        }
    }
}

Так что все работает отлично и отображает мои журналы так, как я хочу.

Проблема

Однако, после их отображения , если я прокручиваю свой DataGrid, GUI зависает и выдает мне следующее исключение.

System.InvalidOperationException: 'ItemsControl несовместим с источником своих элементов.См. Внутреннее исключение для получения дополнительной информации. '

Прочитав об этом в SO и с помощью Google, я понял, что это потому, что мой LogLineList несовместим с ItemsSource, которыйприводит к конфликту.

Текущее решение

Я обнаружил, что если я добавлю строку кода в ProcessOneFile, где я добавлю объект класса в свой список внутривторой Dispatcher.Invoke() это решает мою проблему.Вот так:

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

Теперь это снова работает нормально, но проблема в том, что это ужасно замедляет время обработки.Если раньше файл журнала с 10 000 строк занимал около 1 с, то теперь он может занимать в 5-10 раз больше времени.

Я что-то не так делаю, или это следовало ожидать?Есть ли лучший способ справиться с этим?

Ответы [ 2 ]

0 голосов
/ 02 октября 2018

Хорошо наблюдаемая коллекция не является поточно-ориентированной.Таким образом, он работает вторым способом, потому что вся работа выполняется в потоке пользовательского интерфейса с помощью диспетчера.

Вы можете использовать асинхронные операции, чтобы упростить этот тип потока.Дождавшись результатов и обновив коллекцию \ прогресс по результату, вы будете поддерживать отзывчивость вашего интерфейса и чистоту кода.

Если вы не можете или не хотите использовать асинхронные операции, выполните пакетное обновление коллекции и выполните обновление в потоке пользовательского интерфейса.

Редактировать Пример, подобный этому

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);            
}
0 голосов
/ 02 октября 2018
public object SyncLock = new object();

В вашем конструкторе:

BindingOperations.EnableCollectionSynchronization(LogLineList, SyncLock);

Затем в вашей функции:

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

Это будет синхронизировать коллекцию, в которой вы когда-либо обновляли поток.

...