Интерфейс подвисает при отображении логов в режиме реального времени

В ScrollViewer у меня идет отображенные пакетов(вечный спам),интерфейс зависает и сильная нагрузка на цп.

Где происходит вывод логов системы:

<ScrollViewer c:AutoScrollBehavior.AutoScroll="True"  Background="White"  Grid.Row="1" Content="{Binding LogSystem, Mode=OneWay}" Margin="10" />

MainWindowViewModel

            private string logSystem;
            public string LogSystem
            {
                get { return logSystem; }
                set
                {
                    logSystem = value;
                    OnPropertyChanged("LogSystem");
                }
            }



 #region LogMessage
    /// <summary>
    /// Очистить журнал, если сообщение не предоставлено или пусто.
    /// </summary>
    /// <param name="msg"></param>
    private void Log(string msg = "")
    {
        if (!exit)
        {
            if (msg.Length > 0)
            {
                LogSystem += string.Format("[ {0} ] {1}{2}", DateTime.Now.ToString("HH:mm"), msg, Environment.NewLine);
            }
            else
            {
                logSystem = string.Empty;
            }
        }

    }

    private string ErrorMsg(string msg)
    {
        return string.Format("ERROR: {0}", msg);
    }

    private string SystemMsg(string msg)
    {
        return string.Format("SYSTEM: {0}", msg);
    }
    #endregion

Connection

 private void Connection(MyClient obj)
        {
            Log(SystemMsg(string.Format("PTU has connected")));
            IpClient = $"{((IPEndPoint)obj.client.Client.RemoteEndPoint).Address}:{((IPEndPoint)obj.client.Client.RemoteEndPoint).Port}";
            Send("Create Menu", "02-30-00-80-00-01-11-11-ad-09-01-00-00-00-60-8c-90-02-01-20-05-00-10-80-00-00-00-02-42-00-02-05-dc-00-00-00-11-00-00-12-b2-09-02-00-00-00-60-8c-91-02-01-20-05-00-10-00-00-00-00-02-40-00-01-05-de-00-00-00-11-00-00-b1-b0-00-00-c8-13-02-00-00-00-00-00-00-00-00-11-b2-af-00-00-c8-13-02-00-00-00-00-00-00-00-00-11-b3-b1-00-60-88-10-02-10-00-00-00-00-00-00-00-11-b4-ae-00-60-88-10-02-10-00-00-00-00-00-00-00-11-00-01-00-01-10-49-03");
            
            while (obj.client.Connected && active)
            {
                try
                {
                    obj.stream.BeginRead(obj.buffer, 0, obj.buffer.Length, new AsyncCallback(Read), obj);
                    obj.handle.WaitOne();
                }
                catch (Exception ex)
                {
                    Log(ErrorMsg(ex.Message));
                }
                
            }
            obj.client.Close();
            IpClient = string.Empty;
            Log(SystemMsg("PTU has disconnected"));
        }

Read

 private void Read(IAsyncResult result)
        {
            MyClient obj = (MyClient)result.AsyncState;
            int bytes = 0;
            if (obj.client.Connected)
            {
                try
                {
                    bytes = obj.stream.EndRead(result);
                }
                catch (Exception ex)
                {
                    Log(ErrorMsg(ex.Message));
                }
            }
            if (bytes > 0)
            {
                obj.data.AppendFormat("{0}", Encoding.UTF8.GetString(obj.buffer, 0, bytes));
                try
                {
                    if (obj.stream.DataAvailable)
                    {
                        obj.stream.BeginRead(obj.buffer, 0, obj.buffer.Length, new AsyncCallback(Read), obj);
                    }
                    else
                    {
                        var hexString = BitConverter.ToString(obj.buffer,0,bytes);
                        var name = getterGataTrain.WhatsIt(hexString.ToString());
                        if (name != null)
                        {
                            Log($"PTU->Train: [{name}] {hexString}");

                            switch (name)
                            {  //TRIAL RUN RECORD
                                case "TRIAL RUN RECORD":
                                    StringBuilder hex0 = new StringBuilder();
                                    hex0.Append("02-30-00-64-F1-03-11-21-03-03-07-24-59-FF-AD-00-");//02-30-00-64-F1-03-11-21-03-03-07
                                    hex0.Append("49-05-D8-0F-C0-2E-16-2E-2E-2E-18-15-16-14-14-16-");
                                    hex0.Append("00-00-00-00-00-00-00-00-00-DB-DB-DB-79-83-7B-7D-");
                                    hex0.Append("7A-83-43-44-96-96-3A-39-6A-01-2E-00-0A-00-00-10-");
                                    hex0.Append("00-00-00-00-00-00-55-00-14-41-00-08-01-10-41-10-");
                                    hex0.Append("FF-0F-E0-3F-FF-FF-50-01-00-FE-47-01-01-01-01-10-");
                                    hex0.Append("50-03-00-00-00-00-00-00-00-00-00-00-00-00-00-00");
                                    Send("TRIAL RUN RECORD", hex0.ToString());
                                    break;


                            }

                        }
                        else Log($"PTU->Train: {hexString}");
                        obj.data.Clear();
                        obj.handle.Set();

                    }
                }
                catch (Exception ex)
                {
                    obj.data.Clear();
                    Log(ErrorMsg(ex.Message));
                    obj.handle.Set();
                }
            }
            else
            {
                obj.client.Close();
                obj.handle.Set();
            }
        }

