Интерфейс подвисает при отображении логов в режиме реального времени
В 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 шт):
Проблема в том, что добавление текста в строку переписыванием ее содержимого полностью влечет за собой перерисовку содержимого всей строки.
Для того, чтобы объехать эту проблему, нам понадобится 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).