From f2b3f2155d78c87b797b704302a0878c231ee3bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=2EB=C3=B6xler?= Date: Tue, 5 May 2026 15:04:39 +0200 Subject: [PATCH 1/5] refactor(wpf): split log viewer into NLogViewerBase and cache-backed NLogViewer --- app/Sentinel.NLogViewer.App/MainWindow.xaml | 6 +- .../Models/LogTabViewModel.cs | 61 +- .../ViewModels/MainViewModel.cs | 8 +- .../Helper/MessageHighlighter.cs | 4 +- ui/Sentinel.NLogViewer.Wpf/NLogViewer.xaml | 4 +- ui/Sentinel.NLogViewer.Wpf/NLogViewer.xaml.cs | 1843 +--------------- ui/Sentinel.NLogViewer.Wpf/NLogViewerBase.cs | 1878 +++++++++++++++++ .../Themes/Generic.xaml | 62 +- 8 files changed, 1985 insertions(+), 1881 deletions(-) create mode 100644 ui/Sentinel.NLogViewer.Wpf/NLogViewerBase.cs diff --git a/app/Sentinel.NLogViewer.App/MainWindow.xaml b/app/Sentinel.NLogViewer.App/MainWindow.xaml index 3b6378f..648ed3a 100644 --- a/app/Sentinel.NLogViewer.App/MainWindow.xaml +++ b/app/Sentinel.NLogViewer.App/MainWindow.xaml @@ -18,11 +18,11 @@ - + ItemsSource="{Binding DataContext.LogEntries, RelativeSource={RelativeSource AncestorType=ContentPresenter}}" + MaxCount="{Binding DataContext.MaxCount, RelativeSource={RelativeSource AncestorType=ContentPresenter}}"/> diff --git a/app/Sentinel.NLogViewer.App/Models/LogTabViewModel.cs b/app/Sentinel.NLogViewer.App/Models/LogTabViewModel.cs index 395a02a..a31ba6b 100644 --- a/app/Sentinel.NLogViewer.App/Models/LogTabViewModel.cs +++ b/app/Sentinel.NLogViewer.App/Models/LogTabViewModel.cs @@ -1,23 +1,25 @@ -using System.Collections.Generic; +using System.Collections.ObjectModel; using System.ComponentModel; using System.Runtime.CompilerServices; using NLog; -using Sentinel.NLogViewer.Wpf.Targets; -using System.Reactive.Linq; -using System.Reactive.Subjects; namespace Sentinel.NLogViewer.App.Models { /// /// ViewModel for a log tab in the TabControl /// - public class LogTabViewModel : INotifyPropertyChanged, ICacheTarget + public class LogTabViewModel : INotifyPropertyChanged { private string _header = string.Empty; private string _targetName = string.Empty; private int _logCount; private int _maxCount = 10000; + /// + /// Backing collection bound to . + /// + public ObservableCollection LogEntries { get; } = new(); + public string Header { get => _header; @@ -70,60 +72,23 @@ public int MaxCount } } - public event PropertyChangedEventHandler? PropertyChanged; + public event PropertyChangedEventHandler? PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } - private readonly object _gate = new(); - private readonly List _buffer = new(); - private Subject? _liveSubject; - private bool _hasConnected; - - /// - /// Observable stream of log events. Replays buffered events only to the first subscriber; later subscribers receive only new events from subscription time. - /// - public IObservable Cache => Observable.Defer(() => - { - lock (_gate) - { - if (!_hasConnected) - { - _hasConnected = true; - _liveSubject = new Subject(); - var snapshot = _buffer.ToList(); - _buffer.Clear(); - return snapshot.ToObservable().Concat(_liveSubject); - } - return _liveSubject!; - } - }).Publish().RefCount(); - /// - /// Pushes a log event into the cache. Subscribers (e.g. NLogViewer) receive it immediately or via replay when they subscribe later. + /// Adds a log event to (same reference the viewer binds to). /// - /// The log event to add. public void AddLogEvent(LogEventInfo logEvent) { - lock (_gate) - { - if (_liveSubject != null) - { - _liveSubject.OnNext(logEvent); - } - else - { - _buffer.Add(logEvent); - while (_buffer.Count > MaxCount) - { - _buffer.RemoveAt(0); - } - } - } + LogEntries.Add(logEvent); + while (MaxCount >= 0 && LogEntries.Count > MaxCount) + LogEntries.RemoveAt(0); + LogCount++; } } } - diff --git a/app/Sentinel.NLogViewer.App/ViewModels/MainViewModel.cs b/app/Sentinel.NLogViewer.App/ViewModels/MainViewModel.cs index 1f89f18..a8132a2 100644 --- a/app/Sentinel.NLogViewer.App/ViewModels/MainViewModel.cs +++ b/app/Sentinel.NLogViewer.App/ViewModels/MainViewModel.cs @@ -22,8 +22,6 @@ using Sentinel.NLogViewer.App.Models; using Sentinel.NLogViewer.App.Services; using Sentinel.NLogViewer.App; -using Sentinel.NLogViewer.Wpf.Targets; - namespace Sentinel.NLogViewer.App.ViewModels; /// @@ -663,7 +661,7 @@ private void ExportLogs() /// Finds the NLogViewer instance in the currently selected tab's visual tree /// /// The NLogViewer instance if found, null otherwise - private Wpf.NLogViewer? FindNLogViewerInTab() + private Wpf.NLogViewerBase? FindNLogViewerInTab() { if (SelectedTab == null) return null; @@ -689,14 +687,14 @@ private void ExportLogs() if (contentControl != null && contentControl.DataContext == SelectedTab) { // Found the content control for the selected tab, now find NLogViewer - var nLogViewer = FindVisualChild(contentControl); + var nLogViewer = FindVisualChild(contentControl); if (nLogViewer != null) return nLogViewer; } } // Fallback: Search directly in TabControl for NLogViewer - return FindVisualChild(tabControl); + return FindVisualChild(tabControl); } /// diff --git a/ui/Sentinel.NLogViewer.Wpf/Helper/MessageHighlighter.cs b/ui/Sentinel.NLogViewer.Wpf/Helper/MessageHighlighter.cs index 3800792..23a0200 100644 --- a/ui/Sentinel.NLogViewer.Wpf/Helper/MessageHighlighter.cs +++ b/ui/Sentinel.NLogViewer.Wpf/Helper/MessageHighlighter.cs @@ -11,7 +11,7 @@ namespace Sentinel.NLogViewer.Wpf.Helper { /// /// Attached behavior to render a TextBlock with highlighted segments based on ActiveSearchTerms. - /// It uses the ancestor NLogViewer to access resolvers and highlight brush. + /// It uses an ancestor to access resolvers and highlight brush. /// public static class MessageHighlighter { @@ -58,7 +58,7 @@ private static void TextBlockOnUnloaded(object sender, RoutedEventArgs e) /// private static void TryUpdate(TextBlock textBlock) { - var nlogViewer = FindAncestor(textBlock); + var nlogViewer = FindAncestor(textBlock); if (nlogViewer == null) return; diff --git a/ui/Sentinel.NLogViewer.Wpf/NLogViewer.xaml b/ui/Sentinel.NLogViewer.Wpf/NLogViewer.xaml index 4247e6d..ccff646 100644 --- a/ui/Sentinel.NLogViewer.Wpf/NLogViewer.xaml +++ b/ui/Sentinel.NLogViewer.Wpf/NLogViewer.xaml @@ -1,4 +1,4 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/ui/Sentinel.NLogViewer.Wpf/NLogViewer.xaml.cs b/ui/Sentinel.NLogViewer.Wpf/NLogViewer.xaml.cs index c0c1058..bdd4bf9 100644 --- a/ui/Sentinel.NLogViewer.Wpf/NLogViewer.xaml.cs +++ b/ui/Sentinel.NLogViewer.Wpf/NLogViewer.xaml.cs @@ -1,511 +1,24 @@ using System; -using System.Collections.ObjectModel; using System.ComponentModel; -using System.Linq; using System.Reactive.Concurrency; using System.Reactive.Linq; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Input; -using System.Windows.Media; using System.Windows.Threading; -using Sentinel.NLogViewer.Wpf.Extensions; -using Sentinel.NLogViewer.Wpf.Helper; -using Sentinel.NLogViewer.Wpf.Resolver; -using Sentinel.NLogViewer.Wpf.Targets; using NLog; -using System.IO; -using System.Collections.Generic; -using Sentinel.NLogViewer.App.Models; +using Sentinel.NLogViewer.Wpf.Targets; namespace Sentinel.NLogViewer.Wpf { /// - /// Base class for search terms that can match against text - /// - public abstract class SearchTerm - { - public string Text { get; protected set; } - - protected SearchTerm(string text) - { - Text = text ?? throw new ArgumentNullException(nameof(text)); - } - - /// - /// Determines if the search term matches the given text - /// - /// The text to match against - /// True if the text matches the search term - public abstract bool Match(string text); - - /// - /// Determines if the search term matches any of the given texts - /// - /// The texts to match against - /// True if any text matches the search term - public bool MatchAny(params string[] texts) - { - if (texts == null) return false; - return texts.Any(text => !string.IsNullOrEmpty(text) && Match(text)); - } - - public override string ToString() - { - return Text; - } - } - - /// - /// A search term that performs case-insensitive substring matching - /// - public class TextSearchTerm : SearchTerm - { - private readonly string _lowercaseText; - - public TextSearchTerm(string text) : base(text) - { - _lowercaseText = text.ToLowerInvariant(); - } - - public override bool Match(string text) - { - if (string.IsNullOrEmpty(text)) return false; - if (string.IsNullOrEmpty(_lowercaseText)) return false; - return text.ToLowerInvariant().Contains(_lowercaseText); - } - - public override string ToString() - { - return Text; - } - } - - /// - /// A search term that performs regex pattern matching - /// - public class RegexSearchTerm : SearchTerm - { - private readonly Regex _regex; - - public RegexSearchTerm(string pattern) : base(pattern) - { - try - { - _regex = new Regex(pattern, RegexOptions.Compiled); - } - catch (ArgumentException ex) - { - throw new ArgumentException($"Invalid regex pattern: {ex.Message}", nameof(pattern), ex); - } - } - - public override bool Match(string text) - { - if (string.IsNullOrEmpty(text)) return false; - return _regex.IsMatch(text); - } - - public override string ToString() - { - return $"/{Text}/"; - } - } - - /// - /// A customizable control for viewing NLog events with filtering and styling capabilities. + /// Log viewer that subscribes to NLog via configuration or the property. /// - public partial class NLogViewer : Control + public partial class NLogViewer : NLogViewerBase { - // ############################################################################################################################## - // Dependency Properties - // ############################################################################################################################## - - #region Dependency Properties - - // ########################################################################################## - // Colors - // ########################################################################################## - - #region Colors - - /// - /// The background for the trace output - /// - [Category("NLogViewerColors")] - [Description("Background brush for Trace level log entries")] - public Brush TraceBackground - { - get => (Brush) GetValue(TraceBackgroundProperty); - set => SetValue(TraceBackgroundProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty TraceBackgroundProperty = - DependencyProperty.Register(nameof(TraceBackground), typeof(Brush), typeof(NLogViewer), - new PropertyMetadata((Brush) (new BrushConverter().ConvertFrom("#D3D3D3")))); - - /// - /// The foreground for the trace output - /// - [Category("NLogViewerColors")] - [Description("Foreground brush for Trace level log entries")] - public Brush TraceForeground - { - get => (Brush) GetValue(TraceForegroundProperty); - set => SetValue(TraceForegroundProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty TraceForegroundProperty = - DependencyProperty.Register(nameof(TraceForeground), typeof(Brush), typeof(NLogViewer), - new PropertyMetadata((Brush) (new BrushConverter().ConvertFrom("#042271")))); - - /// - /// The background for the debug output - /// - [Category("NLogViewerColors")] - [Description("Background brush for Debug level log entries")] - public Brush DebugBackground - { - get => (Brush) GetValue(DebugBackgroundProperty); - set => SetValue(DebugBackgroundProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty DebugBackgroundProperty = - DependencyProperty.Register(nameof(DebugBackground), typeof(Brush), typeof(NLogViewer), - new PropertyMetadata((Brush) (new BrushConverter().ConvertFrom("#90EE90")))); - - /// - /// The foreground for the debug output - /// - [Category("NLogViewerColors")] - [Description("Foreground brush for Debug level log entries")] - public Brush DebugForeground - { - get => (Brush) GetValue(DebugForegroundProperty); - set => SetValue(DebugForegroundProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty DebugForegroundProperty = - DependencyProperty.Register(nameof(DebugForeground), typeof(Brush), typeof(NLogViewer), - new PropertyMetadata((Brush) (new BrushConverter().ConvertFrom("#042271")))); - - /// - /// The background for the info output - /// - [Category("NLogViewerColors")] - [Description("Background brush for Info level log entries")] - public Brush InfoBackground - { - get => (Brush) GetValue(InfoBackgroundProperty); - set => SetValue(InfoBackgroundProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty InfoBackgroundProperty = DependencyProperty.Register(nameof(InfoBackground), - typeof(Brush), typeof(NLogViewer), - new PropertyMetadata((Brush) (new BrushConverter().ConvertFrom("#0000FF")))); - - /// - /// The foreground for the info output - /// - [Category("NLogViewerColors")] - [Description("Foreground brush for Info level log entries")] - public Brush InfoForeground - { - get => (Brush) GetValue(InfoForegroundProperty); - set => SetValue(InfoForegroundProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty InfoForegroundProperty = DependencyProperty.Register(nameof(InfoForeground), - typeof(Brush), typeof(NLogViewer), new PropertyMetadata(Brushes.White)); - - /// - /// The background for the warn output - /// - [Category("NLogViewerColors")] - [Description("Background brush for Warn level log entries")] - public Brush WarnBackground - { - get => (Brush) GetValue(WarnBackgroundProperty); - set => SetValue(WarnBackgroundProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty WarnBackgroundProperty = DependencyProperty.Register(nameof(WarnBackground), - typeof(Brush), typeof(NLogViewer), - new PropertyMetadata((Brush) (new BrushConverter().ConvertFrom("#FFFF00")))); - - /// - /// The foreground for the warn output - /// - [Category("NLogViewerColors")] - [Description("Foreground brush for Warn level log entries")] - public Brush WarnForeground - { - get => (Brush) GetValue(WarnForegroundProperty); - set => SetValue(WarnForegroundProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty WarnForegroundProperty = DependencyProperty.Register(nameof(WarnForeground), - typeof(Brush), typeof(NLogViewer), - new PropertyMetadata((Brush) (new BrushConverter().ConvertFrom("#324B5C")))); - - /// - /// The background for the error output - /// - [Category("NLogViewerColors")] - [Description("Background brush for Error level log entries")] - public Brush ErrorBackground - { - get => (Brush) GetValue(ErrorBackgroundProperty); - set => SetValue(ErrorBackgroundProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty ErrorBackgroundProperty = - DependencyProperty.Register(nameof(ErrorBackground), typeof(Brush), typeof(NLogViewer), - new PropertyMetadata(Brushes.Red)); - - /// - /// The foreground for the error output - /// - [Category("NLogViewerColors")] - [Description("Foreground brush for Error level log entries")] - public Brush ErrorForeground - { - get => (Brush) GetValue(ErrorForegroundProperty); - set => SetValue(ErrorForegroundProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty ErrorForegroundProperty = - DependencyProperty.Register(nameof(ErrorForeground), typeof(Brush), typeof(NLogViewer), - new PropertyMetadata(Brushes.White)); - - /// - /// The background for the fatal output - /// - [Category("NLogViewerColors")] - [Description("Background brush for Fatal level log entries")] - public Brush FatalBackground - { - get => (Brush) GetValue(FatalBackgroundProperty); - set => SetValue(FatalBackgroundProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty FatalBackgroundProperty = - DependencyProperty.Register(nameof(FatalBackground), typeof(Brush), typeof(NLogViewer), - new PropertyMetadata(Brushes.Black)); - - /// - /// The foreground for the fatal output - /// - [Category("NLogViewerColors")] - [Description("Foreground brush for Fatal level log entries")] - public Brush FatalForeground - { - get => (Brush) GetValue(FatalForegroundProperty); - set => SetValue(FatalForegroundProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty FatalForegroundProperty = - DependencyProperty.Register(nameof(FatalForeground), typeof(Brush), typeof(NLogViewer), - new PropertyMetadata(Brushes.Yellow)); - - // ------------------------------------------------------------------------------------------ - // Search Term Chip Colors - // ------------------------------------------------------------------------------------------ - - /// - /// Background brush for plain text search term chips - /// - [Category("NLogViewerColors")] - [Description("Background brush for plain text search term chips")] - public Brush TextSearchTermBackground - { - get => (Brush)GetValue(TextSearchTermBackgroundProperty); - set => SetValue(TextSearchTermBackgroundProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty TextSearchTermBackgroundProperty = - DependencyProperty.Register(nameof(TextSearchTermBackground), typeof(Brush), typeof(NLogViewer), - new PropertyMetadata((Brush)(new BrushConverter().ConvertFrom("#FFFFFBE6")))); - - /// - /// Border brush for plain text search term chips - /// - [Category("NLogViewerColors")] - [Description("Border brush for plain text search term chips")] - public Brush TextSearchTermBorderBrush - { - get => (Brush)GetValue(TextSearchTermBorderBrushProperty); - set => SetValue(TextSearchTermBorderBrushProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty TextSearchTermBorderBrushProperty = - DependencyProperty.Register(nameof(TextSearchTermBorderBrush), typeof(Brush), typeof(NLogViewer), - new PropertyMetadata((Brush)(new BrushConverter().ConvertFrom("#FFFFC107")))); - - /// - /// Foreground brush for plain text search term chips - /// - [Category("NLogViewerColors")] - [Description("Foreground brush for plain text search term chips")] - public Brush TextSearchTermForeground - { - get => (Brush)GetValue(TextSearchTermForegroundProperty); - set => SetValue(TextSearchTermForegroundProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty TextSearchTermForegroundProperty = - DependencyProperty.Register(nameof(TextSearchTermForeground), typeof(Brush), typeof(NLogViewer), - new PropertyMetadata(Brushes.Black)); - - /// - /// Background brush for regex search term chips - /// - [Category("NLogViewerColors")] - [Description("Background brush for regex search term chips")] - public Brush RegexSearchTermBackground - { - get => (Brush)GetValue(RegexSearchTermBackgroundProperty); - set => SetValue(RegexSearchTermBackgroundProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty RegexSearchTermBackgroundProperty = - DependencyProperty.Register(nameof(RegexSearchTermBackground), typeof(Brush), typeof(NLogViewer), - new PropertyMetadata((Brush)(new BrushConverter().ConvertFrom("#FFFFFBE6")))); - - /// - /// Border brush for regex search term chips - /// - [Category("NLogViewerColors")] - [Description("Border brush for regex search term chips")] - public Brush RegexSearchTermBorderBrush - { - get => (Brush)GetValue(RegexSearchTermBorderBrushProperty); - set => SetValue(RegexSearchTermBorderBrushProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty RegexSearchTermBorderBrushProperty = - DependencyProperty.Register(nameof(RegexSearchTermBorderBrush), typeof(Brush), typeof(NLogViewer), - new PropertyMetadata((Brush)(new BrushConverter().ConvertFrom("#FFFFC107")))); - - /// - /// Foreground brush for regex search term chips - /// - [Category("NLogViewerColors")] - [Description("Foreground brush for regex search term chips")] - public Brush RegexSearchTermForeground - { - get => (Brush)GetValue(RegexSearchTermForegroundProperty); - set => SetValue(RegexSearchTermForegroundProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty RegexSearchTermForegroundProperty = - DependencyProperty.Register(nameof(RegexSearchTermForeground), typeof(Brush), typeof(NLogViewer), - new PropertyMetadata(Brushes.Black)); - - /// - /// Foreground brush for the "Regex:" prefix label - /// - [Category("NLogViewerColors")] - [Description("Foreground brush for the 'Regex:' prefix label")] - public Brush RegexPrefixForeground - { - get => (Brush)GetValue(RegexPrefixForegroundProperty); - set => SetValue(RegexPrefixForegroundProperty, value); - } + private IDisposable _subscription; + private bool _isListening; /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty RegexPrefixForegroundProperty = - DependencyProperty.Register(nameof(RegexPrefixForeground), typeof(Brush), typeof(NLogViewer), - new PropertyMetadata((Brush)(new BrushConverter().ConvertFrom("#FFFF8F00")))); - - /// - /// Background brush used to highlight matched search text in the message - /// - [Category("NLogViewerColors")] - [Description("Background brush used to highlight matched search text in the message")] - public Brush SearchHighlightBackground - { - get => (Brush)GetValue(SearchHighlightBackgroundProperty); - set => SetValue(SearchHighlightBackgroundProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty SearchHighlightBackgroundProperty = - DependencyProperty.Register(nameof(SearchHighlightBackground), typeof(Brush), typeof(NLogViewer), - new PropertyMetadata((Brush)(new BrushConverter().ConvertFrom("#FFFF80C0")))); - - #endregion - - // ########################################################################################## - // NLogViewer - // ########################################################################################## - - #region NLogViewer - - /// - /// Is looking if any target with this name is configured and tries to link it + /// Looks up a target with this name in the NLog configuration and links it via . /// [Category("NLogViewer")] public string TargetName @@ -517,10 +30,11 @@ public string TargetName /// /// The DependencyProperty. /// - public static readonly DependencyProperty TargetNameProperty = DependencyProperty.Register(nameof(TargetName), typeof(string), typeof(NLogViewer), new PropertyMetadata(null)); + public static readonly DependencyProperty TargetNameProperty = + DependencyProperty.Register(nameof(TargetName), typeof(string), typeof(NLogViewer), new PropertyMetadata(null)); /// - /// Cache target to subscribe to for log events. When set to a non-null value, StartListen is called with this target. + /// Cache target to subscribe to. When set, is invoked with this instance. /// [Category("NLogViewer")] [Browsable(true)] @@ -546,1330 +60,79 @@ private static void OnCacheTargetChanged(DependencyObject d, DependencyPropertyC instance.StartListen(target); } - /// - /// Private DP to bind to the gui - /// - [Category("NLogViewer")] - public CollectionViewSource LogEvents + static NLogViewer() { - get => (CollectionViewSource) GetValue(LogEventsProperty); - private set => SetValue(LogEventsProperty, value); + DefaultStyleKeyProperty.OverrideMetadata(typeof(NLogViewer), + new FrameworkPropertyMetadata(typeof(NLogViewerBase))); } /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty LogEventsProperty = DependencyProperty.Register(nameof(LogEvents), - typeof(CollectionViewSource), typeof(NLogViewer), new PropertyMetadata(null)); - - /// - /// Automatically scroll to the newest entry + /// Initializes a viewer wired for NLog subscriptions. /// - [Category("NLogViewerControls")] - [Browsable(true)] - [Description("Automatically scroll to the newest log entry when new entries are added")] - public bool AutoScroll + public NLogViewer() { - get => (bool)GetValue(AutoScrollProperty); - set => SetValue(AutoScrollProperty, value); } /// - /// The DependencyProperty. + /// Starts listening by subscribing to the cache observable (explicit target or ). /// - public static readonly DependencyProperty AutoScrollProperty = DependencyProperty.Register(nameof(AutoScroll), typeof(bool), typeof(NLogViewer), new PropertyMetadata(true, AutoScrollChangedCallback)); - - private static void AutoScrollChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs args) - { - if (d is NLogViewer instance) - { - instance.OnAutoScrollChanged(); - } - } - - protected virtual void OnAutoScrollChanged() - { - if (AutoScroll) - PART_ListView?.ScrollToEnd(); - } - - private void UpdateFilter() + public void StartListen(ICacheTarget? target = null) { - if (LogEvents?.View is not CollectionView collectionView) return; - - collectionView.Filter = item => - { - if (item is not LogEventInfo logEvent) return true; + if (_isListening || DesignerProperties.GetIsInDesignMode(this)) + return; - // Apply level filters first - var levelFilterPassed = logEvent.Level.Name switch - { - "Trace" => !TraceFilter, // If TraceFilter is true, hide Trace entries (!true = false) - "Debug" => !DebugFilter, // If DebugFilter is true, hide Debug entries (!true = false) - "Info" => !InfoFilter, // If InfoFilter is true, hide Info entries (!true = false) - "Warn" => !WarnFilter, // If WarnFilter is true, hide Warn entries (!true = false) - "Error" => !ErrorFilter, // If ErrorFilter is true, hide Error entries (!true = false) - "Fatal" => !FatalFilter, // If FatalFilter is true, hide Fatal entries (!true = false) - _ => true - }; + var dispatcher = Window.GetWindow(this)?.Dispatcher + ?? Application.Current?.Dispatcher + ?? Dispatcher.CurrentDispatcher; + if (dispatcher == null) + return; - if (!levelFilterPassed) return false; + target ??= global::Sentinel.NLogViewer.Wpf.Targets.CacheTarget.GetInstance(targetName: TargetName); - // Apply search filters if any active search terms exist - if (ActiveSearchTerms?.Count > 0) - { - var loggerName = logEvent.LoggerName ?? string.Empty; - var message = MessageResolver?.Resolve(logEvent) ?? string.Empty; - - // Separate include and exclude terms based on regex pattern - // Exclude patterns start with "^(?!.*" (negative lookahead) - var includeTerms = ActiveSearchTerms.Where(t => - !(t is RegexSearchTerm regexTerm && regexTerm.Text.StartsWith("^(?!.*", StringComparison.Ordinal))).ToList(); - var excludeTerms = ActiveSearchTerms.Where(t => - t is RegexSearchTerm regexTerm && regexTerm.Text.StartsWith("^(?!.*", StringComparison.Ordinal)).ToList(); - - // AND logic: ALL include terms must match - foreach (var searchTerm in includeTerms) - { - if (!searchTerm.MatchAny(loggerName, message)) - return false; - } - - // Exclude logic: Hide entry if ANY string (loggerName OR message) contains the excluded pattern - // Note: For exclude patterns (negative lookahead ^(?!.*pattern)), if the pattern matches, - // it means the text does NOT contain the excluded pattern. If it doesn't match, the text contains it. - // We need to check each string individually because MatchAny would return true if ANY string matches, - // but we want to hide if ANY string contains the pattern. - foreach (var searchTerm in excludeTerms) - { - // Check each string individually: if either contains the pattern, hide the entry - var loggerContainsPattern = !searchTerm.Match(loggerName); - var messageContainsPattern = !searchTerm.Match(message); - - if (loggerContainsPattern || messageContainsPattern) - return false; // Hide entry if either string contains the excluded pattern - } - - // All include terms matched and no exclude terms matched - return true; - } + _subscription = target.Cache.SubscribeOn(Scheduler.Default) + .Buffer(TimeSpan.FromMilliseconds(100)) + .Where(x => x.Any()) + .ObserveOn(new DispatcherSynchronizationContext(dispatcher)) + .Subscribe(AppendLogEntriesBatch); - return true; - }; - - // Refresh the view to apply the filter - collectionView.Refresh(); + _isListening = true; } - + /// - /// Delete all entries + /// Stops listening by disposing the cache subscription. /// - [Category("NLogViewerControls")] - [Browsable(true)] - [Description("Command to clear all log entries from the viewer")] - public ICommand ClearCommand + public void StopListen() { - get => (ICommand) GetValue(ClearCommandProperty); - set => SetValue(ClearCommandProperty, value); - } + if (!_isListening) + return; - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty ClearCommandProperty = DependencyProperty.Register(nameof(ClearCommand), - typeof(ICommand), typeof(NLogViewer), new PropertyMetadata(null)); + _subscription?.Dispose(); + _subscription = null; + _isListening = false; + } - /// - /// Command to add a new search term - /// - [Category("NLogViewerControls")] - [Browsable(true)] - [Description("Command to add a new search term")] - public ICommand AddSearchTermCommand + /// + protected override void OnPausePropertyChanged(bool isPaused) { - get => (ICommand) GetValue(AddSearchTermCommandProperty); - set => SetValue(AddSearchTermCommandProperty, value); - } + if (DesignerProperties.GetIsInDesignMode(this)) + return; - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty AddSearchTermCommandProperty = DependencyProperty.Register(nameof(AddSearchTermCommand), - typeof(ICommand), typeof(NLogViewer), new PropertyMetadata(null)); + if (isPaused) + StopListen(); + else + StartListen(CacheTarget); + } - /// - /// Command to clear all search terms - /// - [Category("NLogViewerControls")] - [Browsable(true)] - [Description("Command to clear all search terms")] - public ICommand ClearAllSearchTermsCommand + /// + protected override void OnViewerLoaded() { - get => (ICommand) GetValue(ClearAllSearchTermsCommandProperty); - set => SetValue(ClearAllSearchTermsCommandProperty, value); + StartListen(); } - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty ClearAllSearchTermsCommandProperty = DependencyProperty.Register(nameof(ClearAllSearchTermsCommand), - typeof(ICommand), typeof(NLogViewer), new PropertyMetadata(null)); - - /// - /// Command to add a regex search term from a provided text - /// - [Category("NLogViewerControls")] - [Browsable(true)] - [Description("Command to add a regex search term from the provided text")] - public ICommand AddRegexSearchTermCommand - { - get => (ICommand) GetValue(AddRegexSearchTermCommandProperty); - set => SetValue(AddRegexSearchTermCommandProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty AddRegexSearchTermCommandProperty = DependencyProperty.Register(nameof(AddRegexSearchTermCommand), - typeof(ICommand), typeof(NLogViewer), new PropertyMetadata(null)); - - /// - /// Command to add a regex search term as an exclude filter from a provided text - /// - [Category("NLogViewerControls")] - [Browsable(true)] - [Description("Command to add a regex search term as an exclude filter from the provided text")] - public ICommand AddRegexSearchTermExcludeCommand - { - get => (ICommand) GetValue(AddRegexSearchTermExcludeCommandProperty); - set => SetValue(AddRegexSearchTermExcludeCommandProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty AddRegexSearchTermExcludeCommandProperty = DependencyProperty.Register(nameof(AddRegexSearchTermExcludeCommand), - typeof(ICommand), typeof(NLogViewer), new PropertyMetadata(null)); - - /// - /// Command to remove a specific search term - /// - [Category("NLogViewerControls")] - [Browsable(true)] - [Description("Command to remove a specific search term")] - public ICommand RemoveSearchTermCommand - { - get => (ICommand) GetValue(RemoveSearchTermCommandProperty); - set => SetValue(RemoveSearchTermCommandProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty RemoveSearchTermCommandProperty = DependencyProperty.Register(nameof(RemoveSearchTermCommand), - typeof(ICommand), typeof(NLogViewer), new PropertyMetadata(null)); - - /// - /// Command to edit a search term by copying it to the current search text - /// - [Category("NLogViewerControls")] - [Browsable(true)] - [Description("Command to edit a search term by copying it to the current search text")] - public ICommand EditSearchTermCommand - { - get => (ICommand) GetValue(EditSearchTermCommandProperty); - set => SetValue(EditSearchTermCommandProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty EditSearchTermCommandProperty = DependencyProperty.Register(nameof(EditSearchTermCommand), - typeof(ICommand), typeof(NLogViewer), new PropertyMetadata(null)); - - /// - /// Command to copy text to clipboard - /// - [Category("NLogViewerControls")] - [Browsable(true)] - [Description("Command to copy text to clipboard")] - public ICommand CopyToClipboardCommand - { - get => (ICommand) GetValue(CopyToClipboardCommandProperty); - set => SetValue(CopyToClipboardCommandProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty CopyToClipboardCommandProperty = DependencyProperty.Register(nameof(CopyToClipboardCommand), - typeof(ICommand), typeof(NLogViewer), new PropertyMetadata(null)); - - /// - /// Command to export filtered log entries to a file - /// - [Category("NLogViewerControls")] - [Browsable(true)] - [Description("Command to export filtered log entries to a file")] - public ICommand ExportCommand - { - get => (ICommand) GetValue(ExportCommandProperty); - set => SetValue(ExportCommandProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty ExportCommandProperty = DependencyProperty.Register(nameof(ExportCommand), - typeof(ICommand), typeof(NLogViewer), new PropertyMetadata(null)); - - /// - /// Command to scroll to the end of the log entries - /// - [Category("NLogViewerControls")] - [Browsable(true)] - [Description("Command to scroll to the end of the log entries")] - public ICommand ScrollToEndCommand - { - get => (ICommand) GetValue(ScrollToEndCommandProperty); - set => SetValue(ScrollToEndCommandProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty ScrollToEndCommandProperty = DependencyProperty.Register(nameof(ScrollToEndCommand), - typeof(ICommand), typeof(NLogViewer), new FrameworkPropertyMetadata - { - BindsTwoWayByDefault = true, - DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged - }); - - /// - /// Stop logging - /// - [Category("NLogViewerControls")] - [Browsable(true)] - [Description("Pause or resume logging to improve performance when not actively monitoring")] - public bool Pause - { - get => (bool)GetValue(PauseProperty); - set => SetValue(PauseProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty PauseProperty = DependencyProperty.Register(nameof(Pause), typeof(bool), typeof(NLogViewer), new PropertyMetadata(false, OnPauseChanged)); - - private static void OnPauseChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is NLogViewer viewer && !DesignerProperties.GetIsInDesignMode(viewer)) - { - if ((bool)e.NewValue) - { - // Pause: Stop listening for better performance - viewer.StopListen(); - } - else - { - // Resume: Start listening again - viewer.StartListen(viewer.CacheTarget); - } - } - } - - /// - /// The maximum number of entries before automatic cleaning is performed. There is a hysteresis of 100 entries which must be exceeded. - /// Example: is '1000'. Then after '1100' entries, everything until '1000' is deleted. - /// If set to '0' or less, it is deactivated - /// - [Category("NLogViewer")] - public int MaxCount - { - get => (int)GetValue(MaxCountProperty); - set => SetValue(MaxCountProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty MaxCountProperty = DependencyProperty.Register(nameof(MaxCount), typeof(int), typeof(NLogViewer), new PropertyMetadata(5000)); - - - #endregion - - // ########################################################################################## - // Resolver - // ########################################################################################## - - #region Resolver - - /// - /// The to format the id - /// - [Category("NLogViewerResolver")] - public ILogEventInfoResolver IdResolver - { - get => (ILogEventInfoResolver)GetValue(IdResolverProperty); - set => SetValue(IdResolverProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty IdResolverProperty = DependencyProperty.Register(nameof(IdResolver), typeof(ILogEventInfoResolver), typeof(NLogViewer), new PropertyMetadata(new IdResolver())); - - /// - /// The to format the timestamp output - /// - [Category("NLogViewerResolver")] - public ILogEventInfoResolver TimeStampResolver - { - get => (ILogEventInfoResolver)GetValue(TimeStampResolverProperty); - set => SetValue(TimeStampResolverProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty TimeStampResolverProperty = DependencyProperty.Register(nameof(TimeStampResolver), typeof(ILogEventInfoResolver), typeof(NLogViewer), new PropertyMetadata(new TimeStampResolver())); - - /// - /// The to format the loggername - /// - [Category("NLogViewerResolver")] - public ILogEventInfoResolver LoggerNameResolver - { - get => (ILogEventInfoResolver)GetValue(LoggerNameResolverProperty); - set => SetValue(LoggerNameResolverProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty LoggerNameResolverProperty = DependencyProperty.Register(nameof(LoggerNameResolver), typeof(ILogEventInfoResolver), typeof(NLogViewer), new PropertyMetadata(new LoggerNameResolver())); - - /// - /// The to format the message - /// - [Category("NLogViewerResolver")] - public ILogEventInfoResolver MessageResolver - { - get => (ILogEventInfoResolver)GetValue(MessageResolverProperty); - set => SetValue(MessageResolverProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty MessageResolverProperty = DependencyProperty.Register(nameof(MessageResolver), typeof(ILogEventInfoResolver), typeof(NLogViewer), new PropertyMetadata(new MessageResolver())); - - /// - /// The to format log entries for export - /// - [Category("NLogViewerResolver")] - public ILogExportFormatter ExportFormatter - { - get => (ILogExportFormatter)GetValue(ExportFormatterProperty); - set => SetValue(ExportFormatterProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty ExportFormatterProperty = DependencyProperty.Register(nameof(ExportFormatter), typeof(ILogExportFormatter), typeof(NLogViewer), new PropertyMetadata(new DefaultLogExportFormatter())); - - #endregion - - // ########################################################################################## - // Column Visibility - // ########################################################################################## - - #region Column Visibility - - /// - /// Controls the visibility of the ID column - /// - [Category("NLogViewerColumns")] - public bool ShowIdColumn - { - get => (bool)GetValue(ShowIdColumnProperty); - set => SetValue(ShowIdColumnProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty ShowIdColumnProperty = - DependencyProperty.Register(nameof(ShowIdColumn), typeof(bool), typeof(NLogViewer), - new PropertyMetadata(true, OnColumnVisibilityChanged)); - - /// - /// Controls the visibility of the Level column - /// - [Category("NLogViewerColumns")] - public bool ShowLevelColumn - { - get => (bool)GetValue(ShowLevelColumnProperty); - set => SetValue(ShowLevelColumnProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty ShowLevelColumnProperty = - DependencyProperty.Register(nameof(ShowLevelColumn), typeof(bool), typeof(NLogViewer), - new PropertyMetadata(true, OnColumnVisibilityChanged)); - - /// - /// Controls the visibility of the TimeStamp column - /// - [Category("NLogViewerColumns")] - public bool ShowTimeStampColumn - { - get => (bool)GetValue(ShowTimeStampColumnProperty); - set => SetValue(ShowTimeStampColumnProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty ShowTimeStampColumnProperty = - DependencyProperty.Register(nameof(ShowTimeStampColumn), typeof(bool), typeof(NLogViewer), - new PropertyMetadata(true, OnColumnVisibilityChanged)); - - /// - /// Controls the visibility of the LoggerName column - /// - [Category("NLogViewerColumns")] - public bool ShowLoggerNameColumn - { - get => (bool)GetValue(ShowLoggerNameColumnProperty); - set => SetValue(ShowLoggerNameColumnProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty ShowLoggerNameColumnProperty = - DependencyProperty.Register(nameof(ShowLoggerNameColumn), typeof(bool), typeof(NLogViewer), - new PropertyMetadata(true, OnColumnVisibilityChanged)); - - - private static void OnColumnVisibilityChanged(DependencyObject d, DependencyPropertyChangedEventArgs args) - { - if (d is NLogViewer instance) - { - instance.UpdateColumnVisibility(); - } - } - - /// - /// Updates column visibility by removing or adding columns based on the Show properties. - /// When a column is hidden, it's removed from the grid. When shown again, it's added back at its original position. - /// - private void UpdateColumnVisibility() - { - if (PART_ListView?.View is not AutoSizedGridView gridView) return; - - // Update ID column visibility (original index 0) - UpdateColumnVisibility(gridView, _originalIdColumn, ShowIdColumn, _originalIdColumnWidth, 0); - - // Update Level column visibility (original index 1) - UpdateColumnVisibility(gridView, _originalLevelColumn, ShowLevelColumn, _originalLevelColumnWidth, 1); - - // Update TimeStamp column visibility (original index 2) - UpdateColumnVisibility(gridView, _originalTimeStampColumn, ShowTimeStampColumn, _originalTimeStampColumnWidth, 2); - - // Update LoggerName column visibility (original index 3) - UpdateColumnVisibility(gridView, _originalLoggerNameColumn, ShowLoggerNameColumn, _originalLoggerNameColumnWidth, 3); - - // Message column is always visible (no visibility control) - } - - /// - /// Helper method to update individual column visibility by removing or adding the column. - /// Calculates the correct insertion position based on which columns are currently visible. - /// - /// The GridView containing the columns - /// The original column reference to add/remove - /// Whether the column should be visible - /// The original width of the column - /// The original index of the column (0=ID, 1=Level, 2=TimeStamp, 3=LoggerName) - private void UpdateColumnVisibility(AutoSizedGridView gridView, GridViewColumn originalColumn, bool showColumn, double originalWidth, int originalIndex) - { - if (originalColumn == null) return; - - // Check if column is currently in the grid - var columnExists = gridView.Columns.Contains(originalColumn); - - if (showColumn && !columnExists) - { - // Column should be visible but is not in the grid - add it back - originalColumn.Width = originalWidth; - - // Calculate the correct insertion index based on which columns are currently visible - var insertionIndex = CalculateInsertionIndex(gridView, originalIndex); - gridView.Columns.Insert(insertionIndex, originalColumn); - } - else if (!showColumn && columnExists) - { - // Column should be hidden but is in the grid - remove it - gridView.Columns.Remove(originalColumn); - } - } - - /// - /// Calculates the correct insertion index for a column based on its original position - /// and which columns are currently visible. - /// - /// The GridView containing the columns - /// The original index of the column to insert - /// The correct insertion index - private int CalculateInsertionIndex(AutoSizedGridView gridView, int originalIndex) - { - // Count how many visible columns come before this one in the original order - var insertionIndex = 0; - - // Check ID column (original index 0) - if (originalIndex > 0 && ShowIdColumn && gridView.Columns.Contains(_originalIdColumn)) - { - insertionIndex++; - } - - // Check Level column (original index 1) - if (originalIndex > 1 && ShowLevelColumn && gridView.Columns.Contains(_originalLevelColumn)) - { - insertionIndex++; - } - - // Check TimeStamp column (original index 2) - if (originalIndex > 2 && ShowTimeStampColumn && gridView.Columns.Contains(_originalTimeStampColumn)) - { - insertionIndex++; - } - - // Check LoggerName column (original index 3) - if (originalIndex > 3 && ShowLoggerNameColumn && gridView.Columns.Contains(_originalLoggerNameColumn)) - { - insertionIndex++; - } - - return insertionIndex; - } - - #endregion - - // ########################################################################################## - // Filter Properties - // ########################################################################################## - - #region Filter Properties - - /// - /// Filter for Trace log level - /// - [Category("NLogViewerFilters")] - [Browsable(true)] - [Description("Hide/show Trace level log entries")] - public bool TraceFilter - { - get => (bool)GetValue(TraceFilterProperty); - set => SetValue(TraceFilterProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty TraceFilterProperty = - DependencyProperty.Register(nameof(TraceFilter), typeof(bool), typeof(NLogViewer), - new PropertyMetadata(false, OnFilterChanged)); - - /// - /// Filter for Debug log level - /// - [Category("NLogViewerFilters")] - [Browsable(true)] - [Description("Hide/show Debug level log entries")] - public bool DebugFilter - { - get => (bool)GetValue(DebugFilterProperty); - set => SetValue(DebugFilterProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty DebugFilterProperty = - DependencyProperty.Register(nameof(DebugFilter), typeof(bool), typeof(NLogViewer), - new PropertyMetadata(false, OnFilterChanged)); - - /// - /// Filter for Info log level - /// - [Category("NLogViewerFilters")] - [Browsable(true)] - [Description("Hide/show Info level log entries")] - public bool InfoFilter - { - get => (bool)GetValue(InfoFilterProperty); - set => SetValue(InfoFilterProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty InfoFilterProperty = - DependencyProperty.Register(nameof(InfoFilter), typeof(bool), typeof(NLogViewer), - new PropertyMetadata(false, OnFilterChanged)); - - /// - /// Filter for Warn log level - /// - [Category("NLogViewerFilters")] - [Browsable(true)] - [Description("Hide/show Warn level log entries")] - public bool WarnFilter - { - get => (bool)GetValue(WarnFilterProperty); - set => SetValue(WarnFilterProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty WarnFilterProperty = - DependencyProperty.Register(nameof(WarnFilter), typeof(bool), typeof(NLogViewer), - new PropertyMetadata(false, OnFilterChanged)); - - /// - /// Filter for Error log level - /// - [Category("NLogViewerFilters")] - [Browsable(true)] - [Description("Hide/show Error level log entries")] - public bool ErrorFilter - { - get => (bool)GetValue(ErrorFilterProperty); - set => SetValue(ErrorFilterProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty ErrorFilterProperty = - DependencyProperty.Register(nameof(ErrorFilter), typeof(bool), typeof(NLogViewer), - new PropertyMetadata(false, OnFilterChanged)); - - /// - /// Filter for Fatal log level - /// - [Category("NLogViewerFilters")] - [Browsable(true)] - [Description("Hide/show Fatal level log entries")] - public bool FatalFilter - { - get => (bool)GetValue(FatalFilterProperty); - set => SetValue(FatalFilterProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty FatalFilterProperty = - DependencyProperty.Register(nameof(FatalFilter), typeof(bool), typeof(NLogViewer), - new PropertyMetadata(false, OnFilterChanged)); - - /// - /// Current search text being typed (not yet activated) - /// - [Category("NLogViewerFilters")] - [Browsable(true)] - [Description("Current search text being typed (activated with Enter key)")] - public string CurrentSearchText - { - get => (string)GetValue(CurrentSearchTextProperty); - set => SetValue(CurrentSearchTextProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty CurrentSearchTextProperty = - DependencyProperty.Register(nameof(CurrentSearchText), typeof(string), typeof(NLogViewer), - new PropertyMetadata(string.Empty)); - - /// - /// Whether to use regex pattern matching for search - /// - [Category("NLogViewerFilters")] - [Browsable(true)] - [Description("Use regex pattern matching instead of free text search")] - public bool UseRegexSearch - { - get => (bool)GetValue(UseRegexSearchProperty); - set => SetValue(UseRegexSearchProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty UseRegexSearchProperty = - DependencyProperty.Register(nameof(UseRegexSearch), typeof(bool), typeof(NLogViewer), - new PropertyMetadata(false)); - - /// - /// Collection of active search terms - /// - [Category("NLogViewerFilters")] - [Browsable(false)] - [Description("Collection of active search terms")] - public ObservableCollection ActiveSearchTerms - { - get => (ObservableCollection)GetValue(ActiveSearchTermsProperty); - private set => SetValue(ActiveSearchTermsProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty ActiveSearchTermsProperty = - DependencyProperty.Register(nameof(ActiveSearchTerms), typeof(ObservableCollection), typeof(NLogViewer), - new PropertyMetadata(null)); - - private static void OnFilterChanged(DependencyObject d, DependencyPropertyChangedEventArgs args) - { - if (d is NLogViewer instance) - { - instance.UpdateFilter(); - } - } - - - /// - /// Adds a new search term to the active search terms collection - /// - public void AddSearchTerm() - { - if (string.IsNullOrWhiteSpace(CurrentSearchText)) - return; - - SearchTerm searchTerm; - - if (UseRegexSearch) - { - try - { - searchTerm = new RegexSearchTerm(CurrentSearchText); - } - catch (ArgumentException ex) - { - MessageBox.Show($"Invalid regex pattern: {ex.Message}", "Regex Error", - MessageBoxButton.OK, MessageBoxImage.Warning); - return; - } - } - else - { - searchTerm = new TextSearchTerm(CurrentSearchText); - } - - ActiveSearchTerms.Add(searchTerm); - - // Clear the input - CurrentSearchText = string.Empty; - - // Update filter - UpdateFilter(); - } - - /// - /// Removes a search term from the active search terms collection - /// - public void RemoveSearchTerm(SearchTerm searchTerm) - { - ActiveSearchTerms.Remove(searchTerm); - UpdateFilter(); - } - - /// - /// Clears all active search terms - /// - public void ClearAllSearchTerms() - { - ActiveSearchTerms.Clear(); - UpdateFilter(); - } - - /// - /// Adds a RegexSearchTerm from a provided text and updates the filter - /// Special regex characters are escaped to treat the text as a literal string - /// - /// The text to use as regex pattern (will be escaped) - public void AddRegexSearchTerm(string text) - { - if (string.IsNullOrWhiteSpace(text)) - return; - - try - { - // Escape special regex characters to treat the text as a literal string - var escapedPattern = Regex.Escape(text); - var term = new RegexSearchTerm(escapedPattern); - ActiveSearchTerms.Add(term); - UpdateFilter(); - } - catch (ArgumentException ex) - { - MessageBox.Show($"Invalid regex pattern: {ex.Message}", "Regex Error", - MessageBoxButton.OK, MessageBoxImage.Warning); - } - } - - /// - /// Adds a RegexSearchTerm as an exclude filter from a provided text and updates the filter - /// Special regex characters are escaped and wrapped in a negative lookahead pattern - /// - /// The text to use as regex pattern (will be escaped and wrapped in negative lookahead) - public void AddRegexSearchTermExclude(string text) - { - if (string.IsNullOrWhiteSpace(text)) - return; - - try - { - // Escape special regex characters to treat the text as a literal string - var escapedPattern = Regex.Escape(text); - // Wrap in negative lookahead: matches any text that does NOT contain the pattern - var negativeLookaheadPattern = $"^(?!.*{escapedPattern}$)"; - var term = new RegexSearchTerm(negativeLookaheadPattern); - ActiveSearchTerms.Add(term); - UpdateFilter(); - } - catch (ArgumentException ex) - { - MessageBox.Show($"Invalid regex pattern: {ex.Message}", "Regex Error", - MessageBoxButton.OK, MessageBoxImage.Warning); - } - } - - /// - /// Edits a search term by copying its text to the current search text field - /// - /// The search term to edit - public void EditSearchTerm(SearchTerm searchTerm) - { - if (searchTerm == null) - return; - - // Copy the search term text to the current search text field - CurrentSearchText = searchTerm.Text; - - // Set the regex checkbox based on the search term type - UseRegexSearch = searchTerm is RegexSearchTerm; - - // Remove the original search term - ActiveSearchTerms.Remove(searchTerm); - UpdateFilter(); - } - - /// - /// Copies text to the clipboard - /// - /// The text to copy to clipboard - public void CopyToClipboard(string text) - { - if (string.IsNullOrEmpty(text)) - return; - - try - { - Clipboard.SetText(text); - } - catch (Exception ex) - { - MessageBox.Show($"Failed to copy to clipboard: {ex.Message}", "Clipboard Error", - MessageBoxButton.OK, MessageBoxImage.Warning); - } - } - - /// - /// Gets all filtered log entries from the current view - /// - /// An enumerable collection of filtered LogEventInfo entries - public IEnumerable GetFilteredLogEntries() - { - if (LogEvents?.View == null) - return Enumerable.Empty(); - - return LogEvents.View.Cast(); - } - - /// - /// Exports filtered log entries to a file in the specified format - /// - /// Export parameters containing file path and format - public void ExportLogs(ExportParameter parameter) - { - if (parameter == null) - throw new ArgumentNullException(nameof(parameter)); - - if (string.IsNullOrWhiteSpace(parameter.FilePath)) - throw new ArgumentException("File path cannot be empty", nameof(parameter)); - - try - { - var filteredEntries = GetFilteredLogEntries().ToList(); - - if (filteredEntries.Count == 0) - { - MessageBox.Show("No log entries to export.", "Export", - MessageBoxButton.OK, MessageBoxImage.Information); - return; - } - - switch (parameter.Format) - { - case ExportFormat.Log: - ExportToLogFormat(parameter.FilePath, filteredEntries, parameter.CustomFormatter); - break; - default: - throw new NotSupportedException($"Export format {parameter.Format} is not supported."); - } - - MessageBox.Show($"Successfully exported {filteredEntries.Count} log entry(ies) to {parameter.FilePath}", - "Export Complete", MessageBoxButton.OK, MessageBoxImage.Information); - } - catch (Exception ex) - { - MessageBox.Show($"Failed to export logs: {ex.Message}", "Export Error", - MessageBoxButton.OK, MessageBoxImage.Error); - } - } - - /// - /// Exports log entries to a standard log file format - /// - /// Target file path - /// Log entries to export - /// Optional custom formatter to override the default formatter - private void ExportToLogFormat(string filePath, IEnumerable logEntries, ILogExportFormatter? customFormatter = null) - { - using var writer = new StreamWriter(filePath, false, System.Text.Encoding.UTF8); - - // Use custom formatter if provided, otherwise use ExportFormatter, fall back to default formatter - var formatter = customFormatter ?? ExportFormatter ?? new DefaultLogExportFormatter(); - - foreach (var logEvent in logEntries) - { - var formattedLine = formatter.Format( - logEvent, - TimeStampResolver ?? new TimeStampResolver(), - LoggerNameResolver ?? new LoggerNameResolver(), - MessageResolver ?? new MessageResolver()); - - writer.WriteLine(formattedLine); - } - } - - #endregion - - // ########################################################################################## - // Filter Visibility - // ########################################################################################## - - #region Filter Visibility - - /// - /// Controls the visibility of the filter buttons - /// - [Category("NLogViewerFilters")] - [Browsable(true)] - [Description("Show/hide the filter buttons")] - public bool ShowFilterButtons - { - get => (bool)GetValue(ShowFilterButtonsProperty); - set => SetValue(ShowFilterButtonsProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty ShowFilterButtonsProperty = - DependencyProperty.Register(nameof(ShowFilterButtons), typeof(bool), typeof(NLogViewer), - new PropertyMetadata(true)); - - /// - /// Controls the visibility of the search controls - /// - [Category("NLogViewerFilters")] - [Browsable(true)] - [Description("Show/hide the search controls")] - public bool ShowSearchControls - { - get => (bool)GetValue(ShowSearchControlsProperty); - set => SetValue(ShowSearchControlsProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty ShowSearchControlsProperty = - DependencyProperty.Register(nameof(ShowSearchControls), typeof(bool), typeof(NLogViewer), - new PropertyMetadata(true)); - - #endregion - - // ########################################################################################## - // Controls Visibility - // ########################################################################################## - - #region Controls Visibility - - /// - /// Controls the visibility of the control buttons (Auto-Scroll, Clear, Pause) - /// - [Category("NLogViewerControls")] - [Browsable(true)] - [Description("Show/hide the control buttons (Auto-Scroll, Clear, Pause)")] - public bool ShowControlButtons - { - get => (bool)GetValue(ShowControlButtonsProperty); - set => SetValue(ShowControlButtonsProperty, value); - } - - /// - /// The DependencyProperty. - /// - public static readonly DependencyProperty ShowControlButtonsProperty = - DependencyProperty.Register(nameof(ShowControlButtons), typeof(bool), typeof(NLogViewer), - new PropertyMetadata(true)); - - #endregion - - #endregion - - // ############################################################################################################################## - // Properties - // ############################################################################################################################## - - #region Properties - - // ########################################################################################## - // Public Properties - // ########################################################################################## - - - - // ########################################################################################## - // Private Properties - // ########################################################################################## - - private ObservableCollection _LogEventInfos { get; } = new ObservableCollection(); - private IDisposable _Subscription; - private Window? _ParentWindow; - private bool _isListening = false; - - // Store original column references and widths for restoration - private GridViewColumn _originalIdColumn; - private GridViewColumn _originalLevelColumn; - private GridViewColumn _originalTimeStampColumn; - private GridViewColumn _originalLoggerNameColumn; - private double _originalIdColumnWidth = 40; - private double _originalLevelColumnWidth = double.NaN; // Auto - private double _originalTimeStampColumnWidth = double.NaN; // Auto - private double _originalLoggerNameColumnWidth = double.NaN; // Auto - - /// - /// Reference to the ListView template part for programmatic access - /// - private ListView PART_ListView => GetTemplateChild("PART_ListView") as ListView; - - #endregion - - // ############################################################################################################################## - // Public Methods - // ############################################################################################################################## - - #region Public Methods - - /// - /// Starts listening for log events by subscribing to the cache target. - /// Subscribes to when set, otherwise uses and subscribes to that. - /// Call when the control needs to resume listening (e.g. when undocking or when the window loads again). - /// - public void StartListen(ICacheTarget? target = null) - { - if (_isListening || DesignerProperties.GetIsInDesignMode(this)) - return; - - // Resolve Dispatcher: prefer parent window (for cleanup on Closed/Unloaded), else Application/Current (e.g. unit tests without Window). - var window = Window.GetWindow(this); - if (_ParentWindow == null && window != null) - { - _ParentWindow = window; - _ParentWindow.Closed += _ParentWindowOnClosed; - } - - var dispatcher = window?.Dispatcher ?? Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher; - if (dispatcher == null) - return; - - target ??= Targets.CacheTarget.GetInstance(targetName: TargetName); - - _Subscription = target.Cache.SubscribeOn(Scheduler.Default) - .Buffer(TimeSpan.FromMilliseconds(100)) - .Where(x => x.Any()) - .ObserveOn(new DispatcherSynchronizationContext(dispatcher)) - .Subscribe(infos => - { - using (LogEvents.DeferRefresh()) - { - foreach (var info in infos) - { - _LogEventInfos.Add(info); - } - if (MaxCount >= 0 & _LogEventInfos.Count - 100 > MaxCount) - { - for (var i = 0; i < _LogEventInfos.Count - MaxCount; i++) - { - _LogEventInfos.RemoveAt(0); - } - } - } - - if (AutoScroll) - { - PART_ListView?.ScrollToEnd(); - } - }); - - _isListening = true; - } - - /// - /// Stops listening for log events by disposing the subscription. - /// This method should be called when the control needs to stop listening for logs, - /// such as when docking in a docking system or when the window is unloaded. - /// - public void StopListen() - { - if (!_isListening) - return; - - _Subscription?.Dispose(); - _Subscription = null; - _isListening = false; - } - - #endregion - - // ############################################################################################################################## - // Constructor - // ############################################################################################################################## - - #region Constructor - - static NLogViewer() - { - DefaultStyleKeyProperty.OverrideMetadata(typeof(NLogViewer), new FrameworkPropertyMetadata(typeof(NLogViewer))); - } - - public NLogViewer() - { - DataContext = this; - - // save instance UID - Uid = GetHashCode().ToString(); - - if (DesignerProperties.GetIsInDesignMode(this)) - return; - - // Initialize with internal collection (filled from CacheTarget subscription; target is CacheTargetProperty when set, else CacheTarget.GetInstance). - LogEvents = new CollectionViewSource {Source = _LogEventInfos}; - ActiveSearchTerms = new ObservableCollection(); - UpdateFilter(); // Initialize filter - - Loaded += _OnLoaded; - Unloaded += _OnUnloaded; - - // ClearCommand: clears the internal collection (data comes from CacheTarget or GetInstance). - ClearCommand = new RelayCommand(() => _LogEventInfos.Clear()); - AddSearchTermCommand = new RelayCommand(AddSearchTerm); - ClearAllSearchTermsCommand = new RelayCommand(ClearAllSearchTerms); - RemoveSearchTermCommand = new RelayCommand(RemoveSearchTerm); - AddRegexSearchTermCommand = new RelayCommand(AddRegexSearchTerm); - AddRegexSearchTermExcludeCommand = new RelayCommand(AddRegexSearchTermExclude); - EditSearchTermCommand = new RelayCommand(EditSearchTerm); - CopyToClipboardCommand = new RelayCommand(CopyToClipboard); - ExportCommand = new RelayCommand(ExportLogs); - ScrollToEndCommand = new RelayCommand(() => - { - PART_ListView?.ScrollToEnd(); - }); - } - - private void _OnUnloaded(object sender, RoutedEventArgs e) - { - // look in logical and visual tree if the control has been removed - // If there is no parent window found before, we have a special case (https://github.com/dojo90/NLogViewer/issues/30) and just dispose it anyway - if (_ParentWindow.FindChildByUid(Uid) == null) - { - _Dispose(); - } - } - - private void _ParentWindowOnClosed(object sender, EventArgs e) - { - _Dispose(); - } - - private void _Dispose() + /// + protected override void DisposeViewerResources() { StopListen(); } - - private void _OnLoaded(object sender, RoutedEventArgs e) - { - // removed loaded handler to prevent duplicate subscribing - Loaded -= _OnLoaded; - - PART_ListView?.ScrollToEnd(); - - // Store original column references and widths for visibility management - if (PART_ListView?.View is AutoSizedGridView gridView) - { - if (gridView.Columns.Count > 0) - { - _originalIdColumn = gridView.Columns[0]; - _originalIdColumnWidth = gridView.Columns[0].Width; - } - if (gridView.Columns.Count > 1) - { - _originalLevelColumn = gridView.Columns[1]; - _originalLevelColumnWidth = gridView.Columns[1].Width; - } - if (gridView.Columns.Count > 2) - { - _originalTimeStampColumn = gridView.Columns[2]; - _originalTimeStampColumnWidth = gridView.Columns[2].Width; - } - if (gridView.Columns.Count > 3) - { - _originalLoggerNameColumn = gridView.Columns[3]; - _originalLoggerNameColumnWidth = gridView.Columns[3].Width; - } - // Message column (index 4) is always visible, no need to store its reference - - // Apply initial column visibility - UpdateColumnVisibility(); - } - - // Start listening: subscribe to CacheTarget if set, else to CacheTarget.GetInstance(). - StartListen(); - } - - #endregion } } diff --git a/ui/Sentinel.NLogViewer.Wpf/NLogViewerBase.cs b/ui/Sentinel.NLogViewer.Wpf/NLogViewerBase.cs new file mode 100644 index 0000000..91b9cae --- /dev/null +++ b/ui/Sentinel.NLogViewer.Wpf/NLogViewerBase.cs @@ -0,0 +1,1878 @@ +using System; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Linq; +using System.Text.RegularExpressions; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Threading; +using Sentinel.NLogViewer.App.Models; +using Sentinel.NLogViewer.Wpf.Extensions; +using Sentinel.NLogViewer.Wpf.Helper; +using Sentinel.NLogViewer.Wpf.Resolver; +using NLog; +using System.IO; +using System.Collections.Generic; +using System.Collections.Specialized; + +namespace Sentinel.NLogViewer.Wpf +{ + /// + /// Base class for search terms that can match against text + /// + public abstract class SearchTerm + { + public string Text { get; protected set; } + + protected SearchTerm(string text) + { + Text = text ?? throw new ArgumentNullException(nameof(text)); + } + + /// + /// Determines if the search term matches the given text + /// + /// The text to match against + /// True if the text matches the search term + public abstract bool Match(string text); + + /// + /// Determines if the search term matches any of the given texts + /// + /// The texts to match against + /// True if any text matches the search term + public bool MatchAny(params string[] texts) + { + if (texts == null) return false; + return texts.Any(text => !string.IsNullOrEmpty(text) && Match(text)); + } + + public override string ToString() + { + return Text; + } + } + + /// + /// A search term that performs case-insensitive substring matching + /// + public class TextSearchTerm : SearchTerm + { + private readonly string _lowercaseText; + + public TextSearchTerm(string text) : base(text) + { + _lowercaseText = text.ToLowerInvariant(); + } + + public override bool Match(string text) + { + if (string.IsNullOrEmpty(text)) return false; + if (string.IsNullOrEmpty(_lowercaseText)) return false; + return text.ToLowerInvariant().Contains(_lowercaseText); + } + + public override string ToString() + { + return Text; + } + } + + /// + /// A search term that performs regex pattern matching + /// + public class RegexSearchTerm : SearchTerm + { + private readonly Regex _regex; + + public RegexSearchTerm(string pattern) : base(pattern) + { + try + { + _regex = new Regex(pattern, RegexOptions.Compiled); + } + catch (ArgumentException ex) + { + throw new ArgumentException($"Invalid regex pattern: {ex.Message}", nameof(pattern), ex); + } + } + + public override bool Match(string text) + { + if (string.IsNullOrEmpty(text)) return false; + return _regex.IsMatch(text); + } + + public override string ToString() + { + return $"/{Text}/"; + } + } + + /// + /// Base log viewer control: filtering, styling, and optional external . + /// Use to subscribe to NLog . + /// + public class NLogViewerBase : Control + { + // ############################################################################################################################## + // Dependency Properties + // ############################################################################################################################## + + #region Dependency Properties + + // ########################################################################################## + // Colors + // ########################################################################################## + + #region Colors + + /// + /// The background for the trace output + /// + [Category("NLogViewerColors")] + [Description("Background brush for Trace level log entries")] + public Brush TraceBackground + { + get => (Brush) GetValue(TraceBackgroundProperty); + set => SetValue(TraceBackgroundProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty TraceBackgroundProperty = + DependencyProperty.Register(nameof(TraceBackground), typeof(Brush), typeof(NLogViewerBase), + new PropertyMetadata((Brush) (new BrushConverter().ConvertFrom("#D3D3D3")))); + + /// + /// The foreground for the trace output + /// + [Category("NLogViewerColors")] + [Description("Foreground brush for Trace level log entries")] + public Brush TraceForeground + { + get => (Brush) GetValue(TraceForegroundProperty); + set => SetValue(TraceForegroundProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty TraceForegroundProperty = + DependencyProperty.Register(nameof(TraceForeground), typeof(Brush), typeof(NLogViewerBase), + new PropertyMetadata((Brush) (new BrushConverter().ConvertFrom("#042271")))); + + /// + /// The background for the debug output + /// + [Category("NLogViewerColors")] + [Description("Background brush for Debug level log entries")] + public Brush DebugBackground + { + get => (Brush) GetValue(DebugBackgroundProperty); + set => SetValue(DebugBackgroundProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty DebugBackgroundProperty = + DependencyProperty.Register(nameof(DebugBackground), typeof(Brush), typeof(NLogViewerBase), + new PropertyMetadata((Brush) (new BrushConverter().ConvertFrom("#90EE90")))); + + /// + /// The foreground for the debug output + /// + [Category("NLogViewerColors")] + [Description("Foreground brush for Debug level log entries")] + public Brush DebugForeground + { + get => (Brush) GetValue(DebugForegroundProperty); + set => SetValue(DebugForegroundProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty DebugForegroundProperty = + DependencyProperty.Register(nameof(DebugForeground), typeof(Brush), typeof(NLogViewerBase), + new PropertyMetadata((Brush) (new BrushConverter().ConvertFrom("#042271")))); + + /// + /// The background for the info output + /// + [Category("NLogViewerColors")] + [Description("Background brush for Info level log entries")] + public Brush InfoBackground + { + get => (Brush) GetValue(InfoBackgroundProperty); + set => SetValue(InfoBackgroundProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty InfoBackgroundProperty = DependencyProperty.Register(nameof(InfoBackground), + typeof(Brush), typeof(NLogViewerBase), + new PropertyMetadata((Brush) (new BrushConverter().ConvertFrom("#0000FF")))); + + /// + /// The foreground for the info output + /// + [Category("NLogViewerColors")] + [Description("Foreground brush for Info level log entries")] + public Brush InfoForeground + { + get => (Brush) GetValue(InfoForegroundProperty); + set => SetValue(InfoForegroundProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty InfoForegroundProperty = DependencyProperty.Register(nameof(InfoForeground), + typeof(Brush), typeof(NLogViewerBase), new PropertyMetadata(Brushes.White)); + + /// + /// The background for the warn output + /// + [Category("NLogViewerColors")] + [Description("Background brush for Warn level log entries")] + public Brush WarnBackground + { + get => (Brush) GetValue(WarnBackgroundProperty); + set => SetValue(WarnBackgroundProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty WarnBackgroundProperty = DependencyProperty.Register(nameof(WarnBackground), + typeof(Brush), typeof(NLogViewerBase), + new PropertyMetadata((Brush) (new BrushConverter().ConvertFrom("#FFFF00")))); + + /// + /// The foreground for the warn output + /// + [Category("NLogViewerColors")] + [Description("Foreground brush for Warn level log entries")] + public Brush WarnForeground + { + get => (Brush) GetValue(WarnForegroundProperty); + set => SetValue(WarnForegroundProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty WarnForegroundProperty = DependencyProperty.Register(nameof(WarnForeground), + typeof(Brush), typeof(NLogViewerBase), + new PropertyMetadata((Brush) (new BrushConverter().ConvertFrom("#324B5C")))); + + /// + /// The background for the error output + /// + [Category("NLogViewerColors")] + [Description("Background brush for Error level log entries")] + public Brush ErrorBackground + { + get => (Brush) GetValue(ErrorBackgroundProperty); + set => SetValue(ErrorBackgroundProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty ErrorBackgroundProperty = + DependencyProperty.Register(nameof(ErrorBackground), typeof(Brush), typeof(NLogViewerBase), + new PropertyMetadata(Brushes.Red)); + + /// + /// The foreground for the error output + /// + [Category("NLogViewerColors")] + [Description("Foreground brush for Error level log entries")] + public Brush ErrorForeground + { + get => (Brush) GetValue(ErrorForegroundProperty); + set => SetValue(ErrorForegroundProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty ErrorForegroundProperty = + DependencyProperty.Register(nameof(ErrorForeground), typeof(Brush), typeof(NLogViewerBase), + new PropertyMetadata(Brushes.White)); + + /// + /// The background for the fatal output + /// + [Category("NLogViewerColors")] + [Description("Background brush for Fatal level log entries")] + public Brush FatalBackground + { + get => (Brush) GetValue(FatalBackgroundProperty); + set => SetValue(FatalBackgroundProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty FatalBackgroundProperty = + DependencyProperty.Register(nameof(FatalBackground), typeof(Brush), typeof(NLogViewerBase), + new PropertyMetadata(Brushes.Black)); + + /// + /// The foreground for the fatal output + /// + [Category("NLogViewerColors")] + [Description("Foreground brush for Fatal level log entries")] + public Brush FatalForeground + { + get => (Brush) GetValue(FatalForegroundProperty); + set => SetValue(FatalForegroundProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty FatalForegroundProperty = + DependencyProperty.Register(nameof(FatalForeground), typeof(Brush), typeof(NLogViewerBase), + new PropertyMetadata(Brushes.Yellow)); + + // ------------------------------------------------------------------------------------------ + // Search Term Chip Colors + // ------------------------------------------------------------------------------------------ + + /// + /// Background brush for plain text search term chips + /// + [Category("NLogViewerColors")] + [Description("Background brush for plain text search term chips")] + public Brush TextSearchTermBackground + { + get => (Brush)GetValue(TextSearchTermBackgroundProperty); + set => SetValue(TextSearchTermBackgroundProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty TextSearchTermBackgroundProperty = + DependencyProperty.Register(nameof(TextSearchTermBackground), typeof(Brush), typeof(NLogViewerBase), + new PropertyMetadata((Brush)(new BrushConverter().ConvertFrom("#FFFFFBE6")))); + + /// + /// Border brush for plain text search term chips + /// + [Category("NLogViewerColors")] + [Description("Border brush for plain text search term chips")] + public Brush TextSearchTermBorderBrush + { + get => (Brush)GetValue(TextSearchTermBorderBrushProperty); + set => SetValue(TextSearchTermBorderBrushProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty TextSearchTermBorderBrushProperty = + DependencyProperty.Register(nameof(TextSearchTermBorderBrush), typeof(Brush), typeof(NLogViewerBase), + new PropertyMetadata((Brush)(new BrushConverter().ConvertFrom("#FFFFC107")))); + + /// + /// Foreground brush for plain text search term chips + /// + [Category("NLogViewerColors")] + [Description("Foreground brush for plain text search term chips")] + public Brush TextSearchTermForeground + { + get => (Brush)GetValue(TextSearchTermForegroundProperty); + set => SetValue(TextSearchTermForegroundProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty TextSearchTermForegroundProperty = + DependencyProperty.Register(nameof(TextSearchTermForeground), typeof(Brush), typeof(NLogViewerBase), + new PropertyMetadata(Brushes.Black)); + + /// + /// Background brush for regex search term chips + /// + [Category("NLogViewerColors")] + [Description("Background brush for regex search term chips")] + public Brush RegexSearchTermBackground + { + get => (Brush)GetValue(RegexSearchTermBackgroundProperty); + set => SetValue(RegexSearchTermBackgroundProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty RegexSearchTermBackgroundProperty = + DependencyProperty.Register(nameof(RegexSearchTermBackground), typeof(Brush), typeof(NLogViewerBase), + new PropertyMetadata((Brush)(new BrushConverter().ConvertFrom("#FFFFFBE6")))); + + /// + /// Border brush for regex search term chips + /// + [Category("NLogViewerColors")] + [Description("Border brush for regex search term chips")] + public Brush RegexSearchTermBorderBrush + { + get => (Brush)GetValue(RegexSearchTermBorderBrushProperty); + set => SetValue(RegexSearchTermBorderBrushProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty RegexSearchTermBorderBrushProperty = + DependencyProperty.Register(nameof(RegexSearchTermBorderBrush), typeof(Brush), typeof(NLogViewerBase), + new PropertyMetadata((Brush)(new BrushConverter().ConvertFrom("#FFFFC107")))); + + /// + /// Foreground brush for regex search term chips + /// + [Category("NLogViewerColors")] + [Description("Foreground brush for regex search term chips")] + public Brush RegexSearchTermForeground + { + get => (Brush)GetValue(RegexSearchTermForegroundProperty); + set => SetValue(RegexSearchTermForegroundProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty RegexSearchTermForegroundProperty = + DependencyProperty.Register(nameof(RegexSearchTermForeground), typeof(Brush), typeof(NLogViewerBase), + new PropertyMetadata(Brushes.Black)); + + /// + /// Foreground brush for the "Regex:" prefix label + /// + [Category("NLogViewerColors")] + [Description("Foreground brush for the 'Regex:' prefix label")] + public Brush RegexPrefixForeground + { + get => (Brush)GetValue(RegexPrefixForegroundProperty); + set => SetValue(RegexPrefixForegroundProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty RegexPrefixForegroundProperty = + DependencyProperty.Register(nameof(RegexPrefixForeground), typeof(Brush), typeof(NLogViewerBase), + new PropertyMetadata((Brush)(new BrushConverter().ConvertFrom("#FFFF8F00")))); + + /// + /// Background brush used to highlight matched search text in the message + /// + [Category("NLogViewerColors")] + [Description("Background brush used to highlight matched search text in the message")] + public Brush SearchHighlightBackground + { + get => (Brush)GetValue(SearchHighlightBackgroundProperty); + set => SetValue(SearchHighlightBackgroundProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty SearchHighlightBackgroundProperty = + DependencyProperty.Register(nameof(SearchHighlightBackground), typeof(Brush), typeof(NLogViewerBase), + new PropertyMetadata((Brush)(new BrushConverter().ConvertFrom("#FFFF80C0")))); + + #endregion + + // ########################################################################################## + // NLogViewer + // ########################################################################################## + + #region NLogViewer + + /// + /// When set, the viewer binds its list to this collection so callers can push directly. + /// When null, an internal collection is used. + /// + [Category("NLogViewer")] + [Browsable(true)] + [Description("Observable collection of log entries displayed by the viewer.")] + public ObservableCollection? ItemsSource + { + get => (ObservableCollection?)GetValue(ItemsSourceProperty); + set => SetValue(ItemsSourceProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register( + nameof(ItemsSource), + typeof(ObservableCollection), + typeof(NLogViewerBase), + new PropertyMetadata(null, OnItemsSourceChanged)); + + private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is not NLogViewerBase viewer) + return; + viewer.SyncItemsSourceToView(); + } + + /// + /// Private DP to bind to the gui + /// + [Category("NLogViewer")] + public CollectionViewSource LogEvents + { + get => (CollectionViewSource) GetValue(LogEventsProperty); + private set => SetValue(LogEventsProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty LogEventsProperty = DependencyProperty.Register(nameof(LogEvents), + typeof(CollectionViewSource), typeof(NLogViewerBase), new PropertyMetadata(null)); + + /// + /// Automatically scroll to the newest entry + /// + [Category("NLogViewerControls")] + [Browsable(true)] + [Description("Automatically scroll to the newest log entry when new entries are added")] + public bool AutoScroll + { + get => (bool)GetValue(AutoScrollProperty); + set => SetValue(AutoScrollProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty AutoScrollProperty = DependencyProperty.Register(nameof(AutoScroll), typeof(bool), typeof(NLogViewerBase), new PropertyMetadata(true, AutoScrollChangedCallback)); + + private static void AutoScrollChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs args) + { + if (d is NLogViewerBase instance) + { + instance.OnAutoScrollChanged(); + } + } + + protected virtual void OnAutoScrollChanged() + { + if (AutoScroll) + PART_ListView?.ScrollToEnd(); + } + + private void UpdateFilter() + { + if (LogEvents?.View is not CollectionView collectionView) return; + + collectionView.Filter = item => + { + if (item is not LogEventInfo logEvent) return true; + + // Apply level filters first + var levelFilterPassed = logEvent.Level.Name switch + { + "Trace" => !TraceFilter, // If TraceFilter is true, hide Trace entries (!true = false) + "Debug" => !DebugFilter, // If DebugFilter is true, hide Debug entries (!true = false) + "Info" => !InfoFilter, // If InfoFilter is true, hide Info entries (!true = false) + "Warn" => !WarnFilter, // If WarnFilter is true, hide Warn entries (!true = false) + "Error" => !ErrorFilter, // If ErrorFilter is true, hide Error entries (!true = false) + "Fatal" => !FatalFilter, // If FatalFilter is true, hide Fatal entries (!true = false) + _ => true + }; + + if (!levelFilterPassed) return false; + + // Apply search filters if any active search terms exist + if (ActiveSearchTerms?.Count > 0) + { + var loggerName = logEvent.LoggerName ?? string.Empty; + var message = MessageResolver?.Resolve(logEvent) ?? string.Empty; + + // Separate include and exclude terms based on regex pattern + // Exclude patterns start with "^(?!.*" (negative lookahead) + var includeTerms = ActiveSearchTerms.Where(t => + !(t is RegexSearchTerm regexTerm && regexTerm.Text.StartsWith("^(?!.*", StringComparison.Ordinal))).ToList(); + var excludeTerms = ActiveSearchTerms.Where(t => + t is RegexSearchTerm regexTerm && regexTerm.Text.StartsWith("^(?!.*", StringComparison.Ordinal)).ToList(); + + // AND logic: ALL include terms must match + foreach (var searchTerm in includeTerms) + { + if (!searchTerm.MatchAny(loggerName, message)) + return false; + } + + // Exclude logic: Hide entry if ANY string (loggerName OR message) contains the excluded pattern + // Note: For exclude patterns (negative lookahead ^(?!.*pattern)), if the pattern matches, + // it means the text does NOT contain the excluded pattern. If it doesn't match, the text contains it. + // We need to check each string individually because MatchAny would return true if ANY string matches, + // but we want to hide if ANY string contains the pattern. + foreach (var searchTerm in excludeTerms) + { + // Check each string individually: if either contains the pattern, hide the entry + var loggerContainsPattern = !searchTerm.Match(loggerName); + var messageContainsPattern = !searchTerm.Match(message); + + if (loggerContainsPattern || messageContainsPattern) + return false; // Hide entry if either string contains the excluded pattern + } + + // All include terms matched and no exclude terms matched + return true; + } + + return true; + }; + + // Refresh the view to apply the filter + collectionView.Refresh(); + } + + /// + /// Delete all entries + /// + [Category("NLogViewerControls")] + [Browsable(true)] + [Description("Command to clear all log entries from the viewer")] + public ICommand ClearCommand + { + get => (ICommand) GetValue(ClearCommandProperty); + set => SetValue(ClearCommandProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty ClearCommandProperty = DependencyProperty.Register(nameof(ClearCommand), + typeof(ICommand), typeof(NLogViewerBase), new PropertyMetadata(null)); + + /// + /// Command to add a new search term + /// + [Category("NLogViewerControls")] + [Browsable(true)] + [Description("Command to add a new search term")] + public ICommand AddSearchTermCommand + { + get => (ICommand) GetValue(AddSearchTermCommandProperty); + set => SetValue(AddSearchTermCommandProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty AddSearchTermCommandProperty = DependencyProperty.Register(nameof(AddSearchTermCommand), + typeof(ICommand), typeof(NLogViewerBase), new PropertyMetadata(null)); + + /// + /// Command to clear all search terms + /// + [Category("NLogViewerControls")] + [Browsable(true)] + [Description("Command to clear all search terms")] + public ICommand ClearAllSearchTermsCommand + { + get => (ICommand) GetValue(ClearAllSearchTermsCommandProperty); + set => SetValue(ClearAllSearchTermsCommandProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty ClearAllSearchTermsCommandProperty = DependencyProperty.Register(nameof(ClearAllSearchTermsCommand), + typeof(ICommand), typeof(NLogViewerBase), new PropertyMetadata(null)); + + /// + /// Command to add a regex search term from a provided text + /// + [Category("NLogViewerControls")] + [Browsable(true)] + [Description("Command to add a regex search term from the provided text")] + public ICommand AddRegexSearchTermCommand + { + get => (ICommand) GetValue(AddRegexSearchTermCommandProperty); + set => SetValue(AddRegexSearchTermCommandProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty AddRegexSearchTermCommandProperty = DependencyProperty.Register(nameof(AddRegexSearchTermCommand), + typeof(ICommand), typeof(NLogViewerBase), new PropertyMetadata(null)); + + /// + /// Command to add a regex search term as an exclude filter from a provided text + /// + [Category("NLogViewerControls")] + [Browsable(true)] + [Description("Command to add a regex search term as an exclude filter from the provided text")] + public ICommand AddRegexSearchTermExcludeCommand + { + get => (ICommand) GetValue(AddRegexSearchTermExcludeCommandProperty); + set => SetValue(AddRegexSearchTermExcludeCommandProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty AddRegexSearchTermExcludeCommandProperty = DependencyProperty.Register(nameof(AddRegexSearchTermExcludeCommand), + typeof(ICommand), typeof(NLogViewerBase), new PropertyMetadata(null)); + + /// + /// Command to remove a specific search term + /// + [Category("NLogViewerControls")] + [Browsable(true)] + [Description("Command to remove a specific search term")] + public ICommand RemoveSearchTermCommand + { + get => (ICommand) GetValue(RemoveSearchTermCommandProperty); + set => SetValue(RemoveSearchTermCommandProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty RemoveSearchTermCommandProperty = DependencyProperty.Register(nameof(RemoveSearchTermCommand), + typeof(ICommand), typeof(NLogViewerBase), new PropertyMetadata(null)); + + /// + /// Command to edit a search term by copying it to the current search text + /// + [Category("NLogViewerControls")] + [Browsable(true)] + [Description("Command to edit a search term by copying it to the current search text")] + public ICommand EditSearchTermCommand + { + get => (ICommand) GetValue(EditSearchTermCommandProperty); + set => SetValue(EditSearchTermCommandProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty EditSearchTermCommandProperty = DependencyProperty.Register(nameof(EditSearchTermCommand), + typeof(ICommand), typeof(NLogViewerBase), new PropertyMetadata(null)); + + /// + /// Command to copy text to clipboard + /// + [Category("NLogViewerControls")] + [Browsable(true)] + [Description("Command to copy text to clipboard")] + public ICommand CopyToClipboardCommand + { + get => (ICommand) GetValue(CopyToClipboardCommandProperty); + set => SetValue(CopyToClipboardCommandProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty CopyToClipboardCommandProperty = DependencyProperty.Register(nameof(CopyToClipboardCommand), + typeof(ICommand), typeof(NLogViewerBase), new PropertyMetadata(null)); + + /// + /// Command to export filtered log entries to a file + /// + [Category("NLogViewerControls")] + [Browsable(true)] + [Description("Command to export filtered log entries to a file")] + public ICommand ExportCommand + { + get => (ICommand) GetValue(ExportCommandProperty); + set => SetValue(ExportCommandProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty ExportCommandProperty = DependencyProperty.Register(nameof(ExportCommand), + typeof(ICommand), typeof(NLogViewerBase), new PropertyMetadata(null)); + + /// + /// Command to scroll to the end of the log entries + /// + [Category("NLogViewerControls")] + [Browsable(true)] + [Description("Command to scroll to the end of the log entries")] + public ICommand ScrollToEndCommand + { + get => (ICommand) GetValue(ScrollToEndCommandProperty); + set => SetValue(ScrollToEndCommandProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty ScrollToEndCommandProperty = DependencyProperty.Register(nameof(ScrollToEndCommand), + typeof(ICommand), typeof(NLogViewerBase), new FrameworkPropertyMetadata + { + BindsTwoWayByDefault = true, + DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged + }); + + /// + /// Stop logging + /// + [Category("NLogViewerControls")] + [Browsable(true)] + [Description("Pause or resume logging to improve performance when not actively monitoring")] + public bool Pause + { + get => (bool)GetValue(PauseProperty); + set => SetValue(PauseProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty PauseProperty = DependencyProperty.Register(nameof(Pause), typeof(bool), typeof(NLogViewerBase), new PropertyMetadata(false, OnPauseChanged)); + + private static void OnPauseChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is NLogViewerBase viewer && !DesignerProperties.GetIsInDesignMode(viewer)) + viewer.OnPausePropertyChanged((bool)e.NewValue); + } + + /// + /// Called when changes. Default does nothing (external is unaffected). + /// + protected virtual void OnPausePropertyChanged(bool isPaused) + { + } + + /// + /// The maximum number of entries before automatic cleaning is performed. There is a hysteresis of 100 entries which must be exceeded. + /// Example: is '1000'. Then after '1100' entries, everything until '1000' is deleted. + /// If set to '0' or less, it is deactivated + /// + [Category("NLogViewer")] + public int MaxCount + { + get => (int)GetValue(MaxCountProperty); + set => SetValue(MaxCountProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty MaxCountProperty = DependencyProperty.Register(nameof(MaxCount), typeof(int), typeof(NLogViewerBase), + new PropertyMetadata(5000, OnMaxCountChanged)); + + private static void OnMaxCountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is not NLogViewerBase viewer || DesignerProperties.GetIsInDesignMode(viewer)) + return; + viewer.ApplyMaxCountTrim(viewer.GetEffectiveEntries()); + } + + + #endregion + + // ########################################################################################## + // Resolver + // ########################################################################################## + + #region Resolver + + /// + /// The to format the id + /// + [Category("NLogViewerResolver")] + public ILogEventInfoResolver IdResolver + { + get => (ILogEventInfoResolver)GetValue(IdResolverProperty); + set => SetValue(IdResolverProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty IdResolverProperty = DependencyProperty.Register(nameof(IdResolver), typeof(ILogEventInfoResolver), typeof(NLogViewerBase), new PropertyMetadata(new IdResolver())); + + /// + /// The to format the timestamp output + /// + [Category("NLogViewerResolver")] + public ILogEventInfoResolver TimeStampResolver + { + get => (ILogEventInfoResolver)GetValue(TimeStampResolverProperty); + set => SetValue(TimeStampResolverProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty TimeStampResolverProperty = DependencyProperty.Register(nameof(TimeStampResolver), typeof(ILogEventInfoResolver), typeof(NLogViewerBase), new PropertyMetadata(new TimeStampResolver())); + + /// + /// The to format the loggername + /// + [Category("NLogViewerResolver")] + public ILogEventInfoResolver LoggerNameResolver + { + get => (ILogEventInfoResolver)GetValue(LoggerNameResolverProperty); + set => SetValue(LoggerNameResolverProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty LoggerNameResolverProperty = DependencyProperty.Register(nameof(LoggerNameResolver), typeof(ILogEventInfoResolver), typeof(NLogViewerBase), new PropertyMetadata(new LoggerNameResolver())); + + /// + /// The to format the message + /// + [Category("NLogViewerResolver")] + public ILogEventInfoResolver MessageResolver + { + get => (ILogEventInfoResolver)GetValue(MessageResolverProperty); + set => SetValue(MessageResolverProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty MessageResolverProperty = DependencyProperty.Register(nameof(MessageResolver), typeof(ILogEventInfoResolver), typeof(NLogViewerBase), new PropertyMetadata(new MessageResolver())); + + /// + /// The to format log entries for export + /// + [Category("NLogViewerResolver")] + public ILogExportFormatter ExportFormatter + { + get => (ILogExportFormatter)GetValue(ExportFormatterProperty); + set => SetValue(ExportFormatterProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty ExportFormatterProperty = DependencyProperty.Register(nameof(ExportFormatter), typeof(ILogExportFormatter), typeof(NLogViewerBase), new PropertyMetadata(new DefaultLogExportFormatter())); + + #endregion + + // ########################################################################################## + // Column Visibility + // ########################################################################################## + + #region Column Visibility + + /// + /// Controls the visibility of the ID column + /// + [Category("NLogViewerColumns")] + public bool ShowIdColumn + { + get => (bool)GetValue(ShowIdColumnProperty); + set => SetValue(ShowIdColumnProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty ShowIdColumnProperty = + DependencyProperty.Register(nameof(ShowIdColumn), typeof(bool), typeof(NLogViewerBase), + new PropertyMetadata(true, OnColumnVisibilityChanged)); + + /// + /// Controls the visibility of the Level column + /// + [Category("NLogViewerColumns")] + public bool ShowLevelColumn + { + get => (bool)GetValue(ShowLevelColumnProperty); + set => SetValue(ShowLevelColumnProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty ShowLevelColumnProperty = + DependencyProperty.Register(nameof(ShowLevelColumn), typeof(bool), typeof(NLogViewerBase), + new PropertyMetadata(true, OnColumnVisibilityChanged)); + + /// + /// Controls the visibility of the TimeStamp column + /// + [Category("NLogViewerColumns")] + public bool ShowTimeStampColumn + { + get => (bool)GetValue(ShowTimeStampColumnProperty); + set => SetValue(ShowTimeStampColumnProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty ShowTimeStampColumnProperty = + DependencyProperty.Register(nameof(ShowTimeStampColumn), typeof(bool), typeof(NLogViewerBase), + new PropertyMetadata(true, OnColumnVisibilityChanged)); + + /// + /// Controls the visibility of the LoggerName column + /// + [Category("NLogViewerColumns")] + public bool ShowLoggerNameColumn + { + get => (bool)GetValue(ShowLoggerNameColumnProperty); + set => SetValue(ShowLoggerNameColumnProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty ShowLoggerNameColumnProperty = + DependencyProperty.Register(nameof(ShowLoggerNameColumn), typeof(bool), typeof(NLogViewerBase), + new PropertyMetadata(true, OnColumnVisibilityChanged)); + + + private static void OnColumnVisibilityChanged(DependencyObject d, DependencyPropertyChangedEventArgs args) + { + if (d is NLogViewerBase instance) + { + instance.UpdateColumnVisibility(); + } + } + + /// + /// Updates column visibility by removing or adding columns based on the Show properties. + /// When a column is hidden, it's removed from the grid. When shown again, it's added back at its original position. + /// + private void UpdateColumnVisibility() + { + if (PART_ListView?.View is not AutoSizedGridView gridView) return; + + // Update ID column visibility (original index 0) + UpdateColumnVisibility(gridView, _originalIdColumn, ShowIdColumn, _originalIdColumnWidth, 0); + + // Update Level column visibility (original index 1) + UpdateColumnVisibility(gridView, _originalLevelColumn, ShowLevelColumn, _originalLevelColumnWidth, 1); + + // Update TimeStamp column visibility (original index 2) + UpdateColumnVisibility(gridView, _originalTimeStampColumn, ShowTimeStampColumn, _originalTimeStampColumnWidth, 2); + + // Update LoggerName column visibility (original index 3) + UpdateColumnVisibility(gridView, _originalLoggerNameColumn, ShowLoggerNameColumn, _originalLoggerNameColumnWidth, 3); + + // Message column is always visible (no visibility control) + } + + /// + /// Helper method to update individual column visibility by removing or adding the column. + /// Calculates the correct insertion position based on which columns are currently visible. + /// + /// The GridView containing the columns + /// The original column reference to add/remove + /// Whether the column should be visible + /// The original width of the column + /// The original index of the column (0=ID, 1=Level, 2=TimeStamp, 3=LoggerName) + private void UpdateColumnVisibility(AutoSizedGridView gridView, GridViewColumn originalColumn, bool showColumn, double originalWidth, int originalIndex) + { + if (originalColumn == null) return; + + // Check if column is currently in the grid + var columnExists = gridView.Columns.Contains(originalColumn); + + if (showColumn && !columnExists) + { + // Column should be visible but is not in the grid - add it back + originalColumn.Width = originalWidth; + + // Calculate the correct insertion index based on which columns are currently visible + var insertionIndex = CalculateInsertionIndex(gridView, originalIndex); + gridView.Columns.Insert(insertionIndex, originalColumn); + } + else if (!showColumn && columnExists) + { + // Column should be hidden but is in the grid - remove it + gridView.Columns.Remove(originalColumn); + } + } + + /// + /// Calculates the correct insertion index for a column based on its original position + /// and which columns are currently visible. + /// + /// The GridView containing the columns + /// The original index of the column to insert + /// The correct insertion index + private int CalculateInsertionIndex(AutoSizedGridView gridView, int originalIndex) + { + // Count how many visible columns come before this one in the original order + var insertionIndex = 0; + + // Check ID column (original index 0) + if (originalIndex > 0 && ShowIdColumn && gridView.Columns.Contains(_originalIdColumn)) + { + insertionIndex++; + } + + // Check Level column (original index 1) + if (originalIndex > 1 && ShowLevelColumn && gridView.Columns.Contains(_originalLevelColumn)) + { + insertionIndex++; + } + + // Check TimeStamp column (original index 2) + if (originalIndex > 2 && ShowTimeStampColumn && gridView.Columns.Contains(_originalTimeStampColumn)) + { + insertionIndex++; + } + + // Check LoggerName column (original index 3) + if (originalIndex > 3 && ShowLoggerNameColumn && gridView.Columns.Contains(_originalLoggerNameColumn)) + { + insertionIndex++; + } + + return insertionIndex; + } + + #endregion + + // ########################################################################################## + // Filter Properties + // ########################################################################################## + + #region Filter Properties + + /// + /// Filter for Trace log level + /// + [Category("NLogViewerFilters")] + [Browsable(true)] + [Description("Hide/show Trace level log entries")] + public bool TraceFilter + { + get => (bool)GetValue(TraceFilterProperty); + set => SetValue(TraceFilterProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty TraceFilterProperty = + DependencyProperty.Register(nameof(TraceFilter), typeof(bool), typeof(NLogViewerBase), + new PropertyMetadata(false, OnFilterChanged)); + + /// + /// Filter for Debug log level + /// + [Category("NLogViewerFilters")] + [Browsable(true)] + [Description("Hide/show Debug level log entries")] + public bool DebugFilter + { + get => (bool)GetValue(DebugFilterProperty); + set => SetValue(DebugFilterProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty DebugFilterProperty = + DependencyProperty.Register(nameof(DebugFilter), typeof(bool), typeof(NLogViewerBase), + new PropertyMetadata(false, OnFilterChanged)); + + /// + /// Filter for Info log level + /// + [Category("NLogViewerFilters")] + [Browsable(true)] + [Description("Hide/show Info level log entries")] + public bool InfoFilter + { + get => (bool)GetValue(InfoFilterProperty); + set => SetValue(InfoFilterProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty InfoFilterProperty = + DependencyProperty.Register(nameof(InfoFilter), typeof(bool), typeof(NLogViewerBase), + new PropertyMetadata(false, OnFilterChanged)); + + /// + /// Filter for Warn log level + /// + [Category("NLogViewerFilters")] + [Browsable(true)] + [Description("Hide/show Warn level log entries")] + public bool WarnFilter + { + get => (bool)GetValue(WarnFilterProperty); + set => SetValue(WarnFilterProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty WarnFilterProperty = + DependencyProperty.Register(nameof(WarnFilter), typeof(bool), typeof(NLogViewerBase), + new PropertyMetadata(false, OnFilterChanged)); + + /// + /// Filter for Error log level + /// + [Category("NLogViewerFilters")] + [Browsable(true)] + [Description("Hide/show Error level log entries")] + public bool ErrorFilter + { + get => (bool)GetValue(ErrorFilterProperty); + set => SetValue(ErrorFilterProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty ErrorFilterProperty = + DependencyProperty.Register(nameof(ErrorFilter), typeof(bool), typeof(NLogViewerBase), + new PropertyMetadata(false, OnFilterChanged)); + + /// + /// Filter for Fatal log level + /// + [Category("NLogViewerFilters")] + [Browsable(true)] + [Description("Hide/show Fatal level log entries")] + public bool FatalFilter + { + get => (bool)GetValue(FatalFilterProperty); + set => SetValue(FatalFilterProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty FatalFilterProperty = + DependencyProperty.Register(nameof(FatalFilter), typeof(bool), typeof(NLogViewerBase), + new PropertyMetadata(false, OnFilterChanged)); + + /// + /// Current search text being typed (not yet activated) + /// + [Category("NLogViewerFilters")] + [Browsable(true)] + [Description("Current search text being typed (activated with Enter key)")] + public string CurrentSearchText + { + get => (string)GetValue(CurrentSearchTextProperty); + set => SetValue(CurrentSearchTextProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty CurrentSearchTextProperty = + DependencyProperty.Register(nameof(CurrentSearchText), typeof(string), typeof(NLogViewerBase), + new PropertyMetadata(string.Empty)); + + /// + /// Whether to use regex pattern matching for search + /// + [Category("NLogViewerFilters")] + [Browsable(true)] + [Description("Use regex pattern matching instead of free text search")] + public bool UseRegexSearch + { + get => (bool)GetValue(UseRegexSearchProperty); + set => SetValue(UseRegexSearchProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty UseRegexSearchProperty = + DependencyProperty.Register(nameof(UseRegexSearch), typeof(bool), typeof(NLogViewerBase), + new PropertyMetadata(false)); + + /// + /// Collection of active search terms + /// + [Category("NLogViewerFilters")] + [Browsable(false)] + [Description("Collection of active search terms")] + public ObservableCollection ActiveSearchTerms + { + get => (ObservableCollection)GetValue(ActiveSearchTermsProperty); + private set => SetValue(ActiveSearchTermsProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty ActiveSearchTermsProperty = + DependencyProperty.Register(nameof(ActiveSearchTerms), typeof(ObservableCollection), typeof(NLogViewerBase), + new PropertyMetadata(null)); + + private static void OnFilterChanged(DependencyObject d, DependencyPropertyChangedEventArgs args) + { + if (d is NLogViewerBase instance) + { + instance.UpdateFilter(); + } + } + + + /// + /// Adds a new search term to the active search terms collection + /// + public void AddSearchTerm() + { + if (string.IsNullOrWhiteSpace(CurrentSearchText)) + return; + + SearchTerm searchTerm; + + if (UseRegexSearch) + { + try + { + searchTerm = new RegexSearchTerm(CurrentSearchText); + } + catch (ArgumentException ex) + { + MessageBox.Show($"Invalid regex pattern: {ex.Message}", "Regex Error", + MessageBoxButton.OK, MessageBoxImage.Warning); + return; + } + } + else + { + searchTerm = new TextSearchTerm(CurrentSearchText); + } + + ActiveSearchTerms.Add(searchTerm); + + // Clear the input + CurrentSearchText = string.Empty; + + // Update filter + UpdateFilter(); + } + + /// + /// Removes a search term from the active search terms collection + /// + public void RemoveSearchTerm(SearchTerm searchTerm) + { + ActiveSearchTerms.Remove(searchTerm); + UpdateFilter(); + } + + /// + /// Clears all active search terms + /// + public void ClearAllSearchTerms() + { + ActiveSearchTerms.Clear(); + UpdateFilter(); + } + + /// + /// Adds a RegexSearchTerm from a provided text and updates the filter + /// Special regex characters are escaped to treat the text as a literal string + /// + /// The text to use as regex pattern (will be escaped) + public void AddRegexSearchTerm(string text) + { + if (string.IsNullOrWhiteSpace(text)) + return; + + try + { + // Escape special regex characters to treat the text as a literal string + var escapedPattern = Regex.Escape(text); + var term = new RegexSearchTerm(escapedPattern); + ActiveSearchTerms.Add(term); + UpdateFilter(); + } + catch (ArgumentException ex) + { + MessageBox.Show($"Invalid regex pattern: {ex.Message}", "Regex Error", + MessageBoxButton.OK, MessageBoxImage.Warning); + } + } + + /// + /// Adds a RegexSearchTerm as an exclude filter from a provided text and updates the filter + /// Special regex characters are escaped and wrapped in a negative lookahead pattern + /// + /// The text to use as regex pattern (will be escaped and wrapped in negative lookahead) + public void AddRegexSearchTermExclude(string text) + { + if (string.IsNullOrWhiteSpace(text)) + return; + + try + { + // Escape special regex characters to treat the text as a literal string + var escapedPattern = Regex.Escape(text); + // Wrap in negative lookahead: matches any text that does NOT contain the pattern + var negativeLookaheadPattern = $"^(?!.*{escapedPattern}$)"; + var term = new RegexSearchTerm(negativeLookaheadPattern); + ActiveSearchTerms.Add(term); + UpdateFilter(); + } + catch (ArgumentException ex) + { + MessageBox.Show($"Invalid regex pattern: {ex.Message}", "Regex Error", + MessageBoxButton.OK, MessageBoxImage.Warning); + } + } + + /// + /// Edits a search term by copying its text to the current search text field + /// + /// The search term to edit + public void EditSearchTerm(SearchTerm searchTerm) + { + if (searchTerm == null) + return; + + // Copy the search term text to the current search text field + CurrentSearchText = searchTerm.Text; + + // Set the regex checkbox based on the search term type + UseRegexSearch = searchTerm is RegexSearchTerm; + + // Remove the original search term + ActiveSearchTerms.Remove(searchTerm); + UpdateFilter(); + } + + /// + /// Copies text to the clipboard + /// + /// The text to copy to clipboard + public void CopyToClipboard(string text) + { + if (string.IsNullOrEmpty(text)) + return; + + try + { + Clipboard.SetText(text); + } + catch (Exception ex) + { + MessageBox.Show($"Failed to copy to clipboard: {ex.Message}", "Clipboard Error", + MessageBoxButton.OK, MessageBoxImage.Warning); + } + } + + /// + /// Gets all filtered log entries from the current view + /// + /// An enumerable collection of filtered LogEventInfo entries + public IEnumerable GetFilteredLogEntries() + { + if (LogEvents?.View == null) + return Enumerable.Empty(); + + return LogEvents.View.Cast(); + } + + /// + /// Exports filtered log entries to a file in the specified format + /// + /// Export parameters containing file path and format + public void ExportLogs(ExportParameter parameter) + { + if (parameter == null) + throw new ArgumentNullException(nameof(parameter)); + + if (string.IsNullOrWhiteSpace(parameter.FilePath)) + throw new ArgumentException("File path cannot be empty", nameof(parameter)); + + try + { + var filteredEntries = GetFilteredLogEntries().ToList(); + + if (filteredEntries.Count == 0) + { + MessageBox.Show("No log entries to export.", "Export", + MessageBoxButton.OK, MessageBoxImage.Information); + return; + } + + switch (parameter.Format) + { + case ExportFormat.Log: + ExportToLogFormat(parameter.FilePath, filteredEntries, parameter.CustomFormatter); + break; + default: + throw new NotSupportedException($"Export format {parameter.Format} is not supported."); + } + + MessageBox.Show($"Successfully exported {filteredEntries.Count} log entry(ies) to {parameter.FilePath}", + "Export Complete", MessageBoxButton.OK, MessageBoxImage.Information); + } + catch (Exception ex) + { + MessageBox.Show($"Failed to export logs: {ex.Message}", "Export Error", + MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + /// + /// Exports log entries to a standard log file format + /// + /// Target file path + /// Log entries to export + /// Optional custom formatter to override the default formatter + private void ExportToLogFormat(string filePath, IEnumerable logEntries, ILogExportFormatter? customFormatter = null) + { + using var writer = new StreamWriter(filePath, false, System.Text.Encoding.UTF8); + + // Use custom formatter if provided, otherwise use ExportFormatter, fall back to default formatter + var formatter = customFormatter ?? ExportFormatter ?? new DefaultLogExportFormatter(); + + foreach (var logEvent in logEntries) + { + var formattedLine = formatter.Format( + logEvent, + TimeStampResolver ?? new TimeStampResolver(), + LoggerNameResolver ?? new LoggerNameResolver(), + MessageResolver ?? new MessageResolver()); + + writer.WriteLine(formattedLine); + } + } + + #endregion + + // ########################################################################################## + // Filter Visibility + // ########################################################################################## + + #region Filter Visibility + + /// + /// Controls the visibility of the filter buttons + /// + [Category("NLogViewerFilters")] + [Browsable(true)] + [Description("Show/hide the filter buttons")] + public bool ShowFilterButtons + { + get => (bool)GetValue(ShowFilterButtonsProperty); + set => SetValue(ShowFilterButtonsProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty ShowFilterButtonsProperty = + DependencyProperty.Register(nameof(ShowFilterButtons), typeof(bool), typeof(NLogViewerBase), + new PropertyMetadata(true)); + + /// + /// Controls the visibility of the search controls + /// + [Category("NLogViewerFilters")] + [Browsable(true)] + [Description("Show/hide the search controls")] + public bool ShowSearchControls + { + get => (bool)GetValue(ShowSearchControlsProperty); + set => SetValue(ShowSearchControlsProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty ShowSearchControlsProperty = + DependencyProperty.Register(nameof(ShowSearchControls), typeof(bool), typeof(NLogViewerBase), + new PropertyMetadata(true)); + + #endregion + + // ########################################################################################## + // Controls Visibility + // ########################################################################################## + + #region Controls Visibility + + /// + /// Controls the visibility of the control buttons (Auto-Scroll, Clear, Pause) + /// + [Category("NLogViewerControls")] + [Browsable(true)] + [Description("Show/hide the control buttons (Auto-Scroll, Clear, Pause)")] + public bool ShowControlButtons + { + get => (bool)GetValue(ShowControlButtonsProperty); + set => SetValue(ShowControlButtonsProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty ShowControlButtonsProperty = + DependencyProperty.Register(nameof(ShowControlButtons), typeof(bool), typeof(NLogViewerBase), + new PropertyMetadata(true)); + + #endregion + + #endregion + + // ############################################################################################################################## + // Properties + // ############################################################################################################################## + + #region Properties + + // ########################################################################################## + // Public Properties + // ########################################################################################## + + + + // ########################################################################################## + // Private Properties + // ########################################################################################## + + private readonly ObservableCollection _internalLogEntries = new(); + private ObservableCollection? _collectionChangedSubscriber; + private Window? _ParentWindow; + + // Store original column references and widths for restoration + private GridViewColumn _originalIdColumn; + private GridViewColumn _originalLevelColumn; + private GridViewColumn _originalTimeStampColumn; + private GridViewColumn _originalLoggerNameColumn; + private double _originalIdColumnWidth = 40; + private double _originalLevelColumnWidth = double.NaN; // Auto + private double _originalTimeStampColumnWidth = double.NaN; // Auto + private double _originalLoggerNameColumnWidth = double.NaN; // Auto + + /// + /// Reference to the ListView template part for programmatic access + /// + private ListView PART_ListView => GetTemplateChild("PART_ListView") as ListView; + + #endregion + + // ############################################################################################################################## + // Collection / ItemsSource + // ############################################################################################################################## + + /// + /// Returns the collection currently feeding (external or the internal list). + /// + protected ObservableCollection GetEffectiveEntries() + => ItemsSource ?? _internalLogEntries; + + /// + /// Adds log entries on the UI thread batch path (used by when subscribing to ). + /// + protected internal void AppendLogEntriesBatch(IEnumerable infos) + { + if (LogEvents == null) + return; + + var coll = GetEffectiveEntries(); + using (LogEvents.DeferRefresh()) + { + foreach (var info in infos) + coll.Add(info); + ApplyMaxCountTrim(coll); + } + + if (AutoScroll) + PART_ListView?.ScrollToEnd(); + } + + private void SyncItemsSourceToView() + { + if (LogEvents != null) + LogEvents.Source = GetEffectiveEntries(); + + AttachEffectiveCollectionChangedHandler(); + LogEvents?.View?.Refresh(); + UpdateFilter(); + } + + private void AttachEffectiveCollectionChangedHandler() + { + var effective = GetEffectiveEntries(); + if (_collectionChangedSubscriber == effective) + return; + + if (_collectionChangedSubscriber != null) + _collectionChangedSubscriber.CollectionChanged -= OnLogEntriesCollectionChanged; + + _collectionChangedSubscriber = effective; + _collectionChangedSubscriber.CollectionChanged += OnLogEntriesCollectionChanged; + } + + private void OnLogEntriesCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (sender is not ObservableCollection coll) + return; + + if (e.Action is NotifyCollectionChangedAction.Add or NotifyCollectionChangedAction.Reset or NotifyCollectionChangedAction.Replace) + ApplyMaxCountTrim(coll); + + if (AutoScroll) + PART_ListView?.ScrollToEnd(); + } + + private void ApplyMaxCountTrim(ObservableCollection coll) + { + if (MaxCount < 0 || LogEvents == null) + return; + + if (coll.Count - 100 <= MaxCount) + return; + + using (LogEvents.DeferRefresh()) + { + while (coll.Count > MaxCount) + coll.RemoveAt(0); + } + } + + // ############################################################################################################################## + // Constructor + // ############################################################################################################################## + + #region Constructor + + static NLogViewerBase() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(NLogViewerBase), new FrameworkPropertyMetadata(typeof(NLogViewerBase))); + } + + /// + /// Initializes a new instance of the log viewer base control. + /// + public NLogViewerBase() + { + DataContext = this; + + Uid = GetHashCode().ToString(); + + if (DesignerProperties.GetIsInDesignMode(this)) + return; + + LogEvents = new CollectionViewSource { Source = _internalLogEntries }; + ActiveSearchTerms = new ObservableCollection(); + UpdateFilter(); + + AttachEffectiveCollectionChangedHandler(); + + Loaded += _OnLoaded; + Unloaded += _OnUnloaded; + + ClearCommand = new RelayCommand(() => GetEffectiveEntries().Clear()); + AddSearchTermCommand = new RelayCommand(AddSearchTerm); + ClearAllSearchTermsCommand = new RelayCommand(ClearAllSearchTerms); + RemoveSearchTermCommand = new RelayCommand(RemoveSearchTerm); + AddRegexSearchTermCommand = new RelayCommand(AddRegexSearchTerm); + AddRegexSearchTermExcludeCommand = new RelayCommand(AddRegexSearchTermExclude); + EditSearchTermCommand = new RelayCommand(EditSearchTerm); + CopyToClipboardCommand = new RelayCommand(CopyToClipboard); + ExportCommand = new RelayCommand(ExportLogs); + ScrollToEndCommand = new RelayCommand(() => + { + PART_ListView?.ScrollToEnd(); + }); + } + + private void _OnUnloaded(object sender, RoutedEventArgs e) + { + var window = _ParentWindow ?? Window.GetWindow(this); + if (window == null || window.FindChildByUid(Uid) == null) + DisposeViewerResources(); + } + + private void _ParentWindowOnClosed(object sender, EventArgs e) + { + DisposeViewerResources(); + } + + /// + /// Releases listener-specific resources (overridden by for cache subscription disposal). + /// + protected virtual void DisposeViewerResources() + { + } + + private void _OnLoaded(object sender, RoutedEventArgs e) + { + Loaded -= _OnLoaded; + + var window = Window.GetWindow(this); + if (_ParentWindow == null && window != null) + { + _ParentWindow = window; + _ParentWindow.Closed += _ParentWindowOnClosed; + } + + PART_ListView?.ScrollToEnd(); + + if (PART_ListView?.View is AutoSizedGridView gridView) + { + if (gridView.Columns.Count > 0) + { + _originalIdColumn = gridView.Columns[0]; + _originalIdColumnWidth = gridView.Columns[0].Width; + } + + if (gridView.Columns.Count > 1) + { + _originalLevelColumn = gridView.Columns[1]; + _originalLevelColumnWidth = gridView.Columns[1].Width; + } + + if (gridView.Columns.Count > 2) + { + _originalTimeStampColumn = gridView.Columns[2]; + _originalTimeStampColumnWidth = gridView.Columns[2].Width; + } + + if (gridView.Columns.Count > 3) + { + _originalLoggerNameColumn = gridView.Columns[3]; + _originalLoggerNameColumnWidth = gridView.Columns[3].Width; + } + + UpdateColumnVisibility(); + } + + OnViewerLoaded(); + } + + /// + /// Called once after the template is applied and columns are initialized. + /// + protected virtual void OnViewerLoaded() + { + } + + #endregion + } +} diff --git a/ui/Sentinel.NLogViewer.Wpf/Themes/Generic.xaml b/ui/Sentinel.NLogViewer.Wpf/Themes/Generic.xaml index 09a6919..c234d96 100644 --- a/ui/Sentinel.NLogViewer.Wpf/Themes/Generic.xaml +++ b/ui/Sentinel.NLogViewer.Wpf/Themes/Generic.xaml @@ -13,10 +13,10 @@ - @@ -332,7 +332,7 @@ - + @@ -380,7 +380,7 @@ - + @@ -388,10 +388,10 @@ @@ -439,10 +439,10 @@ From 0185434cb22c42838718a1eee5cd7257021637e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=2EB=C3=B6xler?= Date: Tue, 5 May 2026 16:34:14 +0200 Subject: [PATCH 2/5] feat(app): add file drop and import destination dialog for log batches --- .../ImportDestinationWindow.xaml | 66 +++ .../ImportDestinationWindow.xaml.cs | 164 ++++++ app/Sentinel.NLogViewer.App/MainWindow.xaml | 6 +- .../MainWindow.xaml.cs | 30 ++ .../Models/LogFileImportBatchContext.cs | 39 ++ .../Resources/Resources.de.resx | 60 +++ .../Resources/Resources.en.resx | 60 +++ .../Resources/Resources.es.resx | 60 +++ .../Resources/Resources.fr.resx | 60 +++ .../Resources/Resources.hu.resx | 60 +++ .../Resources/Resources.pl.resx | 60 +++ .../Resources/Resources.resx | 60 +++ .../Resources/Resources.ro.resx | 60 +++ .../ViewModels/MainViewModel.cs | 493 ++++++++++++------ 14 files changed, 1104 insertions(+), 174 deletions(-) create mode 100644 app/Sentinel.NLogViewer.App/ImportDestinationWindow.xaml create mode 100644 app/Sentinel.NLogViewer.App/ImportDestinationWindow.xaml.cs create mode 100644 app/Sentinel.NLogViewer.App/Models/LogFileImportBatchContext.cs diff --git a/app/Sentinel.NLogViewer.App/ImportDestinationWindow.xaml b/app/Sentinel.NLogViewer.App/ImportDestinationWindow.xaml new file mode 100644 index 0000000..d26ece2 --- /dev/null +++ b/app/Sentinel.NLogViewer.App/ImportDestinationWindow.xaml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + +