0

++++++ Link to example project ++++++

I have a file that can contain thousands of lines of logged messages. I am parsing this file and adding each line (as a log event) to a collection. This collection should then be shown in a ListView. As below: ListView with items

                <ListView
                Grid.Row="0"
                Margin="5"
                ItemsSource="{Binding SelectedSerilogFileLog.LogEvents}"
                ScrollViewer.CanContentScroll="False"
                ScrollViewer.HorizontalScrollBarVisibility="Disabled"
                SelectedItem="{Binding SelectedLogEvent}"
                SelectionMode="Single">                    

The parsing of the file (this one contains 2500+ log events) and adding to the collection takes around 100ms. Then when the bound collection is updated with the ReplaceContent method (this suppresses the collectionchanged event firing on every item added) the GUI hangs, but I cannot see why or what can be causing this.

MainWindow.cs

    ...

    /// <summary>
    /// 
    /// </summary>
    public SerilogFileLog SelectedSerilogFileLog
    {
        get => selectedSerilogFileLog; set
        {
            if (selectedSerilogFileLog != null)
            {
                SelectedSerilogFileLog.OnSerilogParserFinished -= OnSerilogParserFinished;
                SelectedSerilogFileLog.OnSerilogParserProgressChanged -= OnSerilogParserProgressChanged;
            }

            selectedSerilogFileLog = value;

            if (selectedSerilogFileLog != null)
            {
                ParserProgress = 0;

                SelectedSerilogFileLog.OnSerilogParserFinished += OnSerilogParserFinished;
                SelectedSerilogFileLog.OnSerilogParserProgressChanged += OnSerilogParserProgressChanged;

                sw.Start();
                SelectedSerilogFileLog.Parse();
            }

            NotifyPropertyChanged(nameof(SelectedSerilogFileLog));
        }
    }

    ...

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        SelectedSerilogFileLog = null;
        SelectedSerilogFileLog = new SerilogFileLog() { FilePath = "Application20210216.log" };
    }

The parsing and loading of the items occurs in a separate Task.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace LargeListViewTest.Classes
{
public class SerilogFileLog : INotifyPropertyChanged
{
    private LogEvent lastLogEvent;
    private ObservableCollectionEx<LogEvent> logEvents;
    private string name;
    private string description;
    private string filePath;

    private Regex patternMatching;
    private string matchExpression = @"^(?<DateTime>[^|]+)\| (?<Level>[^|]+) \| (?<MachineName>[^|]+) \| (?<Source>[^|]+) \| (?<Message>[^$]*)$";

    public delegate void SerilogParserProgressHandler(int Percentage);
    public delegate void SerilogParserFinishedHandler();

    /// <summary>
    /// 
    /// </summary>
    public event SerilogParserProgressHandler OnSerilogParserProgressChanged;

    /// <summary>
    /// 
    /// </summary>
    public event SerilogParserFinishedHandler OnSerilogParserFinished;

    /// <summary>
    /// Gets or sets the LogEvents.
    /// </summary>
    public ObservableCollectionEx<LogEvent> LogEvents { get => logEvents; private set { logEvents = value; NotifyPropertyChanged(nameof(LogEvents)); } }

    /// <summary>
    /// Gets or sets the Name.
    /// </summary>
    public string Name { get => name; private set { name = value; NotifyPropertyChanged(nameof(Name)); } }

    /// <summary>
    /// Gets or sets the Description.
    /// </summary>
    public string Description { get => description; private set { description = value; NotifyPropertyChanged(nameof(Description)); } }

    /// <summary>
    /// Gets or sets the FilePath.
    /// </summary>
    public string FilePath
    {
        get => filePath;
        set
        {
            filePath = value;
            Name = Path.GetFileNameWithoutExtension(value);
            Description = FilePath;
        }
    }

    /// <summary>
    /// 
    /// </summary>
    public SerilogFileLog()
    {
        LogEvents = new ObservableCollectionEx<LogEvent>();
        patternMatching = new Regex(matchExpression, RegexOptions.Singleline | RegexOptions.Compiled);
    }

    /// <summary>
    /// 
    /// </summary>
    public void Parse()
    {
        Task task = Task.Factory.StartNew(() => { InternalParse(); });
    }

    /// <summary>
    /// 
    /// </summary>
    private void InternalParse()
    {
        OnSerilogParserProgressChanged?.Invoke(0);

        try
        {
            if (!string.IsNullOrWhiteSpace(FilePath))
            {
                Console.WriteLine("Starting parse for {0}", FilePath);

                long currentLength = 0;

                FileInfo fi = new FileInfo(FilePath);

                if (fi.Exists)
                {
                    Console.WriteLine("Parsing Serilog file: {0}.", FilePath);

                    fi.Refresh();

                    List<LogEvent> parsedLogEvents = new List<LogEvent>();
                    StringBuilder sb = new StringBuilder();

                    using (FileStream fileStream = fi.Open(FileMode.Open, FileAccess.Read, FileShare.Write))
                    using (var streamReader = new StreamReader(fileStream))
                    {
                        while (streamReader.Peek() != -1)
                        {
                            sb.Append(streamReader.ReadLine());
                            LogEvent newLogEvent = ParseLogEvent(sb.ToString());
                            if (newLogEvent != null)
                            {
                                parsedLogEvents.Add(newLogEvent);
                                lastLogEvent = newLogEvent;
                            }

                            OnSerilogParserProgressChanged?.Invoke((int)(currentLength * 100 / fi.Length));
                            currentLength = currentLength + sb.ToString().Length;
                            sb.Clear();
                        }
                    }

                    LogEvents.ReplaceContent(parsedLogEvents);
                }

                Console.WriteLine("Finished parsing Serilog {0}.", FilePath);
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine("Error parsing Serilog." + ex.Message);
        }

        OnSerilogParserProgressChanged?.Invoke(100);

        SerilogParserFinishedHandler onSerilogParserFinished = OnSerilogParserFinished;

        if (onSerilogParserFinished == null)
            return;

        OnSerilogParserFinished();
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="mes"></param>
    /// <returns></returns>
    private LogEvent ParseLogEvent(string mes)
    {
        LogEvent logEvent = new LogEvent();

        Match matcher = patternMatching.Match(mes);

        try
        {
            if (matcher.Success)
            {
                logEvent.Message = matcher.Groups["Message"].Value;

                DateTime dt;
                if (!DateTime.TryParse(matcher.Groups["DateTime"].Value, out dt))
                {
                    Console.WriteLine("Failed to parse date {Value}", matcher.Groups["DateTime"].Value);
                }
                logEvent.DateTime = dt;
                logEvent.Level = matcher.Groups["Level"].Value;
                logEvent.MachineName = matcher.Groups["MachineName"].Value;
                logEvent.Source = matcher.Groups["Source"].Value;
            }
            else
            {
                if ((string.IsNullOrEmpty(mes) || (!Char.IsDigit(mes[0])) || !Char.IsDigit(mes[1])) && lastLogEvent != null)
                {
                    // seems to be a continuation of the previous line, add it to the last event.
                    lastLogEvent.Message += Environment.NewLine;
                    lastLogEvent.Message += mes;
                    logEvent = null;
                }
                else
                {
                    Console.WriteLine("Message parsing failed.");
                }
                if (logEvent != null)
                    logEvent.Message = mes;
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine("ParseLogEvent exception." + ex.Message);
        }

        return logEvent;
    }

    #region INotify

    public event PropertyChangedEventHandler PropertyChanged;

    public void NotifyPropertyChanged(string p) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(p));

    #endregion
}
}

I have an ObservableCollectionEx class that extends the default ObservableCollection, this class suppresses the collection changed event until all the items have been added/replaced.

    /// <summary>
    /// Adds the supplied items to the collection and raises a single <see cref="CollectionChanged"/> event
    /// when the operation is complete.
    /// </summary>
    /// <param name="items">The items to add.</param>
    public void AddRange(IEnumerable<T> items, bool notifyAfter = true)
    {
        if (null == items)
        {
            throw new ArgumentNullException("items");
        }
        if (items.Any())
        {
            try
            {
                SuppressChangeNotification();
                CheckReentrancy();
                foreach (var item in items)
                {
                    Add(item);
                }
            }
            finally
            {
                if (notifyAfter)
                    FireChangeNotification();
                suppressOnCollectionChanged = false;
            }
        }
    }

    /// <summary>
    /// Replaces the content of the collection with the supplied items and raises a single <see cref="CollectionChanged"/> event
    /// when the operation is complete.
    /// </summary>
    /// <param name="items">The items to replace the current content.</param>
    public void ReplaceContent(IEnumerable<T> items)
    {
        SuppressChangeNotification();
        ClearItems();
        AddRange(items);
    }

    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        if (!suppressOnCollectionChanged)
        {
#if NoCrossThreadSupport
            base.OnCollectionChanged(e);
#else
            using (BlockReentrancy())
            {
                NotifyCollectionChangedEventHandler eventHandler = CollectionChanged;
                if (eventHandler == null)
                    return;

                Delegate[] delegates = eventHandler.GetInvocationList();

                // Walk the invocation list
                foreach (NotifyCollectionChangedEventHandler handler in delegates)
                {
                    DispatcherObject dispatcherObject = handler.Target as DispatcherObject;

                    // If the subscriber is a DispatcherObject and different thread
                    if (dispatcherObject != null && !dispatcherObject.CheckAccess())
                    {
                        // Invoke handler in the target dispatcher's thread
                        dispatcherObject.Dispatcher.Invoke(DispatcherPriority.DataBind, handler, this, e);
                    }
                    else // Execute handler as is
                        handler(this, e);
                }
            }
#endif
        }
    }

I have tried using a List but I got the same behaviour. Any ideas?

PJonHar
  • 19
  • 4
  • 2
    Besides that there is not enough detail in your question, two notes: 1. You don't need an ObservableCollection if you re-populate the whole collection. Use an ordinary List and assign a new List instance to a property with change notification. 2. Do not run a Task without awaiting it. – Clemens Feb 24 '21 at 10:50
  • 1
    It's not clear what you are doing. Whatever Parse is doing, is likely to prevent the ListView from updating itself. ListView is using virtualization. So better partition your data (given that you have that huge data set) and process the visible items first (related to the viewport and page size of the VirtualizingStackPanel). Then process more on scroll. It's impossible to tell you what takes that long or causes the UI to freeze. It can be anything. A **minimal** example that reproduces this problem allows us to review your code. As Clemens said, please provide more details if you expect help. – BionicCode Feb 24 '21 at 11:03
  • There was a link to a minimal example project, I have now highlighted this. I have added more code to the question. Not sure what else I could add to give you sufficient insight. – PJonHar Feb 24 '21 at 11:55
  • Showing us all your code does not necessarily add any details. You should explain what exactly you are trying to achieve, and show only those parts of your code that are relevant to your specific approach. Of course also explain why the standard approach, i.e. an asynchronously populated ObservableCollection with [EnableCollectionSynchronization](https://stackoverflow.com/a/14602121/1136211) will not work for you. – Clemens Feb 24 '21 at 12:33
  • Is that any better? Its hard to put into words, when a simple run of the example project provided would speak a thousand words. – PJonHar Feb 24 '21 at 13:08
  • Can you please reopen the question, so that others can also see. – PJonHar Feb 24 '21 at 13:21
  • @PJonHar - You don't need to speak a thousand words. You recreate the problem using the simplest code. Your application is single threaded, so when the application reads the file, it must read each individual record. I suspect you also don't understand that you are not using an asynchronously task but a synchronous task. – Security Hound Feb 24 '21 at 15:28
  • Has anyone download and ran the project? The application seems to me that it is running in separate threads as the 'finished' event gets fired and handled for the parsing. If it is not I would be grateful if you could tell me where I am going wrong. – PJonHar Feb 24 '21 at 16:34

0 Answers0