Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
## Release Notes - Serilog.Sinks.RichTextBox.WinForms.Colored v3.0.1
## Release Notes - Serilog.Sinks.RichTextBox.WinForms.Colored v3.1.0

### Minor Release
### Feature Release

This minor release focuses on config improvements and performance optimization.
This release introduces the ability to clear and restore the log view in a non-destructive way.

### What Changed

- Adjusted MaxLogLines limit From 512 to 2048 lines.
- Fixed a bug where the RichTextBox would not persist the zoom factor.
- Optimized the Concurrent Circular Buffer
- Added `Clear()` and `Restore()` methods to the sink, allowing you to hide all current log entries from the view without deleting them from memory. New log events after clearing are still shown. You can restore the view to show all buffered events at any time (unless overwritten by new logs).
- You can now use `.WriteTo.RichTextBox(richTextBox, out sink, ...)` to capture the sink instance and call `Clear()`/`Restore()` directly.

**Note:** The buffer is still a fixed size (default up to 2048 lines). If you clear and then log enough new events to fill the buffer, the oldest (hidden) events will be overwritten and cannot be restored.

### Resources

Expand Down
28 changes: 26 additions & 2 deletions Demo/Form1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ namespace Demo
public partial class Form1 : Form
{
private RichTextBoxSinkOptions? _options;
private RichTextBoxSink? _sink;
private bool _toolbarsVisible = true;

public Form1()
Expand All @@ -47,10 +48,11 @@ private void Initialize()
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:l}{NewLine}{Exception}",
formatProvider: new CultureInfo("en-US"));

var sink = new RichTextBoxSink(richTextBox1, _options);
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Verbose()
.WriteTo.Sink(sink, LogEventLevel.Verbose)
.WriteTo.RichTextBox(richTextBox1, out _sink, theme: ThemePresets.Literate,
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:l}{NewLine}{Exception}",
formatProvider: new CultureInfo("en-US"))
.CreateLogger();

Log.Debug("Started logger.");
Expand Down Expand Up @@ -317,6 +319,16 @@ private void btnReset_Click(object sender, EventArgs e)
Initialize();
}

private void ClearLog()
{
_sink?.Clear();
}

private void RestoreLog()
{
_sink?.Restore();
}

private void btnAutoScroll_Click(object sender, EventArgs e)
{
if (_options == null)
Expand All @@ -336,6 +348,18 @@ private void Form1_KeyDown(object sender, KeyEventArgs e)
toolStrip1.Visible = _toolbarsVisible;
toolStrip2.Visible = _toolbarsVisible;
}
else if (e.Control && e.KeyCode == Keys.L)
{
// Ctrl+L: Clear the log display
ClearLog();
Log.Information("Log display cleared (Ctrl+L). Use Ctrl+R to restore.");
}
else if (e.Control && e.KeyCode == Keys.R)
{
// Ctrl+R: Restore the log display
RestoreLog();
Log.Information("Log display restored (Ctrl+R).");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
using Serilog.Events;
using Serilog.Parsing;
using Xunit;

namespace Serilog.Tests.Integration
{
public class ClearFunctionalityTests : RichTextBoxSinkTestBase
{
[Fact]
public void Clear_HidesExistingLogEntries_ButShowsNewOnes()
{
var template1 = _parser.Parse("First message");
var logEvent1 = new LogEvent(
DateTimeOffset.Now,
LogEventLevel.Information,
null,
template1,
Array.Empty<LogEventProperty>());

var template2 = _parser.Parse("Second message");
var logEvent2 = new LogEvent(
DateTimeOffset.Now,
LogEventLevel.Information,
null,
template2,
Array.Empty<LogEventProperty>());

_sink.Emit(logEvent1);
_sink.Emit(logEvent2);

// Wait with timeout
var timeout = TimeSpan.FromSeconds(5);
var startTime = DateTime.UtcNow;
while (string.IsNullOrWhiteSpace(_richTextBox.Text) && DateTime.UtcNow - startTime < timeout)
{
Thread.Sleep(50);
}

var initialText = _richTextBox.Text;
Assert.Contains("First message", initialText);
Assert.Contains("Second message", initialText);

_sink.Clear();

// Wait with timeout
startTime = DateTime.UtcNow;
while (_richTextBox.Text.Contains("First message") && DateTime.UtcNow - startTime < timeout)
{
Thread.Sleep(50);
}

var clearedText = _richTextBox.Text;
Assert.DoesNotContain("First message", clearedText);
Assert.DoesNotContain("Second message", clearedText);

var template3 = _parser.Parse("Third message after clear");
var logEvent3 = new LogEvent(
DateTimeOffset.Now,
LogEventLevel.Information,
null,
template3,
Array.Empty<LogEventProperty>());

_sink.Emit(logEvent3);

// Wait with timeout
startTime = DateTime.UtcNow;
while (!_richTextBox.Text.Contains("Third message after clear") && DateTime.UtcNow - startTime < timeout)
{
Thread.Sleep(50);
}

var afterClearText = _richTextBox.Text;
Assert.DoesNotContain("First message", afterClearText);
Assert.DoesNotContain("Second message", afterClearText);
Assert.Contains("Third message after clear", afterClearText);
}

[Fact]
public void Restore_RestoresAllLogEntries_AfterClear()
{
var template1 = _parser.Parse("First message");
var logEvent1 = new LogEvent(
DateTimeOffset.Now,
LogEventLevel.Information,
null,
template1,
Array.Empty<LogEventProperty>());

var template2 = _parser.Parse("Second message");
var logEvent2 = new LogEvent(
DateTimeOffset.Now,
LogEventLevel.Information,
null,
template2,
Array.Empty<LogEventProperty>());

_sink.Emit(logEvent1);
_sink.Emit(logEvent2);

// Wait with timeout
var timeout = TimeSpan.FromSeconds(5);
var startTime = DateTime.UtcNow;
while (string.IsNullOrWhiteSpace(_richTextBox.Text) && DateTime.UtcNow - startTime < timeout)
{
Thread.Sleep(50);
}

_sink.Clear();

// Wait with timeout
startTime = DateTime.UtcNow;
while (_richTextBox.Text.Contains("First message") && DateTime.UtcNow - startTime < timeout)
{
Thread.Sleep(50);
}

var template3 = _parser.Parse("Third message after clear");
var logEvent3 = new LogEvent(
DateTimeOffset.Now,
LogEventLevel.Information,
null,
template3,
Array.Empty<LogEventProperty>());

_sink.Emit(logEvent3);

// Wait with timeout
startTime = DateTime.UtcNow;
while (!_richTextBox.Text.Contains("Third message after clear") && DateTime.UtcNow - startTime < timeout)
{
Thread.Sleep(50);
}

_sink.Restore();

// Wait with timeout
startTime = DateTime.UtcNow;
while (!_richTextBox.Text.Contains("First message") && DateTime.UtcNow - startTime < timeout)
{
Thread.Sleep(50);
}

var restoredText = _richTextBox.Text;
Assert.Contains("First message", restoredText);
Assert.Contains("Second message", restoredText);
Assert.Contains("Third message after clear", restoredText);
}

[Fact]
public void Clear_WithoutAnyMessages_DoesNotThrow()
{
_sink.Clear();

// Wait with timeout
var timeout = TimeSpan.FromSeconds(5);
var startTime = DateTime.UtcNow;
while (_richTextBox.Text.Length > 0 && DateTime.UtcNow - startTime < timeout)
{
Thread.Sleep(50);
}

Assert.True(string.IsNullOrWhiteSpace(_richTextBox.Text));
}

[Fact]
public void Restore_WithoutPreviousClear_DoesNotThrow()
{
var template = _parser.Parse("Test message");
var logEvent = new LogEvent(
DateTimeOffset.Now,
LogEventLevel.Information,
null,
template,
Array.Empty<LogEventProperty>());

_sink.Emit(logEvent);

// Wait with timeout
var timeout = TimeSpan.FromSeconds(5);
var startTime = DateTime.UtcNow;
while (!_richTextBox.Text.Contains("Test message") && DateTime.UtcNow - startTime < timeout)
{
Thread.Sleep(50);
}

_sink.Restore();

// Wait with timeout
startTime = DateTime.UtcNow;
while (!_richTextBox.Text.Contains("Test message") && DateTime.UtcNow - startTime < timeout)
{
Thread.Sleep(50);
}

var text = _richTextBox.Text;
Assert.Contains("Test message", text);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,27 @@ protected string RenderAndGetText(LogEvent logEvent, string outputTemplate, IFor

public virtual void Dispose()
{
GC.SuppressFinalize(this);
if (!_disposed)
if (_disposed) return;

_disposed = true;

try
{
// Dispose sink first to stop background processing
_sink?.Dispose();
_richTextBox?.Dispose();
_disposed = true;

// Then dispose the RichTextBox
if (_richTextBox != null && !_richTextBox.IsDisposed)
{
_richTextBox.Dispose();
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error during test cleanup: {ex}");
}

GC.SuppressFinalize(this);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,39 @@ public static LoggerConfiguration RichTextBox(
var sink = new RichTextBoxSink(richTextBoxControl, options, renderer);
return sinkConfiguration.Sink(sink, minimumLogEventLevel, levelSwitch);
}

/// <summary>
/// Adds a sink that writes log events to the specified Windows Forms <see cref="RichTextBox"/>
/// using colour-coded rich-text formatting, returning the sink instance for direct access.
/// </summary>
/// <param name="sinkConfiguration">The logger sink configuration this extension method operates on.</param>
/// <param name="richTextBoxControl">The target <see cref="RichTextBox"/> instance that will display the log output.</param>
/// <param name="sink">Outputs the created sink instance, which can be used to call Clear() and Restore() methods.</param>
/// <param name="theme">Optional theme controlling colours of individual message tokens. When <c>null</c>, <see cref="Serilog.Sinks.RichTextBoxForms.Themes.ThemePresets.Literate"/> is used.</param>
/// <param name="autoScroll">When <c>true</c> (default) the control automatically scrolls to the newest log entry.</param>
/// <param name="maxLogLines">Maximum number of log events retained in the circular buffer and rendered in the control.</param>
/// <param name="outputTemplate">Message template that controls the textual representation of each log event.</param>
/// <param name="formatProvider">Culture-specific or custom formatting provider, or <c>null</c> to use the invariant culture.</param>
/// <param name="minimumLogEventLevel">Minimum level below which events are ignored by this sink.</param>
/// <param name="levelSwitch">Optional switch allowing the minimum log level to be changed at runtime.</param>
/// <returns>A <see cref="LoggerConfiguration"/> object that can be further configured.</returns>
public static LoggerConfiguration RichTextBox(
this LoggerSinkConfiguration sinkConfiguration,
RichTextBox richTextBoxControl,
out RichTextBoxSink sink,
Theme? theme = null,
bool autoScroll = true,
int maxLogLines = 256,
string outputTemplate = OutputTemplate,
IFormatProvider? formatProvider = null,
LogEventLevel minimumLogEventLevel = LogEventLevel.Verbose,
LoggingLevelSwitch? levelSwitch = null)
{
var appliedTheme = theme ?? ThemePresets.Literate;
var renderer = new TemplateRenderer(appliedTheme, outputTemplate, formatProvider);
var options = new RichTextBoxSinkOptions(appliedTheme, autoScroll, maxLogLines, outputTemplate, formatProvider);
sink = new RichTextBoxSink(richTextBoxControl, options, renderer);
return sinkConfiguration.Sink(sink, minimumLogEventLevel, levelSwitch);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@
<TargetFrameworks>net462;net471;net6.0-windows;net8.0-windows;net9.0-windows;netcoreapp3.0-windows;netcoreapp3.1-windows</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<UseWindowsForms>true</UseWindowsForms>
<Version>3.0.1</Version>
<Version>3.1.0</Version>
<PackageReleaseNotes>
- Adjusted MaxLogLines constraint from 512 to 2048 lines.
- Fixed bug where the RTB control would not persist the zoom factor.
- Minor improvements to the concurrent circular buffer.
- Added `Clear()` and `Restore()` methods to hide or show buffered log entries in the view without deleting them.
- You can now access the sink instance via `.WriteTo.RichTextBox(richTextBox, out sink, ...)` to call these methods.

**Note:** The buffer is fixed size; old entries may be lost if overwritten after clearing.

See repository for more information:
https://github.com/vonhoff/Serilog.Sinks.RichTextBox.WinForms.Colored
Expand Down
Loading
Loading