Что я ищу:
- Журналирование в реальном времени, без опроса
- Выделен синтаксис
- Ограничено определенным количеством записей журнала
- Прокрутка вверх останавливает представление, чтобы пользователь мог прочитать, что происходит (хотя я предполагаю, что сам текст начнет двигаться, как только буфер журнала заполнится, не знаю, как это обойти)
- Журналы записываются сервером, поэтому при открытии журналов уже есть полная история
- Использование Prism.js в качестве библиотеки подсветки
- Пользовательская коллекция LimitedSizeObservableCollection, помогающая с пунктами 3 и 5
Код: Выделить всё
using System.Collections.ObjectModel;
namespace Commons;
///
/// ObservableCollection that automatically removes oldest elements when it reaches specified size
///
/// Type of elements
public class LimitedSizeObservableCollection : ObservableCollection
{
///
/// ObservableCollection that automatically removes oldest elements when it reaches specified size
///
/// Type of elements
/// Maximum capacity of collection
/// How many elements to remove once capacity is reached. Has to be lower than Capacity. Default: 10
public LimitedSizeObservableCollection(uint capacity, uint clearCount = 10)
{
Capacity = capacity;
ClearCount = clearCount;
if (Capacity < clearCount)
{
throw new ArgumentException($"Capacity [{Capacity}] has to be higher to clearCount [{clearCount}]", nameof(clearCount));
}
}
public uint Capacity { get; set; }
public uint ClearCount { get; set; }
protected override void InsertItem(int index, T item)
{
base.InsertItem(index, item);
if (Count > Capacity)
{
// Remove last ClearCount items
for (var i = 0; i < ClearCount && Count > 0; i++)
{
RemoveAt(Count - 1);
}
}
}
}
- ViewModel, которая обрабатывает блокировку, чтобы предотвратить проблему с исключением при обновлении, если элементы добавляются во время рендеринга (однако это, похоже, сильно замедляет обновления, вызывая скопление ожидающих потоков)
Код: Выделить всё
using System.Diagnostics;
using System.Text;
using BusinessLogic.Services.Interfaces;
using Commons;
using CommunityToolkit.Mvvm.ComponentModel;
using Serilog.Events;
namespace Charon.Shared.UIComponents.Models;
public partial class LogWindowViewModel : ObservableObject
{
public LimitedSizeObservableCollection Logs { get; }
private readonly Lock _logsGate = new();
private readonly SynchronizationContext _uiContext;
public static IEnumerable LogLevels => Enum.GetValues();
[ObservableProperty]
private LogEventLevel _logEventLevel = LogEventLevel.Information;
private Process? ExtProcess => _ExtService.ExtProcess;
private readonly IExtService _ExtService;
public LogWindowViewModel(IExtService ExtService, uint maxLogs = 1500)
{
_uiContext = SynchronizationContext.Current
?? throw new InvalidOperationException("LogWindowViewModel must be constructed on the UI thread.");
Logs = new LimitedSizeObservableCollection(maxLogs);
_ExtService = ExtService;
_ExtService.ExtProcessChanged += RegisterExtLogListeners;
// Workaround because direct binding to Logs with a converter does not work. Element does not react to PropertyChanged notification. See https://github.com/AvaloniaUI/Avalonia/issues/11610
}
private void AddLog(string message)
{
_uiContext.Post(_ =>
{
lock (_logsGate)
{
Logs.Add(message);
}
}, null);
}
public List GetLogsSnapshot()
{
lock (_logsGate)
{
return Logs.ToList();
}
}
public string GetLogsTextSnapshot()
{
lock (_logsGate)
{
if (Logs.Count == 0)
return string.Empty;
var sb = new StringBuilder(capacity: Logs.Count * 64);
foreach (var line in Logs)
sb.AppendLine(line);
return sb.ToString();
}
}
public void ClearLogs()
{
lock (_logsGate)
{
Logs.Clear();
}
}
private void RegisterExtLogListeners(object? sender, Process? ExtProcess)
{
if (ExtProcess == null) return;
AddLog("Ext connected");
_ExtService.ChangeLogLevel(LogEventLevel);
if (_ExtService.External)
{
AddLog("Process acquired externally. Logging not available");
return;
}
ExtProcess!.OutputDataReceived += (_, args) =>
{
if (args.Data != null) AddLog(args.Data);
};
ExtProcess!.ErrorDataReceived += (_, args) =>
{
if (args.Data != null) AddLog(args.Data);
};
}
}
- LogsView запускает соответствующий JS. Я пробовал разные подходы, и этот сработал лучше всего, поскольку я хотел избегать JS.
Код: Выделить всё
@using Charon.Shared.UIComponents.Models
@using System.Collections.Specialized
@inject LogWindowViewModel LogWindowViewModel
@inject IJSRuntime JSRuntime;
@inject ILogger Logger;
@implements IDisposable
@if (_cleaning)
{
}
else
{
}
@code {
private bool _cleaning;
private NotifyCollectionChangedEventHandler? _logsChangedHandler;
private bool _jsReady;
public void Dispose()
{
if (_logsChangedHandler is not null)
LogWindowViewModel.Logs.CollectionChanged -= _logsChangedHandler;
}
protected override Task OnParametersSetAsync()
{
_logsChangedHandler ??= async void (_, args) =>
{
try
{
if (_cleaning || !_jsReady)
return;
if (args is { Action: NotifyCollectionChangedAction.Add, NewItems: not null })
{
var lines = args.NewItems.Cast().Where(s => s is not null).Cast().ToArray();
if (lines.Length > 0)
await JSRuntime.InvokeVoidAsync("logsView.appendLines", "logsCode", lines, 250);
}
else
{
await JSRuntime.InvokeVoidAsync("logsView.setLogs", "logsCode", LogWindowViewModel.GetLogsTextSnapshot());
}
}
catch (Exception e)
{
Logger.LogError(e, "Error updating logs");
}
};
LogWindowViewModel.Logs.CollectionChanged += _logsChangedHandler;
return base.OnParametersSetAsync();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_jsReady = true;
await JSRuntime.InvokeVoidAsync("logsView.setLogs", "logsCode", LogWindowViewModel.GetLogsTextSnapshot());
}
await base.OnAfterRenderAsync(firstRender);
}
///
/// Clears logs and the related view.
/// Uses a workaround since PrismJS modifies the DOM directly which means Blazor cannot remove the logs by itself.
///
public async Task Clear()
{
_cleaning = true;
await InvokeAsync(StateHasChanged);
LogWindowViewModel.ClearLogs();
await Task.Delay(500);
_cleaning = false;
if (_jsReady)
await JSRuntime.InvokeVoidAsync("logsView.setLogs", "logsCode", string.Empty);
await InvokeAsync(StateHasChanged);
}
}
- Сопровождающий JS, который добавляет элемент пользовательского интерфейса и запускает подсветку синтаксиса на нем.
Displaimer. Для создания этой части я использовал искусственный интеллект, так как я плохо разбираюсь в JS.
Код: Выделить всё
// AI Generated and reviewed. Unfortunately, there were too many issues with adding new entries to logs via just Blazor's DOM manipulation, so this JS helper is used instead
(function () {
const ensureElement = (id) => {
if (!id) return null;
return document.getElementById(id);
};
const highlightNow = (el) => {
if (!el) return;
if (window.Prism && window.Prism.highlightElement) {
window.Prism.highlightElement(el);
}
};
window.logsView = {
setLogs: (codeId, text) => {
const el = ensureElement(codeId);
if (!el) return;
el.textContent = text ?? "";
highlightNow(el);
},
appendLines: (codeId, lines, highlightDelayMs) => {
const el = ensureElement(codeId);
if (!el) return;
if (!Array.isArray(lines) || lines.length === 0) return;
// Use a fragment to minimize layout work.
const frag = document.createDocumentFragment();
for (const line of lines) {
frag.appendChild(document.createTextNode((line ?? "") + "\n"));
}
el.appendChild(frag);
// Highlight when new lines are appended (no timer/debounce).
highlightNow(el);
}
};
})();
Проблемы:
- Плохая производительность и нагрузка на память, задачи по добавлению записей накапливаются.
- Вновь добавленные записи «мигают», поскольку для завершения выделения требуется некоторое время.
Подробнее здесь: https://stackoverflow.com/questions/798 ... ghlighting
Мобильная версия