Send

 private void Send(string message,string hex)
    {   Log($"Train->PTU:{hex}");
        byte[] Send = Convertors.StringToByteArray(hex);
        obj.stream.Write(Send, 0, Send.Length);
    }

Ответы (1 шт):

Автор решения: aepot

Проблема в том, что добавление текста в строку переписыванием ее содержимого полностью влечет за собой перерисовку содержимого всей строки.

Для того, чтобы объехать эту проблему, нам понадобится TextBox, пусть даже в режиме IsReadOnly = true. У текстбокса есть метод TextBox.AppendText(), который быстро и безболезненно добавляет какой-то текст в конец текстбокса. Даже если текста в нем очень много, добавление происходит очень быстро.

Но есть нюанс, связанный с тем, что бесконечно все-таки добавлять в него строки не стоит, потому что если у вас длина текста превысит (в байтах) 2 ГБ, то приложение упадет, к тому же я не думаю, что на больших объемах текста оно не будет тормозить. Поэтому я бы ввел ограничение на максимальное количество строк. И здесь нам помогут еще метод TextBox.Select() и свойство TextBox.SelectedText.

Обратите внимание, я вообще не работаю с свойством TextBox.Text. Обращение к данному свойству вызывает рендер всего содержимого текстбокса в string, что может при больших объемах текста и частых вызовах повлечь те же самые тормоза.

Так как прямая работа с контролами в MVVM - дело не чистое, я создал наследника текстбокса.

public class LogTextBox : TextBox
{
    public static readonly DependencyProperty TotalMaxLinesProperty =
        DependencyProperty.Register(nameof(TotalMaxLines), typeof(int), typeof(LogTextBox), new PropertyMetadata(-1));

    public static readonly DependencyProperty LineSourceProperty =
        DependencyProperty.Register(nameof(LineSource), typeof(string), typeof(LogTextBox), new FrameworkPropertyMetadata(string.Empty, new PropertyChangedCallback(LineAdded)));

    public int TotalMaxLines
    {
        get { return (int)GetValue(TotalMaxLinesProperty); }
        set { SetValue(TotalMaxLinesProperty, value); }
    }

    public string LineSource
    {
        get => (string)GetValue(LineSourceProperty);
        set => SetValue(LineSourceProperty, value);
    }

    private static void LineAdded(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        LogTextBox box = (LogTextBox)d;
        string newText = (string)e.NewValue;
        if (newText.Length == 0)
        {
            box.Text = string.Empty;
            return;
        }

        if (box.LineCount > 1 || box.Text.Length > 0) 
            box.AppendText(Environment.NewLine);
        box.AppendText(newText);
        int maxLines = box.TotalMaxLines;
        if (maxLines > 0)
        {
            int linesCountToRemove = box.LineCount - maxLines;
            if (linesCountToRemove > 0)
            {
                int index = 0;
                for (int i = 0; i < linesCountToRemove; i++)
                    index += box.GetLineLength(i);
                box.Select(0, index);
                box.SelectedText = string.Empty;
            }
        }
        box.ScrollToEnd();
    }
}
  • Свойство TotalMaxLines отвечает за удаление лишних старых строк. Если оно 0 или меньше, то ограничения нет.
  • LineSource отвечает за запись новой строки в текстбокс, при этом разделение строк переносом на новую строку происходит автоматически. То есть вам не следует вручную добавлять Environment.NewLine в коде к своим строчкам.

Далее вкручиваю этот контрол в окно

<local:LogTextBox Margin="5" ScrollViewer.VerticalScrollBarVisibility="Auto" IsReadOnly="True" LineSource="{Binding LogSystem}" TotalMaxLines="10"/>

И с помощью вот такого кода я это все тестировал

public class MainViewModel : NotifyPropertyChanged
{
    private string logSystem;

    public string LogSystem
    {
        get => logSystem;
        set
        {
            logSystem = value;
            OnPropertyChanged();
        }
    }

    private void Log(string msg = "")
    {
        LogSystem = msg.Length > 0 ? string.Format("[ {0} ] {1}", DateTime.Now.ToString("HH:mm"), msg) : string.Empty;
    }

    private async void LogLoop()
    {
        int i = 0;
        while (true)
        {
            Log($"Hello world {i++}");
            await Task.Delay(10);
        }
    }

    public MainViewModel()
    {
        LogLoop();
    }
}

public class NotifyPropertyChanged : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

Внимание на LogSystem = string.Format, здесь поменялся код.

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


Следующая ваша проблема - это вызов метода Log из стороннего потока. Чтобы перенаправить вывод в UI поток, можно использвать Dispatcher, или например Progress<T>.

Вот так может измениться код, чтобы вывод в интерфейс стал потокобезопасным.

public class MainViewModel : NotifyPropertyChanged
{
    private IProgress<string> _logToUI;

    private string logSystem;
    public string LogSystem
    {
        get => logSystem;
        set
        {
            logSystem = value;
            OnPropertyChanged();
        }
    }

    private void Log(string msg = "")
    {
        _logToUI.Report(msg.Length > 0 ? string.Format("[ {0} ] {1}", DateTime.Now.ToString("HH:mm"), msg) : string.Empty);
    }

    private async void LogLoop()
    {
        int i = 0;
        while (true)
        {
            Log($"Hello world {i++}");
            await Task.Delay(10);
        }
    }

    public MainViewModel()
    {
        _logToUI = new Progress<string>(s => LogSystem = s);
        LogLoop();
    }
}

Progress автоматически перенаправляет вывод в тот контекст синхронизации, в котором он был создан (new Progress).

→ Ссылка