diff --git a/ColorControl.sln b/ColorControl.sln index 722c607..09a46d9 100644 --- a/ColorControl.sln +++ b/ColorControl.sln @@ -11,6 +11,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NvAPIWrapper", "NvAPIWrappe EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "novideo_srgb", "novideo_srgb\novideo_srgb.csproj", "{A6A97834-7BE1-474A-B92F-A512DB1D5186}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NLogViewer", "NLogViewer\NLogViewer.csproj", "{D5E4D314-CFCD-4859-98FC-68F660AC7FB4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -51,6 +53,14 @@ Global {A6A97834-7BE1-474A-B92F-A512DB1D5186}.Release|Any CPU.Build.0 = Release|Any CPU {A6A97834-7BE1-474A-B92F-A512DB1D5186}.Release|x64.ActiveCfg = Release|x64 {A6A97834-7BE1-474A-B92F-A512DB1D5186}.Release|x64.Build.0 = Release|x64 + {D5E4D314-CFCD-4859-98FC-68F660AC7FB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D5E4D314-CFCD-4859-98FC-68F660AC7FB4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D5E4D314-CFCD-4859-98FC-68F660AC7FB4}.Debug|x64.ActiveCfg = Debug|Any CPU + {D5E4D314-CFCD-4859-98FC-68F660AC7FB4}.Debug|x64.Build.0 = Debug|Any CPU + {D5E4D314-CFCD-4859-98FC-68F660AC7FB4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D5E4D314-CFCD-4859-98FC-68F660AC7FB4}.Release|Any CPU.Build.0 = Release|Any CPU + {D5E4D314-CFCD-4859-98FC-68F660AC7FB4}.Release|x64.ActiveCfg = Release|Any CPU + {D5E4D314-CFCD-4859-98FC-68F660AC7FB4}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ColorControl/AppContext.cs b/ColorControl/AppContext.cs index 8d57f0b..0ebcb81 100644 --- a/ColorControl/AppContext.cs +++ b/ColorControl/AppContext.cs @@ -1,4 +1,6 @@ using ColorControl.Common; +using NLog.Config; +using System; using System.ComponentModel; using System.Threading; @@ -13,12 +15,15 @@ namespace ColorControl public StartUpParams StartUpParams { get; private set; } public string DataPath { get; private set; } public SynchronizationContext SynchronizationContext { get; set; } = AsyncOperationManager.SynchronizationContext; + public LoggingRule LoggingRule { get; private set; } + public DateTime StartTime { get; private set; } = DateTime.Now; - public AppContext(Config config, StartUpParams startUpParams, string dataPath) + public AppContext(Config config, StartUpParams startUpParams, string dataPath, LoggingRule loggingRule) { Config = config; StartUpParams = startUpParams; DataPath = dataPath; + LoggingRule = loggingRule; CurrentContext = this; } @@ -27,5 +32,11 @@ namespace ColorControl { return Utils.IsServiceRunning(); } + + public void SetLogLevel(NLog.LogLevel logLevel) + { + Config.LogLevel = logLevel.ToString(); + LoggingRule.SetLoggingLevels(logLevel, NLog.LogLevel.Fatal); + } } } diff --git a/ColorControl/ColorControl.csproj b/ColorControl/ColorControl.csproj index 5a89eee..0c6b14d 100644 --- a/ColorControl/ColorControl.csproj +++ b/ColorControl/ColorControl.csproj @@ -47,6 +47,9 @@ bin\x64\Release\ MinimumRecommendedRules.ruleset + + + True @@ -93,8 +96,6 @@ - - @@ -104,6 +105,12 @@ + + MSBuild:Compile + + + + diff --git a/ColorControl/Common/Utils.cs b/ColorControl/Common/Utils.cs index c2db8e0..9631d7c 100644 --- a/ColorControl/Common/Utils.cs +++ b/ColorControl/Common/Utils.cs @@ -1117,6 +1117,46 @@ The best and suggested method to provide this is via a Windows Service. Only whe return null; } + public static List ReadLines(string fileName, bool reverse = false) + { + var exists = File.Exists(fileName); + + if (!exists) + { + return null; + } + + try + { + var lines = new List(); + + using var fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + + using var reader = new StreamReader(fs); + + string line; + + while ((line = reader.ReadLine()) != null) + { + if (reverse) + { + lines.Insert(0, line + "\r\n"); + continue; + } + + lines.Add(line); + } + + return lines; + } + catch (Exception e) + { + Logger.Error(e.ToLogString()); + } + + return null; + } + public static bool WriteObject(string fileName, object value) { var data = JsonConvert.SerializeObject(value); diff --git a/ColorControl/Config.cs b/ColorControl/Config.cs index eef87d7..1865167 100644 --- a/ColorControl/Config.cs +++ b/ColorControl/Config.cs @@ -45,10 +45,12 @@ namespace ColorControl public ElevationMethod ElevationMethod { get; set; } public bool ElevationMethodAsked { get; set; } public int ProcessMonitorPollingInterval { get; set; } + public string LogLevel { get; set; } public List Modules { get; set; } public Config() { + LogLevel = "DEBUG"; DisplaySettingsDelay = 1000; ScreenSaverShortcut = string.Empty; FormWidth = 900; diff --git a/ColorControl/MainForm.Designer.cs b/ColorControl/MainForm.Designer.cs index 86f3b3a..ca56c3b 100644 --- a/ColorControl/MainForm.Designer.cs +++ b/ColorControl/MainForm.Designer.cs @@ -41,6 +41,7 @@ lblFixChromeFontsDescription = new System.Windows.Forms.Label(); chkFixChromeFonts = new System.Windows.Forms.CheckBox(); grpGeneralOptions = new System.Windows.Forms.GroupBox(); + btnOptionsAdvanced = new System.Windows.Forms.Button(); chkOptionsUseDarkMode = new System.Windows.Forms.CheckBox(); chkAutoInstallUpdates = new System.Windows.Forms.CheckBox(); btnStartStopService = new System.Windows.Forms.Button(); @@ -70,7 +71,7 @@ label7 = new System.Windows.Forms.Label(); lblInfo = new System.Windows.Forms.Label(); btnUpdate = new System.Windows.Forms.Button(); - btnOptionsAdvanced = new System.Windows.Forms.Button(); + btnOptionsLog = new System.Windows.Forms.Button(); tcMain.SuspendLayout(); tabOptions.SuspendLayout(); grpOptionsModules.SuspendLayout(); @@ -216,6 +217,7 @@ // // grpGeneralOptions // + grpGeneralOptions.Controls.Add(btnOptionsLog); grpGeneralOptions.Controls.Add(btnOptionsAdvanced); grpGeneralOptions.Controls.Add(chkOptionsUseDarkMode); grpGeneralOptions.Controls.Add(chkAutoInstallUpdates); @@ -241,6 +243,17 @@ grpGeneralOptions.TabStop = false; grpGeneralOptions.Text = "General"; // + // btnOptionsAdvanced + // + btnOptionsAdvanced.Location = new System.Drawing.Point(7, 172); + btnOptionsAdvanced.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + btnOptionsAdvanced.Name = "btnOptionsAdvanced"; + btnOptionsAdvanced.Size = new System.Drawing.Size(88, 27); + btnOptionsAdvanced.TabIndex = 11; + btnOptionsAdvanced.Text = "Advanced..."; + btnOptionsAdvanced.UseVisualStyleBackColor = true; + btnOptionsAdvanced.Click += btnOptionsAdvanced_Click; + // // chkOptionsUseDarkMode // chkOptionsUseDarkMode.AutoSize = true; @@ -591,16 +604,16 @@ btnUpdate.Visible = false; btnUpdate.Click += btnUpdate_Click; // - // btnOptionsAdvanced + // btnOptionsLog // - btnOptionsAdvanced.Location = new System.Drawing.Point(7, 172); - btnOptionsAdvanced.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); - btnOptionsAdvanced.Name = "btnOptionsAdvanced"; - btnOptionsAdvanced.Size = new System.Drawing.Size(88, 27); - btnOptionsAdvanced.TabIndex = 11; - btnOptionsAdvanced.Text = "Advanced..."; - btnOptionsAdvanced.UseVisualStyleBackColor = true; - btnOptionsAdvanced.Click += btnOptionsAdvanced_Click; + btnOptionsLog.Location = new System.Drawing.Point(103, 172); + btnOptionsLog.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + btnOptionsLog.Name = "btnOptionsLog"; + btnOptionsLog.Size = new System.Drawing.Size(88, 27); + btnOptionsLog.TabIndex = 68; + btnOptionsLog.Text = "Logging..."; + btnOptionsLog.UseVisualStyleBackColor = true; + btnOptionsLog.Click += btnOptionsLog_Click; // // MainForm // @@ -686,6 +699,7 @@ private System.Windows.Forms.Label label1; private System.Windows.Forms.CheckBox chkOptionsUseDarkMode; private System.Windows.Forms.Button btnOptionsAdvanced; + private System.Windows.Forms.Button btnOptionsLog; } } diff --git a/ColorControl/MainForm.cs b/ColorControl/MainForm.cs index 28b91cb..1de9884 100644 --- a/ColorControl/MainForm.cs +++ b/ColorControl/MainForm.cs @@ -9,6 +9,7 @@ using ColorControl.Services.LG; using ColorControl.Services.NVIDIA; using ColorControl.Services.Samsung; using ColorControl.Svc; +using ColorControl.XForms; using Microsoft.Extensions.DependencyInjection; using NLog; using novideo_srgb; @@ -1501,5 +1502,10 @@ Currently ColorControl is {(Utils.IsAdministrator() ? "" : "not ")}running as ad _config.ProcessMonitorPollingInterval = processPollingIntervalField.ValueAsInt; } + + private void btnOptionsLog_Click(object sender, EventArgs e) + { + LogWindow.CreateAndShow(); + } } } \ No newline at end of file diff --git a/ColorControl/Program.cs b/ColorControl/Program.cs index 3354a1c..f705f64 100644 --- a/ColorControl/Program.cs +++ b/ColorControl/Program.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Newtonsoft.Json; using NLog; +using NLog.Config; using NWin32; using System; using System.Diagnostics; @@ -39,6 +40,7 @@ namespace ColorControl private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private static Mutex _mutex; private static MainForm _mainForm; + private static LoggingRule _loggingRule; [STAThread] // autogenerated public static void Main(string[] args) @@ -70,7 +72,7 @@ namespace ColorControl if (runAsService) { - AppContext = new AppContext(null, new StartUpParams(), DataDir); + AppContext = new AppContext(null, new StartUpParams(), DataDir, _loggingRule); await RunService(args); return; @@ -93,7 +95,7 @@ namespace ColorControl var startUpParams = StartUpParams.Parse(args); - AppContext = new AppContext(Config, startUpParams, DataDir); + AppContext = new AppContext(Config, startUpParams, DataDir, _loggingRule); var existingProcess = Utils.GetProcessByName("ColorControl"); @@ -178,7 +180,7 @@ namespace ColorControl private static void InitLogger(bool runAsService) { - var config = new NLog.Config.LoggingConfiguration(); + var config = new LoggingConfiguration(); // Targets where to log to: File and Console var logFileAppendix = runAsService ? "_svc" : ""; @@ -187,11 +189,10 @@ namespace ColorControl var logfile = new NLog.Targets.FileTarget("logfile") { FileName = LogFilename }; - //var logconsole = new NLog.Targets.ConsoleTarget("logconsole"); + _loggingRule = new LoggingRule("*", LogLevel.Trace, LogLevel.Fatal, logfile); // Rules for mapping loggers to targets - //config.AddRule(LogLevel.Info, LogLevel.Fatal, logconsole); - config.AddRule(LogLevel.Trace, LogLevel.Fatal, logfile); + config.AddRule(_loggingRule); // Apply config LogManager.Configuration = config; @@ -216,6 +217,10 @@ namespace ColorControl Config ??= new Config(); Utils.UseDedicatedElevatedProcess = Config.UseDedicatedElevatedProcess; + + var minLogLevel = LogLevel.FromString(Config.LogLevel); + + _loggingRule.SetLoggingLevels(minLogLevel, LogLevel.Fatal); } public static async Task HandleStartupParams(StartUpParams startUpParams, Process existingProcess) diff --git a/ColorControl/Services/Samsung/SamTvConnection.cs b/ColorControl/Services/Samsung/SamTvConnection.cs index 258f9f6..786197b 100644 --- a/ColorControl/Services/Samsung/SamTvConnection.cs +++ b/ColorControl/Services/Samsung/SamTvConnection.cs @@ -244,6 +244,10 @@ namespace ColorControl.Services.Samsung // taskCompletion.TrySetResult(new { clientKey = key }); //} + } + else + { + } //else if (_tokens.TryGetValue(id, out taskCompletion)) //{ diff --git a/ColorControl/Services/Samsung/SamsungButtons.cs b/ColorControl/Services/Samsung/SamsungButtons.cs index e3a9f08..34c4814 100644 --- a/ColorControl/Services/Samsung/SamsungButtons.cs +++ b/ColorControl/Services/Samsung/SamsungButtons.cs @@ -37,6 +37,8 @@ KEY_9, KEY_DTV, KEY_HDMI, - KEY_CONTENTS + KEY_CONTENTS, + KEY_TV_MODE, + KEY_PC_MODE } } diff --git a/ColorControl/Services/Samsung/SamsungDevice.cs b/ColorControl/Services/Samsung/SamsungDevice.cs index cab3133..9454daf 100644 --- a/ColorControl/Services/Samsung/SamsungDevice.cs +++ b/ColorControl/Services/Samsung/SamsungDevice.cs @@ -39,6 +39,8 @@ namespace ColorControl.Services.Samsung External } + public delegate void GenericDelegate(object sender); + class SamsungDevice { public class InvokableAction @@ -71,9 +73,13 @@ namespace ColorControl.Services.Samsung [JsonIgnore] public bool PoweredOn { get; private set; } + [JsonIgnore] public DateTimeOffset PoweredOffAt { get; private set; } + [JsonIgnore] public PowerOffSource PoweredOffBy { get; internal set; } + public event GenericDelegate Connected; + [JsonIgnore] private SamTvConnection _samTvConnection; @@ -188,6 +194,11 @@ namespace ColorControl.Services.Samsung finally { _connectSemaphore.Release(); + + if (true) + { + Connected?.Invoke(this); + } } } @@ -336,7 +347,7 @@ namespace ColorControl.Services.Samsung var powerState = (string)json.device?.PowerState; - Logger.Error($"Current power state: {powerState}"); + Logger.Debug($"Current power state: {powerState}"); return powerState; } @@ -563,6 +574,21 @@ namespace ColorControl.Services.Samsung return await WakeAndConnectWithRetries(powerOnRetries); } + + internal async Task> GetAppsAsync(bool force) + { + var dynamic = new + { + @event = "ed.installedApp.get", + to = "host", + }; + + var message = new RequestMessage("ms.channel.emit", dynamic); + + var result = await _samTvConnection.SendCommandAsync(message, true); + + return new List(); + } } } diff --git a/ColorControl/Services/Samsung/SamsungPanel.cs b/ColorControl/Services/Samsung/SamsungPanel.cs index 06618c6..2fec244 100644 --- a/ColorControl/Services/Samsung/SamsungPanel.cs +++ b/ColorControl/Services/Samsung/SamsungPanel.cs @@ -364,10 +364,10 @@ namespace ColorControl.Services.Samsung } } - if (cbxSamsungApps.Items.Count == 0 && device != null) - { - //_samsungService.RefreshApps().ContinueWith((task) => BeginInvoke(() => FillLgApps(false))); - } + //if (cbxSamsungApps.Items.Count == 0 && device != null) + //{ + // _samsungService.RefreshAppsAsync().ContinueWith((task) => BeginInvoke(() => FillApps(false))); + //} btnSamsungDeviceConvertToCustom.Enabled = devices.Any(); @@ -402,7 +402,7 @@ namespace ColorControl.Services.Samsung } } - private void FillLgApps(bool forced) + private void FillApps(bool forced) { var lgApps = _samsungService?.GetApps(); if (forced && (lgApps == null || !lgApps.Any())) @@ -410,10 +410,10 @@ namespace ColorControl.Services.Samsung MessageForms.WarningOk("Could not refresh the apps. Check the log for details."); return; } - InitLgApps(); + InitApps(); } - private void InitLgApps() + private void InitApps() { var lgApps = _samsungService?.GetApps(); diff --git a/ColorControl/Services/Samsung/SamsungService.cs b/ColorControl/Services/Samsung/SamsungService.cs index d7b3532..d513651 100644 --- a/ColorControl/Services/Samsung/SamsungService.cs +++ b/ColorControl/Services/Samsung/SamsungService.cs @@ -37,6 +37,7 @@ namespace ColorControl.Services.Samsung private bool _poweredOffByScreenSaver; private int _poweredOffByScreenSaverProcessId; private object _lastTriggeredPreset; + private List _samsungApps = new List(); public List Devices { get; private set; } @@ -184,6 +185,8 @@ namespace ColorControl.Services.Samsung var preferredDevice = Devices.FirstOrDefault(x => x.MacAddress != null && x.MacAddress.Equals(Config.PreferredMacAddress)) ?? Devices[0]; SelectedDevice = preferredDevice; + + SelectedDevice.Connected += SelectedDevice_Connected; } else { @@ -216,6 +219,16 @@ namespace ColorControl.Services.Samsung } } + private async void SelectedDevice_Connected(object sender) + { + if (_samsungApps.Any()) + { + return; + } + + //await RefreshAppsAsync(); + } + protected override List GetDefaultPresets() { return new List(); @@ -430,11 +443,27 @@ namespace ColorControl.Services.Samsung } } - internal List GetApps() + public async Task RefreshAppsAsync(bool force = false) { - return new List(); + if (SelectedDevice == null) + { + Logger.Debug("Cannot refresh apps: no device has been selected"); + return; + } + + var apps = await SelectedDevice.GetAppsAsync(force); + + if (apps.Any()) + { + _samsungApps.Clear(); + _samsungApps.AddRange(apps); + } } + internal List GetApps() + { + return _samsungApps; + } public async Task ProcessChanged(object sender, ProcessChangedEventArgs args, CancellationToken token) { diff --git a/ColorControl/XForms/LogWindow.xaml b/ColorControl/XForms/LogWindow.xaml new file mode 100644 index 0000000..ea2fe17 --- /dev/null +++ b/ColorControl/XForms/LogWindow.xaml @@ -0,0 +1,40 @@ + + + + + Log level: + + Trace + Debug + Info + Warning + Error + Fatal + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ColorControl/XForms/LogWindow.xaml.cs b/ColorControl/XForms/LogWindow.xaml.cs new file mode 100644 index 0000000..a6c3b14 --- /dev/null +++ b/ColorControl/XForms/LogWindow.xaml.cs @@ -0,0 +1,119 @@ +using ColorControl.Common; +using NLog; +using System; +using System.ComponentModel; +using System.IO; +using System.Windows; +using System.Windows.Navigation; + +namespace ColorControl.XForms +{ + public partial class LogWindow : Window + { + private static LogWindow _window; + + private int _logLevelIndex; + + public int LogLevelIndex { get => _logLevelIndex; set => _logLevelIndex = value; } + + public LogWindow() + { + InitializeComponent(); + + LogLevelIndex = LogLevel.FromString(AppContext.CurrentContext.Config.LogLevel).Ordinal; + + DataContext = this; + } + + private void OnRequestNavigate(object sender, RequestNavigateEventArgs e) + { + var processStartInfo = new System.Diagnostics.ProcessStartInfo(e.Uri.AbsoluteUri) + { + UseShellExecute = true, + }; + System.Diagnostics.Process.Start(processStartInfo); + } + + public static void CreateAndShow(bool show = true) + { + if (Application.Current == null) + { + new Application(); + } + + _window ??= new LogWindow(); + + if (show) + { + _window.WindowState = WindowState.Normal; + _window.Show(); + _window.Topmost = true; + _window.Topmost = false; + } + } + + protected override void OnClosing(CancelEventArgs e) + { + e.Cancel = true; + + Hide(); + } + + protected override void OnStateChanged(EventArgs e) + { + if (WindowState == WindowState.Minimized) + { + Hide(); + } + + base.OnStateChanged(e); + } + + private void RawLog_Click(object sender, RoutedEventArgs e) + { + var context = AppContext.CurrentContext; + var logFile = Path.Combine(context.DataPath, "LogFile.txt"); + + Utils.StartProcess(logFile); + } + + private void LoadOlder_Click(object sender, RoutedEventArgs e) + { + var context = AppContext.CurrentContext; + var logFile = Path.Combine(context.DataPath, "LogFile.txt"); + + var lines = Utils.ReadLines(logFile); + + logViewer.LoadLines(lines); + } + + private void Close_Click(object sender, RoutedEventArgs e) + { + Close(); + } + + private void LogLevel_Changed(object sender, System.Windows.Controls.SelectionChangedEventArgs e) + { + var context = AppContext.CurrentContext; + + var logLevel = LogLevel.FromOrdinal(_logLevelIndex); + + context.SetLogLevel(logLevel); + } + + private void Window_Loaded(object sender, RoutedEventArgs e) + { + if (logViewer.HasEvents()) + { + return; + } + + var context = AppContext.CurrentContext; + var logFile = Path.Combine(context.DataPath, "LogFile.txt"); + + var lines = Utils.ReadLines(logFile); + + logViewer.LoadLines(lines, context.StartTime); + } + } +} \ No newline at end of file diff --git a/NLogViewer/ActionCommand.cs b/NLogViewer/ActionCommand.cs new file mode 100644 index 0000000..67db679 --- /dev/null +++ b/NLogViewer/ActionCommand.cs @@ -0,0 +1,42 @@ +using System; +using System.Windows.Input; + +namespace DJ +{ + public sealed class ActionCommand : ICommand + { + private readonly Action _Action; + private readonly Action _ObjectAction; + + public ActionCommand(Action action) + { + _Action = action; + } + + public ActionCommand(Action objectAction) + { + _ObjectAction = objectAction; + } + + private event EventHandler CanExecuteChanged; + + event EventHandler ICommand.CanExecuteChanged + { + add => CanExecuteChanged += value; + remove => CanExecuteChanged -= value; + } + + bool ICommand.CanExecute(object parameter) + { + return true; + } + + public void Execute(object parameter) + { + if (_ObjectAction != null) + _ObjectAction(parameter); + else + _Action(); + } + } +} \ No newline at end of file diff --git a/NLogViewer/Extensions/DependencyObjectExtensions.cs b/NLogViewer/Extensions/DependencyObjectExtensions.cs new file mode 100644 index 0000000..8843d89 --- /dev/null +++ b/NLogViewer/Extensions/DependencyObjectExtensions.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using System.Windows; +using System.Windows.Media; + +namespace DJ.Extensions +{ + public static class DependencyObjectExtensions + { + /// + /// Analyzes both visual and logical tree in order to find all elements of a given + /// type that are descendants of the item. + /// + /// The type of the queried items. + /// The root element that marks the source of the search. If the + /// source is already of the requested type, it will not be included in the result. + /// The UID of the + /// All descendants of that match the requested type. + public static T FindChildByUid(this DependencyObject source, string uid) where T : UIElement + { + if (source != null) + { + var childs = GetChildObjects(source); + foreach (DependencyObject child in childs) + { + //analyze if children match the requested type + if (child != null && child is T dependencyObject && dependencyObject.Uid.Equals(uid)) + { + return dependencyObject; + } + + var descendant = FindChildByUid(child, uid); + if (descendant != null) + return descendant; + } + } + + return null; + } + + /// + /// This method is an alternative to WPF's + /// method, which also + /// supports content elements. Keep in mind that for content elements, + /// this method falls back to the logical tree of the element. + /// + /// The item to be processed. + /// The submitted item's child elements, if available. + public static IEnumerable GetChildObjects(this DependencyObject parent) + { + if (parent == null) yield break; + + if (parent is ContentElement || parent is FrameworkElement) + { + //use the logical tree for content / framework elements + foreach (object obj in LogicalTreeHelper.GetChildren(parent)) + { + var depObj = obj as DependencyObject; + if (depObj != null) yield return (DependencyObject) obj; + } + } + else + { + //use the visual tree per default + int count = VisualTreeHelper.GetChildrenCount(parent); + for (int i = 0; i < count; i++) + { + yield return VisualTreeHelper.GetChild(parent, i); + } + } + } + } +} diff --git a/NLogViewer/Helper/AutoSizedGridView.cs b/NLogViewer/Helper/AutoSizedGridView.cs new file mode 100644 index 0000000..ea2459e --- /dev/null +++ b/NLogViewer/Helper/AutoSizedGridView.cs @@ -0,0 +1,43 @@ +using System; +using System.Reactive.Linq; +using System.Threading; +using System.Windows.Controls; +using NLog; + +namespace DJ.Helper +{ + /// + /// Represents a view mode that displays data items in columns for a System.Windows.Controls.ListView control with auto sized columns based on the column content + /// Used to fix the column width: https://stackoverflow.com/questions/60147905/column-width-adjustment-is-broken-if-using-multibinding-on-displaymemberbinding + /// + public class AutoSizedGridView : GridView + { + private int _MaxLoggerNameLength; + + protected override void PrepareItem(ListViewItem item) + { + if (item.DataContext is LogEventInfo info) + { + if (info.LoggerName.Length > _MaxLoggerNameLength) + { + _MaxLoggerNameLength = info.LoggerName.Length; + Observable.Timer(TimeSpan.FromMilliseconds(1)).ObserveOn(SynchronizationContext.Current).Subscribe(l => + { + foreach (GridViewColumn column in Columns) + { + //setting NaN for the column width automatically determines the required width enough to hold the content completely. + //if column width was set to NaN already, set it ActualWidth temporarily and set to NaN. This raises the property change event and re computes the width. + if (double.IsNaN(column.Width)) + { + column.Width = column.ActualWidth; + column.Width = double.NaN; + } + } + }); + } + } + + base.PrepareItem(item); + } + } +} diff --git a/NLogViewer/Helper/ListViewLayoutManager/ConverterGridViewColumn.cs b/NLogViewer/Helper/ListViewLayoutManager/ConverterGridViewColumn.cs new file mode 100644 index 0000000..54febf0 --- /dev/null +++ b/NLogViewer/Helper/ListViewLayoutManager/ConverterGridViewColumn.cs @@ -0,0 +1,59 @@ +using System; +using System.Globalization; +using System.Windows.Controls; +using System.Windows.Data; + +namespace DJ.Helper.ListViewLayoutManager +{ + public abstract class ConverterGridViewColumn : GridViewColumn, IValueConverter + { + public Type BindingType => _BindingType; + private readonly Type _BindingType; + + // ############################################################################################################################## + // Constructor + // ############################################################################################################################## + + #region Constructor + + protected ConverterGridViewColumn(Type bindingType) + { + if (bindingType == null) + { + throw new ArgumentNullException(nameof(bindingType)); + } + + this._BindingType = bindingType; + + Binding binding = new Binding {Mode = BindingMode.OneWay, Converter = this}; + DisplayMemberBinding = binding; + } + + #endregion + + // ############################################################################################################################## + // IValueConverter + // ############################################################################################################################## + + #region IValueConverter + + object IValueConverter.Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (!_BindingType.IsInstanceOfType(value)) + { + throw new InvalidOperationException(); + } + + return ConvertValue(value); + } + + object IValueConverter.ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + + protected abstract object ConvertValue(object value); + + #endregion + } +} \ No newline at end of file diff --git a/NLogViewer/Helper/ListViewLayoutManager/FixedColumn.cs b/NLogViewer/Helper/ListViewLayoutManager/FixedColumn.cs new file mode 100644 index 0000000..5a64ab5 --- /dev/null +++ b/NLogViewer/Helper/ListViewLayoutManager/FixedColumn.cs @@ -0,0 +1,61 @@ +using System.Windows; +using System.Windows.Controls; + +namespace DJ.Helper.ListViewLayoutManager +{ + public sealed class FixedColumn : LayoutColumn + { + public static bool IsFixedColumn(GridViewColumn column) + { + if (column == null) + { + return false; + } + + return HasPropertyValue(column, WidthProperty); + } + + public static double? GetFixedWidth(GridViewColumn column) + { + return GetColumnWidth(column, WidthProperty); + } + + public static GridViewColumn ApplyWidth(GridViewColumn gridViewColumn, double width) + { + SetWidth(gridViewColumn, width); + return gridViewColumn; + } + + // ############################################################################################################################## + // AttachedProperties + // ############################################################################################################################## + + #region AttachedProperties + + public static double GetWidth(DependencyObject obj) + { + return (double) obj.GetValue(WidthProperty); + } + + public static void SetWidth(DependencyObject obj, double width) + { + obj.SetValue(WidthProperty, width); + } + + public static readonly DependencyProperty WidthProperty = DependencyProperty.RegisterAttached("Width", typeof(double), typeof(FixedColumn)); + + #endregion + + // ############################################################################################################################## + // Constructor + // ############################################################################################################################## + + #region Constructor + + private FixedColumn() + { + } + + #endregion + } +} \ No newline at end of file diff --git a/NLogViewer/Helper/ListViewLayoutManager/ImageGridViewColumn.cs b/NLogViewer/Helper/ListViewLayoutManager/ImageGridViewColumn.cs new file mode 100644 index 0000000..cf81e4a --- /dev/null +++ b/NLogViewer/Helper/ListViewLayoutManager/ImageGridViewColumn.cs @@ -0,0 +1,58 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Media; + +namespace DJ.Helper.ListViewLayoutManager +{ + public abstract class ImageGridViewColumn : GridViewColumn, IValueConverter + { + // ############################################################################################################################## + // Constructor + // ############################################################################################################################## + + #region Constructor + + protected ImageGridViewColumn(Stretch imageStretch) + { + FrameworkElementFactory imageElement = new FrameworkElementFactory(typeof(Image)); + + Binding imageSourceBinding = new Binding {Converter = this, Mode = BindingMode.OneWay}; + imageElement.SetBinding(Image.SourceProperty, imageSourceBinding); + + Binding imageStretchBinding = new Binding {Source = imageStretch}; + imageElement.SetBinding(Image.StretchProperty, imageStretchBinding); + + DataTemplate template = new DataTemplate {VisualTree = imageElement}; + CellTemplate = template; + } + + protected ImageGridViewColumn() : this(Stretch.None) + { + } + + #endregion + + // ############################################################################################################################## + // IValueConverter + // ############################################################################################################################## + + #region IValueConverter + + object IValueConverter.Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return GetImageSource(value); + } + + object IValueConverter.ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + + protected abstract ImageSource GetImageSource(object value); + + #endregion + } +} \ No newline at end of file diff --git a/NLogViewer/Helper/ListViewLayoutManager/LayoutColumn.cs b/NLogViewer/Helper/ListViewLayoutManager/LayoutColumn.cs new file mode 100644 index 0000000..d94d28d --- /dev/null +++ b/NLogViewer/Helper/ListViewLayoutManager/LayoutColumn.cs @@ -0,0 +1,41 @@ +using System; +using System.Windows; +using System.Windows.Controls; + +namespace DJ.Helper.ListViewLayoutManager +{ + public abstract class LayoutColumn + { + protected static bool HasPropertyValue(GridViewColumn column, DependencyProperty dp) + { + if (column == null) + { + throw new ArgumentNullException(nameof(column)); + } + + object value = column.ReadLocalValue(dp); + if (value?.GetType() == dp.PropertyType) + { + return true; + } + + return false; + } + + protected static double? GetColumnWidth(GridViewColumn column, DependencyProperty dp) + { + if (column == null) + { + throw new ArgumentNullException(nameof(column)); + } + + object value = column.ReadLocalValue(dp); + if (value?.GetType() == dp.PropertyType) + { + return (double) value; + } + + return null; + } + } +} diff --git a/NLogViewer/Helper/ListViewLayoutManager/ListViewLayoutManager.cs b/NLogViewer/Helper/ListViewLayoutManager/ListViewLayoutManager.cs new file mode 100644 index 0000000..a850ed9 --- /dev/null +++ b/NLogViewer/Helper/ListViewLayoutManager/ListViewLayoutManager.cs @@ -0,0 +1,623 @@ +using System; +using System.ComponentModel; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Input; +using System.Windows.Media; + +namespace DJ.Helper.ListViewLayoutManager +{ + public class ListViewLayoutManager + { + // ############################################################################################################################## + // Properties + // ############################################################################################################################## + + #region Properties + + // ########################################################################################## + // Public Properties + // ########################################################################################## + + public ListView ListView => _ListView; + + public ScrollBarVisibility VerticalScrollBarVisibility + { + get => _VerticalScrollBarVisibility; + set => _VerticalScrollBarVisibility = value; + } + + // ########################################################################################## + // Private Properties + // ########################################################################################## + + private readonly ListView _ListView; + private ScrollViewer _ScrollViewer; + private bool _Loaded; + private bool _Resizing; + private Cursor _ResizeCursor; + private ScrollBarVisibility _VerticalScrollBarVisibility = ScrollBarVisibility.Auto; + private GridViewColumn _AutoSizedColumn; + + private const double _ZERO_WIDTH_RANGE = 0.1; + + #endregion + + // ############################################################################################################################## + // AttachedProperties + // ############################################################################################################################## + + #region AttachedProperties + + public static void SetEnabled(DependencyObject dependencyObject, bool enabled) + { + dependencyObject.SetValue(EnabledProperty, enabled); + } + + public static readonly DependencyProperty EnabledProperty = DependencyProperty.RegisterAttached("Enabled", + typeof(bool), typeof(ListViewLayoutManager), new FrameworkPropertyMetadata(_OnLayoutManagerEnabledChanged)); + + #endregion + + // ############################################################################################################################## + // Constructor + // ############################################################################################################################## + + #region Constructor + + public ListViewLayoutManager(ListView listView) + { + _ListView = listView ?? throw new ArgumentNullException(nameof(listView)); + _ListView.Loaded += ListView_Loaded; + _ListView.Unloaded += ListView_Unloaded; + } + + private void ListView_Loaded(object sender, RoutedEventArgs e) + { + _RegisterEvents(_ListView); + _InitColumns(); + _DoResizeColumns(); + _Loaded = true; + } + + + private void ListView_Unloaded(object sender, RoutedEventArgs e) + { + if (!_Loaded) + { + return; + } + + _UnRegisterEvents(_ListView); + _Loaded = false; + } + + #endregion + + // ############################################################################################################################## + // public methods + // ############################################################################################################################## + + #region public methods + + public void Refresh() + { + _InitColumns(); + _DoResizeColumns(); + } + + protected virtual void ResizeColumns() + { + GridView view = _ListView.View as GridView; + if (view == null || view.Columns.Count == 0) + { + return; + } + + // listview width + double actualWidth = double.PositiveInfinity; + if (_ScrollViewer != null) + { + actualWidth = _ScrollViewer.ViewportWidth; + } + + if (double.IsInfinity(actualWidth)) + { + actualWidth = _ListView.ActualWidth; + } + + if (double.IsInfinity(actualWidth) || actualWidth <= 0) + { + return; + } + + double resizeableRegionCount = 0; + double otherColumnsWidth = 0; + // determine column sizes + foreach (GridViewColumn gridViewColumn in view.Columns) + { + if (ProportionalColumn.IsProportionalColumn(gridViewColumn)) + { + double? proportionalWidth = ProportionalColumn.GetProportionalWidth(gridViewColumn); + if (proportionalWidth != null) + { + resizeableRegionCount += proportionalWidth.Value; + } + } + else + { + otherColumnsWidth += gridViewColumn.ActualWidth; + } + } + + if (resizeableRegionCount <= 0) + { + // no proportional columns present: commit the regulation to the scroll viewer + if (_ScrollViewer != null) + { + _ScrollViewer.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto; + } + + // search the first fill column + GridViewColumn fillColumn = null; + for (int i = 0; i < view.Columns.Count; i++) + { + GridViewColumn gridViewColumn = view.Columns[i]; + if (_IsFillColumn(gridViewColumn)) + { + fillColumn = gridViewColumn; + break; + } + } + + if (fillColumn != null) + { + double otherColumnsWithoutFillWidth = otherColumnsWidth - fillColumn.ActualWidth; + double fillWidth = actualWidth - otherColumnsWithoutFillWidth; + if (fillWidth > 0) + { + double? minWidth = RangeColumn.GetRangeMinWidth(fillColumn); + double? maxWidth = RangeColumn.GetRangeMaxWidth(fillColumn); + + bool setWidth = !(minWidth.HasValue && fillWidth < minWidth.Value); + if (maxWidth.HasValue && fillWidth > maxWidth.Value) + { + setWidth = false; + } + + if (setWidth) + { + if (_ScrollViewer != null) + { + _ScrollViewer.HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden; + } + + fillColumn.Width = fillWidth; + } + } + } + + return; + } + + double resizeableColumnsWidth = actualWidth - otherColumnsWidth; + if (resizeableColumnsWidth <= 0) + { + return; // missing space + } + + // resize columns + double resizeableRegionWidth = resizeableColumnsWidth / resizeableRegionCount; + foreach (GridViewColumn gridViewColumn in view.Columns) + { + if (ProportionalColumn.IsProportionalColumn(gridViewColumn)) + { + double? proportionalWidth = ProportionalColumn.GetProportionalWidth(gridViewColumn); + if (proportionalWidth != null) + { + gridViewColumn.Width = proportionalWidth.Value * resizeableRegionWidth; + } + } + } + } + + #endregion + + // ############################################################################################################################## + // private methods + // ############################################################################################################################## + + #region private methods + + private void _DoResizeColumns() + { + if (_Resizing) + { + return; + } + + _Resizing = true; + try + { + ResizeColumns(); + } + finally + { + _Resizing = false; + } + } + + private void _RegisterEvents(DependencyObject start) + { + for (int i = 0; i < VisualTreeHelper.GetChildrenCount(start); i++) + { + Visual childVisual = VisualTreeHelper.GetChild(start, i) as Visual; + if (childVisual is Thumb) + { + GridViewColumn gridViewColumn = _FindParentColumn(childVisual); + if (gridViewColumn != null) + { + Thumb thumb = childVisual as Thumb; + if (ProportionalColumn.IsProportionalColumn(gridViewColumn) || + FixedColumn.IsFixedColumn(gridViewColumn) || _IsFillColumn(gridViewColumn)) + { + thumb.IsHitTestVisible = false; + } + else + { + thumb.PreviewMouseMove += _ThumbPreviewMouseMove; + thumb.PreviewMouseLeftButtonDown += + _ThumbPreviewMouseLeftButtonDown; + DependencyPropertyDescriptor.FromProperty( + GridViewColumn.WidthProperty, + typeof(GridViewColumn)).AddValueChanged(gridViewColumn, _GridColumnWidthChanged); + } + } + } + else if (childVisual is GridViewColumnHeader) + { + GridViewColumnHeader columnHeader = childVisual as GridViewColumnHeader; + columnHeader.SizeChanged += _GridColumnHeaderSizeChanged; + } + else if (_ScrollViewer == null && childVisual is ScrollViewer) + { + _ScrollViewer = childVisual as ScrollViewer; + _ScrollViewer.ScrollChanged += _ScrollViewerScrollChanged; + // assume we do the regulation of the horizontal scrollbar + _ScrollViewer.HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden; + _ScrollViewer.VerticalScrollBarVisibility = _VerticalScrollBarVisibility; + } + + _RegisterEvents(childVisual); + } + } + + private void _UnRegisterEvents(DependencyObject start) + { + for (int i = 0; i < VisualTreeHelper.GetChildrenCount(start); i++) + { + Visual childVisual = VisualTreeHelper.GetChild(start, i) as Visual; + if (childVisual is Thumb) + { + GridViewColumn gridViewColumn = _FindParentColumn(childVisual); + if (gridViewColumn != null) + { + Thumb thumb = childVisual as Thumb; + if (ProportionalColumn.IsProportionalColumn(gridViewColumn) || + FixedColumn.IsFixedColumn(gridViewColumn) || _IsFillColumn(gridViewColumn)) + { + thumb.IsHitTestVisible = true; + } + else + { + thumb.PreviewMouseMove -= _ThumbPreviewMouseMove; + thumb.PreviewMouseLeftButtonDown -= + _ThumbPreviewMouseLeftButtonDown; + DependencyPropertyDescriptor.FromProperty( + GridViewColumn.WidthProperty, + typeof(GridViewColumn)).RemoveValueChanged(gridViewColumn, _GridColumnWidthChanged); + } + } + } + else if (childVisual is GridViewColumnHeader) + { + GridViewColumnHeader columnHeader = childVisual as GridViewColumnHeader; + columnHeader.SizeChanged -= _GridColumnHeaderSizeChanged; + } + else if (_ScrollViewer == null && childVisual is ScrollViewer) + { + _ScrollViewer = childVisual as ScrollViewer; + _ScrollViewer.ScrollChanged -= _ScrollViewerScrollChanged; + } + + _UnRegisterEvents(childVisual); + } + } + + private GridViewColumn _FindParentColumn(DependencyObject element) + { + if (element == null) + { + return null; + } + + while (element != null) + { + GridViewColumnHeader gridViewColumnHeader = element as GridViewColumnHeader; + if (gridViewColumnHeader != null) + { + return (gridViewColumnHeader).Column; + } + + element = VisualTreeHelper.GetParent(element); + } + + return null; + } + + private GridViewColumnHeader _FindColumnHeader(DependencyObject start, GridViewColumn gridViewColumn) + { + for (int i = 0; i < VisualTreeHelper.GetChildrenCount(start); i++) + { + Visual childVisual = VisualTreeHelper.GetChild(start, i) as Visual; + if (childVisual is GridViewColumnHeader) + { + GridViewColumnHeader gridViewHeader = childVisual as GridViewColumnHeader; + if (gridViewHeader.Column == gridViewColumn) + { + return gridViewHeader; + } + } + + GridViewColumnHeader childGridViewHeader = _FindColumnHeader(childVisual, gridViewColumn); + if (childGridViewHeader != null) + { + return childGridViewHeader; + } + } + + return null; + } + + private void _InitColumns() + { + GridView view = _ListView.View as GridView; + if (view == null) + { + return; + } + + foreach (GridViewColumn gridViewColumn in view.Columns) + { + if (!RangeColumn.IsRangeColumn(gridViewColumn)) + { + continue; + } + + double? minWidth = RangeColumn.GetRangeMinWidth(gridViewColumn); + double? maxWidth = RangeColumn.GetRangeMaxWidth(gridViewColumn); + if (!minWidth.HasValue && !maxWidth.HasValue) + { + continue; + } + + GridViewColumnHeader columnHeader = _FindColumnHeader(_ListView, gridViewColumn); + if (columnHeader == null) + { + continue; + } + + double actualWidth = columnHeader.ActualWidth; + if (minWidth.HasValue) + { + columnHeader.MinWidth = minWidth.Value; + if (!double.IsInfinity(actualWidth) && actualWidth < columnHeader.MinWidth) + { + gridViewColumn.Width = columnHeader.MinWidth; + } + } + + if (maxWidth.HasValue) + { + columnHeader.MaxWidth = maxWidth.Value; + if (!double.IsInfinity(actualWidth) && actualWidth > columnHeader.MaxWidth) + { + gridViewColumn.Width = columnHeader.MaxWidth; + } + } + } + } + + // returns the delta + private double _SetRangeColumnToBounds(GridViewColumn gridViewColumn) + { + double startWidth = gridViewColumn.Width; + + double? minWidth = RangeColumn.GetRangeMinWidth(gridViewColumn); + double? maxWidth = RangeColumn.GetRangeMaxWidth(gridViewColumn); + + if ((minWidth.HasValue && maxWidth.HasValue) && (minWidth > maxWidth)) + { + return 0; // invalid case + } + + if (minWidth.HasValue && gridViewColumn.Width < minWidth.Value) + { + gridViewColumn.Width = minWidth.Value; + } + else if (maxWidth.HasValue && gridViewColumn.Width > maxWidth.Value) + { + gridViewColumn.Width = maxWidth.Value; + } + + return gridViewColumn.Width - startWidth; + } + + private bool _IsFillColumn(GridViewColumn gridViewColumn) + { + if (gridViewColumn == null) + { + return false; + } + + GridView view = _ListView.View as GridView; + if (view == null || view.Columns.Count == 0) + { + return false; + } + + bool? isFillColumn = RangeColumn.GetRangeIsFillColumn(gridViewColumn); + return isFillColumn.HasValue && isFillColumn.Value; + } + + private void _ThumbPreviewMouseMove(object sender, MouseEventArgs e) + { + Thumb thumb = sender as Thumb; + if (thumb == null) + { + return; + } + + GridViewColumn gridViewColumn = _FindParentColumn(thumb); + if (gridViewColumn == null) + { + return; + } + + // suppress column resizing for proportional, fixed and range fill columns + if (ProportionalColumn.IsProportionalColumn(gridViewColumn) || + FixedColumn.IsFixedColumn(gridViewColumn) || + _IsFillColumn(gridViewColumn)) + { + thumb.Cursor = null; + return; + } + + // check range column bounds + if (thumb.IsMouseCaptured && RangeColumn.IsRangeColumn(gridViewColumn)) + { + double? minWidth = RangeColumn.GetRangeMinWidth(gridViewColumn); + double? maxWidth = RangeColumn.GetRangeMaxWidth(gridViewColumn); + + if ((minWidth.HasValue && maxWidth.HasValue) && (minWidth > maxWidth)) + { + return; // invalid case + } + + if (_ResizeCursor == null) + { + _ResizeCursor = thumb.Cursor; // save the resize cursor + } + + if (minWidth.HasValue && gridViewColumn.Width <= minWidth.Value) + { + thumb.Cursor = Cursors.No; + } + else if (maxWidth.HasValue && gridViewColumn.Width >= maxWidth.Value) + { + thumb.Cursor = Cursors.No; + } + else + { + thumb.Cursor = _ResizeCursor; // between valid min/max + } + } + } + + private void _ThumbPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + Thumb thumb = sender as Thumb; + GridViewColumn gridViewColumn = _FindParentColumn(thumb); + + // suppress column resizing for proportional, fixed and range fill columns + if (ProportionalColumn.IsProportionalColumn(gridViewColumn) || + FixedColumn.IsFixedColumn(gridViewColumn) || + _IsFillColumn(gridViewColumn)) + { + e.Handled = true; + } + } + + private void _GridColumnWidthChanged(object sender, EventArgs e) + { + if (!_Loaded) + { + return; + } + + GridViewColumn gridViewColumn = sender as GridViewColumn; + + // suppress column resizing for proportional and fixed columns + if (ProportionalColumn.IsProportionalColumn(gridViewColumn) || FixedColumn.IsFixedColumn(gridViewColumn)) + { + return; + } + + // ensure range column within the bounds + if (RangeColumn.IsRangeColumn(gridViewColumn)) + { + // special case: auto column width - maybe conflicts with min/max range + if (gridViewColumn != null && gridViewColumn.Width.Equals(double.NaN)) + { + _AutoSizedColumn = gridViewColumn; + return; // handled by the change header size event + } + + // ensure column bounds + if (Math.Abs(_SetRangeColumnToBounds(gridViewColumn) - 0) > _ZERO_WIDTH_RANGE) + { + return; + } + } + + _DoResizeColumns(); + } + + // handle autosized column + private void _GridColumnHeaderSizeChanged(object sender, SizeChangedEventArgs e) + { + if (_AutoSizedColumn == null) + { + return; + } + + GridViewColumnHeader gridViewColumnHeader = sender as GridViewColumnHeader; + if (gridViewColumnHeader != null && gridViewColumnHeader.Column == _AutoSizedColumn) + { + if (gridViewColumnHeader.Width.Equals(double.NaN)) + { + // sync column with + gridViewColumnHeader.Column.Width = gridViewColumnHeader.ActualWidth; + _DoResizeColumns(); + } + + _AutoSizedColumn = null; + } + } + + private void _ScrollViewerScrollChanged(object sender, ScrollChangedEventArgs e) + { + if (_Loaded && Math.Abs(e.ViewportWidthChange - 0) > _ZERO_WIDTH_RANGE) + { + _DoResizeColumns(); + } + } + + private static void _OnLayoutManagerEnabledChanged(DependencyObject dependencyObject, + DependencyPropertyChangedEventArgs e) + { + ListView listView = dependencyObject as ListView; + if (listView != null) + { + bool enabled = (bool) e.NewValue; + if (enabled) + { + new ListViewLayoutManager(listView); + } + } + } + + #endregion + } +} \ No newline at end of file diff --git a/NLogViewer/Helper/ListViewLayoutManager/ProportionalColumn.cs b/NLogViewer/Helper/ListViewLayoutManager/ProportionalColumn.cs new file mode 100644 index 0000000..f43fea7 --- /dev/null +++ b/NLogViewer/Helper/ListViewLayoutManager/ProportionalColumn.cs @@ -0,0 +1,61 @@ +using System.Windows; +using System.Windows.Controls; + +namespace DJ.Helper.ListViewLayoutManager +{ + public sealed class ProportionalColumn : LayoutColumn + { + public static bool IsProportionalColumn(GridViewColumn column) + { + if (column == null) + { + return false; + } + + return HasPropertyValue(column, WidthProperty); + } + + public static double? GetProportionalWidth(GridViewColumn column) + { + return GetColumnWidth(column, WidthProperty); + } + + public static GridViewColumn ApplyWidth(GridViewColumn gridViewColumn, double width) + { + SetWidth(gridViewColumn, width); + return gridViewColumn; + } + + // ############################################################################################################################## + // AttachedProperties + // ############################################################################################################################## + + #region AttachedProperties + + public static double GetWidth(DependencyObject obj) + { + return (double) obj.GetValue(WidthProperty); + } + + public static void SetWidth(DependencyObject obj, double width) + { + obj.SetValue(WidthProperty, width); + } + + public static readonly DependencyProperty WidthProperty = DependencyProperty.RegisterAttached("Width", typeof(double), typeof(ProportionalColumn)); + + #endregion + + // ############################################################################################################################## + // Constructor + // ############################################################################################################################## + + #region Constructor + + private ProportionalColumn() + { + } + + #endregion + } +} \ No newline at end of file diff --git a/NLogViewer/Helper/ListViewLayoutManager/RangeColumn.cs b/NLogViewer/Helper/ListViewLayoutManager/RangeColumn.cs new file mode 100644 index 0000000..66df102 --- /dev/null +++ b/NLogViewer/Helper/ListViewLayoutManager/RangeColumn.cs @@ -0,0 +1,136 @@ +using System; +using System.Windows; +using System.Windows.Controls; + +namespace DJ.Helper.ListViewLayoutManager +{ + + + public sealed class RangeColumn : LayoutColumn + { + + + public static readonly DependencyProperty MinWidthProperty = + DependencyProperty.RegisterAttached( + "MinWidth", + typeof( double ), + typeof( RangeColumn ) ); + + + public static readonly DependencyProperty MaxWidthProperty = + DependencyProperty.RegisterAttached( + "MaxWidth", + typeof( double ), + typeof( RangeColumn ) ); + + + public static readonly DependencyProperty IsFillColumnProperty = + DependencyProperty.RegisterAttached( + "IsFillColumn", + typeof( bool ), + typeof( RangeColumn ) ); + + + private RangeColumn() + { + } // RangeColumn + + + public static double GetMinWidth( DependencyObject obj ) + { + return (double)obj.GetValue( MinWidthProperty ); + } // GetMinWidth + + + public static void SetMinWidth( DependencyObject obj, double minWidth ) + { + obj.SetValue( MinWidthProperty, minWidth ); + } // SetMinWidth + + + public static double GetMaxWidth( DependencyObject obj ) + { + return (double)obj.GetValue( MaxWidthProperty ); + } // GetMaxWidth + + + public static void SetMaxWidth( DependencyObject obj, double maxWidth ) + { + obj.SetValue( MaxWidthProperty, maxWidth ); + } // SetMaxWidth + + + public static bool GetIsFillColumn( DependencyObject obj ) + { + return (bool)obj.GetValue( IsFillColumnProperty ); + } // GetIsFillColumn + + + public static void SetIsFillColumn( DependencyObject obj, bool isFillColumn ) + { + obj.SetValue( IsFillColumnProperty, isFillColumn ); + } // SetIsFillColumn + + + public static bool IsRangeColumn( GridViewColumn column ) + { + if ( column == null ) + { + return false; + } + return + HasPropertyValue( column, MinWidthProperty ) || + HasPropertyValue( column, MaxWidthProperty ) || + HasPropertyValue( column, IsFillColumnProperty ); + } // IsRangeColumn + + + public static double? GetRangeMinWidth( GridViewColumn column ) + { + return GetColumnWidth( column, MinWidthProperty ); + } // GetRangeMinWidth + + + public static double? GetRangeMaxWidth( GridViewColumn column ) + { + return GetColumnWidth( column, MaxWidthProperty ); + } // GetRangeMaxWidth + + + public static bool? GetRangeIsFillColumn( GridViewColumn column ) + { + if ( column == null ) + { + throw new ArgumentNullException( nameof(column) ); + } + object value = column.ReadLocalValue( IsFillColumnProperty ); + if ( value != null && value.GetType() == IsFillColumnProperty.PropertyType ) + { + return (bool)value; + } + + return null; + } // GetRangeIsFillColumn + + + public static GridViewColumn ApplyWidth( GridViewColumn gridViewColumn, double minWidth, + double width, double maxWidth ) + { + return ApplyWidth( gridViewColumn, minWidth, width, maxWidth, false ); + } // ApplyWidth + + + public static GridViewColumn ApplyWidth( GridViewColumn gridViewColumn, double minWidth, + double width, double maxWidth, bool isFillColumn ) + { + SetMinWidth( gridViewColumn, minWidth ); + gridViewColumn.Width = width; + SetMaxWidth( gridViewColumn, maxWidth ); + SetIsFillColumn( gridViewColumn, isFillColumn ); + return gridViewColumn; + } // ApplyWidth + + } // class RangeColumn + +} // namespace Itenso.Windows.Controls.ListViewLayout +// -- EOF ------------------------------------------------------------------- diff --git a/NLogViewer/Helper/PredicateBuilder.cs b/NLogViewer/Helper/PredicateBuilder.cs new file mode 100644 index 0000000..b69f93d --- /dev/null +++ b/NLogViewer/Helper/PredicateBuilder.cs @@ -0,0 +1,28 @@ +using System; +using System.Linq; +using System.Linq.Expressions; + +namespace DJ.Helper +{ + public static class PredicateBuilder + { + public static Expression> True () { return f => true; } + public static Expression> False () { return f => false; } + + public static Expression> Or (this Expression> expr1, + Expression> expr2) + { + var invokedExpr = Expression.Invoke (expr2, expr1.Parameters.Cast ()); + return Expression.Lambda> + (Expression.OrElse (expr1.Body, invokedExpr), expr1.Parameters); + } + + public static Expression> And (this Expression> expr1, + Expression> expr2) + { + var invokedExpr = Expression.Invoke (expr2, expr1.Parameters.Cast ()); + return Expression.Lambda> + (Expression.AndAlso (expr1.Body, invokedExpr), expr1.Parameters); + } + } +} diff --git a/NLogViewer/Images/Glyphs/SortDownArrow.png b/NLogViewer/Images/Glyphs/SortDownArrow.png new file mode 100644 index 0000000..7e5a1a5 Binary files /dev/null and b/NLogViewer/Images/Glyphs/SortDownArrow.png differ diff --git a/NLogViewer/Images/Glyphs/SortUpArrow.png b/NLogViewer/Images/Glyphs/SortUpArrow.png new file mode 100644 index 0000000..3c0cae4 Binary files /dev/null and b/NLogViewer/Images/Glyphs/SortUpArrow.png differ diff --git a/NLogViewer/Images/Small/Add.png b/NLogViewer/Images/Small/Add.png new file mode 100644 index 0000000..a0f4cd9 Binary files /dev/null and b/NLogViewer/Images/Small/Add.png differ diff --git a/NLogViewer/Images/Small/Clear.png b/NLogViewer/Images/Small/Clear.png new file mode 100644 index 0000000..375b8bf Binary files /dev/null and b/NLogViewer/Images/Small/Clear.png differ diff --git a/NLogViewer/Images/Small/Clock.png b/NLogViewer/Images/Small/Clock.png new file mode 100644 index 0000000..1cfffb2 Binary files /dev/null and b/NLogViewer/Images/Small/Clock.png differ diff --git a/NLogViewer/Images/Small/Context.png b/NLogViewer/Images/Small/Context.png new file mode 100644 index 0000000..7d79c0a Binary files /dev/null and b/NLogViewer/Images/Small/Context.png differ diff --git a/NLogViewer/Images/Small/Debug.png b/NLogViewer/Images/Small/Debug.png new file mode 100644 index 0000000..2d5fb90 Binary files /dev/null and b/NLogViewer/Images/Small/Debug.png differ diff --git a/NLogViewer/Images/Small/DebugSource.png b/NLogViewer/Images/Small/DebugSource.png new file mode 100644 index 0000000..2f4fe4f Binary files /dev/null and b/NLogViewer/Images/Small/DebugSource.png differ diff --git a/NLogViewer/Images/Small/Error.png b/NLogViewer/Images/Small/Error.png new file mode 100644 index 0000000..3e5ae9f Binary files /dev/null and b/NLogViewer/Images/Small/Error.png differ diff --git a/NLogViewer/Images/Small/Exception.png b/NLogViewer/Images/Small/Exception.png new file mode 100644 index 0000000..bc2ec84 Binary files /dev/null and b/NLogViewer/Images/Small/Exception.png differ diff --git a/NLogViewer/Images/Small/Exit.png b/NLogViewer/Images/Small/Exit.png new file mode 100644 index 0000000..cf87d2c Binary files /dev/null and b/NLogViewer/Images/Small/Exit.png differ diff --git a/NLogViewer/Images/Small/Export.png b/NLogViewer/Images/Small/Export.png new file mode 100644 index 0000000..35f3c39 Binary files /dev/null and b/NLogViewer/Images/Small/Export.png differ diff --git a/NLogViewer/Images/Small/Fatal.png b/NLogViewer/Images/Small/Fatal.png new file mode 100644 index 0000000..c26cd44 Binary files /dev/null and b/NLogViewer/Images/Small/Fatal.png differ diff --git a/NLogViewer/Images/Small/Info.png b/NLogViewer/Images/Small/Info.png new file mode 100644 index 0000000..12cd1ae Binary files /dev/null and b/NLogViewer/Images/Small/Info.png differ diff --git a/NLogViewer/Images/Small/Layout.png b/NLogViewer/Images/Small/Layout.png new file mode 100644 index 0000000..374cd67 Binary files /dev/null and b/NLogViewer/Images/Small/Layout.png differ diff --git a/NLogViewer/Images/Small/MonoLightning.png b/NLogViewer/Images/Small/MonoLightning.png new file mode 100644 index 0000000..7bf0b77 Binary files /dev/null and b/NLogViewer/Images/Small/MonoLightning.png differ diff --git a/NLogViewer/Images/Small/Network.png b/NLogViewer/Images/Small/Network.png new file mode 100644 index 0000000..9802450 Binary files /dev/null and b/NLogViewer/Images/Small/Network.png differ diff --git a/NLogViewer/Images/Small/Open.png b/NLogViewer/Images/Small/Open.png new file mode 100644 index 0000000..f49c4df Binary files /dev/null and b/NLogViewer/Images/Small/Open.png differ diff --git a/NLogViewer/Images/Small/Pause.png b/NLogViewer/Images/Small/Pause.png new file mode 100644 index 0000000..ebfefa8 Binary files /dev/null and b/NLogViewer/Images/Small/Pause.png differ diff --git a/NLogViewer/Images/Small/Save.png b/NLogViewer/Images/Small/Save.png new file mode 100644 index 0000000..a6cf4c9 Binary files /dev/null and b/NLogViewer/Images/Small/Save.png differ diff --git a/NLogViewer/Images/Small/ScrollDown.png b/NLogViewer/Images/Small/ScrollDown.png new file mode 100644 index 0000000..5891219 Binary files /dev/null and b/NLogViewer/Images/Small/ScrollDown.png differ diff --git a/NLogViewer/Images/Small/Settings.png b/NLogViewer/Images/Small/Settings.png new file mode 100644 index 0000000..a4fd920 Binary files /dev/null and b/NLogViewer/Images/Small/Settings.png differ diff --git a/NLogViewer/Images/Small/Thread.png b/NLogViewer/Images/Small/Thread.png new file mode 100644 index 0000000..22bcf3a Binary files /dev/null and b/NLogViewer/Images/Small/Thread.png differ diff --git a/NLogViewer/Images/Small/Trace.png b/NLogViewer/Images/Small/Trace.png new file mode 100644 index 0000000..330e7f8 Binary files /dev/null and b/NLogViewer/Images/Small/Trace.png differ diff --git a/NLogViewer/Images/Small/Unknown.png b/NLogViewer/Images/Small/Unknown.png new file mode 100644 index 0000000..74f0f57 Binary files /dev/null and b/NLogViewer/Images/Small/Unknown.png differ diff --git a/NLogViewer/Images/Small/Warning.png b/NLogViewer/Images/Small/Warning.png new file mode 100644 index 0000000..7616208 Binary files /dev/null and b/NLogViewer/Images/Small/Warning.png differ diff --git a/NLogViewer/NLogViewer.csproj b/NLogViewer/NLogViewer.csproj new file mode 100644 index 0000000..86ed63e --- /dev/null +++ b/NLogViewer/NLogViewer.csproj @@ -0,0 +1,56 @@ + + + + net7.0-windows10.0.20348.0 + true + DJ + true + Sentinel.NLogViewer + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MSBuild:Compile + + + + diff --git a/NLogViewer/NLogViewer.xaml b/NLogViewer/NLogViewer.xaml new file mode 100644 index 0000000..664f7fc --- /dev/null +++ b/NLogViewer/NLogViewer.xaml @@ -0,0 +1,213 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/NLogViewer/NLogViewer.xaml.cs b/NLogViewer/NLogViewer.xaml.cs new file mode 100644 index 0000000..9c556ce --- /dev/null +++ b/NLogViewer/NLogViewer.xaml.cs @@ -0,0 +1,590 @@ +using DJ.Extensions; +using DJ.Resolver; +using DJ.Targets; +using NLog; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Linq; +using System.Reactive.Concurrency; +using System.Reactive.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Threading; + +namespace DJ +{ + /// + /// Interaktionslogik für NLogViewer.xaml + /// + public partial class NLogViewer : UserControl + { + // ############################################################################################################################## + // Dependency Properties + // ############################################################################################################################## + + #region Dependency Properties + + // ########################################################################################## + // Colors + // ########################################################################################## + + #region Colors + + /// + /// The background for the trace output + /// + [Category("NLogViewerColors")] + public Brush TraceBackground + { + get => (Brush)GetValue(TraceBackgroundProperty); + set => SetValue(TraceBackgroundProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty TraceBackgroundProperty = + DependencyProperty.Register("TraceBackground", typeof(Brush), typeof(NLogViewer), + new PropertyMetadata((Brush)(new BrushConverter().ConvertFrom("#D3D3D3")))); + + /// + /// The foreground for the trace output + /// + [Category("NLogViewerColors")] + public Brush TraceForeground + { + get => (Brush)GetValue(TraceForegroundProperty); + set => SetValue(TraceForegroundProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty TraceForegroundProperty = + DependencyProperty.Register("TraceForeground", typeof(Brush), typeof(NLogViewer), + new PropertyMetadata((Brush)(new BrushConverter().ConvertFrom("#042271")))); + + /// + /// The background for the debug output + /// + [Category("NLogViewerColors")] + public Brush DebugBackground + { + get => (Brush)GetValue(DebugBackgroundProperty); + set => SetValue(DebugBackgroundProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty DebugBackgroundProperty = + DependencyProperty.Register("DebugBackground", typeof(Brush), typeof(NLogViewer), + new PropertyMetadata((Brush)(new BrushConverter().ConvertFrom("#90EE90")))); + + /// + /// The foreground for the debug output + /// + [Category("NLogViewerColors")] + public Brush DebugForeground + { + get => (Brush)GetValue(DebugForegroundProperty); + set => SetValue(DebugForegroundProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty DebugForegroundProperty = + DependencyProperty.Register("DebugForeground", typeof(Brush), typeof(NLogViewer), + new PropertyMetadata((Brush)(new BrushConverter().ConvertFrom("#042271")))); + + /// + /// The background for the info output + /// + [Category("NLogViewerColors")] + public Brush InfoBackground + { + get => (Brush)GetValue(InfoBackgroundProperty); + set => SetValue(InfoBackgroundProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty InfoBackgroundProperty = DependencyProperty.Register("InfoBackground", + typeof(Brush), typeof(NLogViewer), + new PropertyMetadata((Brush)(new BrushConverter().ConvertFrom("#0000FF")))); + + /// + /// The foreground for the info output + /// + [Category("NLogViewerColors")] + public Brush InfoForeground + { + get => (Brush)GetValue(InfoForegroundProperty); + set => SetValue(InfoForegroundProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty InfoForegroundProperty = DependencyProperty.Register("InfoForeground", + typeof(Brush), typeof(NLogViewer), new PropertyMetadata(Brushes.White)); + + /// + /// The background for the warn output + /// + [Category("NLogViewerColors")] + public Brush WarnBackground + { + get => (Brush)GetValue(WarnBackgroundProperty); + set => SetValue(WarnBackgroundProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty WarnBackgroundProperty = DependencyProperty.Register("WarnBackground", + typeof(Brush), typeof(NLogViewer), + new PropertyMetadata((Brush)(new BrushConverter().ConvertFrom("#FFFF00")))); + + /// + /// The foreground for the warn output + /// + [Category("NLogViewerColors")] + public Brush WarnForeground + { + get => (Brush)GetValue(WarnForegroundProperty); + set => SetValue(WarnForegroundProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty WarnForegroundProperty = DependencyProperty.Register("WarnForeground", + typeof(Brush), typeof(NLogViewer), + new PropertyMetadata((Brush)(new BrushConverter().ConvertFrom("#324B5C")))); + + /// + /// The background for the error output + /// + [Category("NLogViewerColors")] + public Brush ErrorBackground + { + get => (Brush)GetValue(ErrorBackgroundProperty); + set => SetValue(ErrorBackgroundProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty ErrorBackgroundProperty = + DependencyProperty.Register("ErrorBackground", typeof(Brush), typeof(NLogViewer), + new PropertyMetadata(Brushes.Red)); + + /// + /// The foreground for the error output + /// + [Category("NLogViewerColors")] + public Brush ErrorForeground + { + get => (Brush)GetValue(ErrorForegroundProperty); + set => SetValue(ErrorForegroundProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty ErrorForegroundProperty = + DependencyProperty.Register("ErrorForeground", typeof(Brush), typeof(NLogViewer), + new PropertyMetadata(Brushes.White)); + + /// + /// The background for the fatal output + /// + [Category("NLogViewerColors")] + public Brush FatalBackground + { + get => (Brush)GetValue(FatalBackgroundProperty); + set => SetValue(FatalBackgroundProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty FatalBackgroundProperty = + DependencyProperty.Register("FatalBackground", typeof(Brush), typeof(NLogViewer), + new PropertyMetadata(Brushes.Black)); + + /// + /// The foreground for the fatal output + /// + [Category("NLogViewerColors")] + public Brush FatalForeground + { + get => (Brush)GetValue(FatalForegroundProperty); + set => SetValue(FatalForegroundProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty FatalForegroundProperty = + DependencyProperty.Register("FatalForeground", typeof(Brush), typeof(NLogViewer), + new PropertyMetadata(Brushes.Yellow)); + + #endregion + + // ########################################################################################## + // NLogViewer + // ########################################################################################## + + #region NLogViewer + + /// + /// Is looking if any target with this name is configured and tries to link it + /// + [Category("NLogViewer")] + public string TargetName + { + get => (string)GetValue(TargetNameProperty); + set => SetValue(TargetNameProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty TargetNameProperty = DependencyProperty.Register("TargetName", typeof(string), typeof(NLogViewer), new PropertyMetadata(null)); + + /// + /// 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("LogEvents", + typeof(CollectionViewSource), typeof(NLogViewer), new PropertyMetadata(null)); + + /// + /// Automatically scroll to the newest entry + /// + [Category("NLogViewer")] + public bool AutoScroll + { + get => (bool)GetValue(AutoScrollProperty); + set => SetValue(AutoScrollProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty AutoScrollProperty = DependencyProperty.Register("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) + ListView?.ScrollToEnd(); + } + + /// + /// Delele all entries + /// + [Category("NLogViewer")] + public ICommand ClearCommand + { + get => (ICommand)GetValue(ClearCommandProperty); + set => SetValue(ClearCommandProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty ClearCommandProperty = DependencyProperty.Register("ClearCommand", + typeof(ICommand), typeof(NLogViewer), new PropertyMetadata(null)); + + /// + /// Stop logging + /// + [Category("NLogViewer")] + public bool Pause + { + get => (bool)GetValue(PauseProperty); + set => SetValue(PauseProperty, value); + } + + /// + /// The DependencyProperty. + /// + public static readonly DependencyProperty PauseProperty = DependencyProperty.Register("Pause", typeof(bool), typeof(NLogViewer), new PropertyMetadata(false)); + + /// + /// 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("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("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("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("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("MessageResolver", typeof(ILogEventInfoResolver), typeof(NLogViewer), new PropertyMetadata(new MessageResolver())); + + #endregion + + + #endregion + + // ############################################################################################################################## + // Properties + // ############################################################################################################################## + + #region Properties + + // ########################################################################################## + // Public Properties + // ########################################################################################## + + + + // ########################################################################################## + // Private Properties + // ########################################################################################## + + private ObservableCollection _LogEventInfos { get; } = new ObservableCollection(); + private IDisposable _Subscription; + private Window _ParentWindow; + + #endregion + + // ############################################################################################################################## + // Constructor + // ############################################################################################################################## + + #region Constructor + + public NLogViewer() + { + InitializeComponent(); + DataContext = this; + + // save instance UID + Uid = GetHashCode().ToString(); + + if (DesignerProperties.GetIsInDesignMode(this)) + return; + + LogEvents = new CollectionViewSource { Source = _LogEventInfos }; + + Loaded += _OnLoaded; + Unloaded += _OnUnloaded; + ClearCommand = new ActionCommand(_LogEventInfos.Clear); + } + + 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() + { + _Subscription?.Dispose(); + } + + private void _OnLoaded(object sender, RoutedEventArgs e) + { + // removed loaded handler to prevent duplicate subscribing + Loaded -= _OnLoaded; + + // add hook to parent window to dispose subscription + // use case: + // NLogViewer is used in a new window inside of a TabControl. If you switch the TabItems, + // the unloaded event is called and would dispose the subscription, even if the control is still alive. + if (Window.GetWindow(this) is { } window) + { + _ParentWindow = window; + _ParentWindow.Closed += _ParentWindowOnClosed; + } + + ListView.ScrollToEnd(); + var target = CacheTarget.GetInstance(targetName: TargetName); + + _Subscription = target.Cache.SubscribeOn(Scheduler.Default).Buffer(TimeSpan.FromMilliseconds(100)).Where(x => x.Any()).ObserveOn(new DispatcherSynchronizationContext(_ParentWindow.Dispatcher)).Subscribe(infos => + { + if (Pause) return; + using (LogEvents.DeferRefresh()) + { + foreach (LogEventInfo info in infos) + { + _LogEventInfos.Add(info); + } + if (MaxCount >= 0 & _LogEventInfos.Count - 100 > MaxCount) + { + for (int i = 0; i < _LogEventInfos.Count - MaxCount; i++) + { + _LogEventInfos.RemoveAt(0); + } + } + } + + if (AutoScroll) + { + ListView?.ScrollToEnd(); + } + }); + } + + public void AddEventInfo(LogEventInfo logEvent) + { + _LogEventInfos.Add(logEvent); + } + + public bool HasEvents() + { + return _LogEventInfos.Any(); + } + + public void LoadLines(List lines, DateTime? fromDateTime = null) + { + _LogEventInfos.Clear(); + + var lastEventInfo = default(LogEventInfo); + + foreach (var line in lines) + { + var parts = line.Split("|"); + + if (parts.Length < 4) + { + if (lastEventInfo != null && !string.IsNullOrWhiteSpace(line)) + { + lastEventInfo.Message += "\r\n" + line; + } + + continue; + } + + var timeStamp = DateTime.Parse(parts[0]); + + if (fromDateTime > timeStamp) + { + continue; + } + + var logLevel = LogLevel.FromString(parts[1]); + + var logEvent = new LogEventInfo(logLevel, parts[2], parts[3]); + logEvent.TimeStamp = timeStamp; + + lastEventInfo = logEvent; + + AddEventInfo(logEvent); + } + } + + #endregion + } +} diff --git a/NLogViewer/Resolver/ILogEventInfoResolver.cs b/NLogViewer/Resolver/ILogEventInfoResolver.cs new file mode 100644 index 0000000..ea0e2f2 --- /dev/null +++ b/NLogViewer/Resolver/ILogEventInfoResolver.cs @@ -0,0 +1,12 @@ +using NLog; + +namespace DJ.Resolver +{ + /// + /// Default interface to resolve a for your personal needs + /// + public interface ILogEventInfoResolver + { + string Resolve(LogEventInfo logEventInfo); + } +} diff --git a/NLogViewer/Resolver/IdResolver.cs b/NLogViewer/Resolver/IdResolver.cs new file mode 100644 index 0000000..e427d29 --- /dev/null +++ b/NLogViewer/Resolver/IdResolver.cs @@ -0,0 +1,12 @@ +using NLog; + +namespace DJ.Resolver +{ + public class IdResolver : ILogEventInfoResolver + { + public string Resolve(LogEventInfo logEventInfo) + { + return logEventInfo.SequenceID.ToString(); + } + } +} \ No newline at end of file diff --git a/NLogViewer/Resolver/LoggerNameResolver.cs b/NLogViewer/Resolver/LoggerNameResolver.cs new file mode 100644 index 0000000..c8773a8 --- /dev/null +++ b/NLogViewer/Resolver/LoggerNameResolver.cs @@ -0,0 +1,12 @@ +using NLog; + +namespace DJ.Resolver +{ + public class LoggerNameResolver : ILogEventInfoResolver + { + public string Resolve(LogEventInfo logEventInfo) + { + return logEventInfo.LoggerName; + } + } +} \ No newline at end of file diff --git a/NLogViewer/Resolver/MessageResolver.cs b/NLogViewer/Resolver/MessageResolver.cs new file mode 100644 index 0000000..e6907e7 --- /dev/null +++ b/NLogViewer/Resolver/MessageResolver.cs @@ -0,0 +1,21 @@ +using System.Text; +using NLog; + +namespace DJ.Resolver +{ + public class MessageResolver : ILogEventInfoResolver + { + public string Resolve(LogEventInfo logEventInfo) + { + StringBuilder builder = new StringBuilder(); + builder.Append(logEventInfo.FormattedMessage); + + if (logEventInfo.Exception != null) + { + builder.AppendLine().Append(logEventInfo.Exception); + } + + return builder.ToString(); + } + } +} \ No newline at end of file diff --git a/NLogViewer/Resolver/TimeStampResolver.cs b/NLogViewer/Resolver/TimeStampResolver.cs new file mode 100644 index 0000000..fc1d9b5 --- /dev/null +++ b/NLogViewer/Resolver/TimeStampResolver.cs @@ -0,0 +1,12 @@ +using NLog; + +namespace DJ.Resolver +{ + public class TimeStampResolver : ILogEventInfoResolver + { + public string Resolve(LogEventInfo logEventInfo) + { + return logEventInfo.TimeStamp.ToString("dd-MM-yyyy HH:mm:ss.fff"); + } + } +} \ No newline at end of file diff --git a/NLogViewer/ScrollingHelper.cs b/NLogViewer/ScrollingHelper.cs new file mode 100644 index 0000000..ee70112 --- /dev/null +++ b/NLogViewer/ScrollingHelper.cs @@ -0,0 +1,44 @@ +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; + +namespace DJ +{ + public static class ScrollingHelper + { + public static void ScrollToEnd(this ListView listView) + { + var scrollViewer = GetDescendantByType(listView, typeof(ScrollViewer)) as ScrollViewer; + scrollViewer?.ScrollToEnd(); + } + + public static Visual GetDescendantByType(Visual element, Type type) + { + if (element != null) + { + if (element.GetType() != type) + { + Visual foundElement = null; + (element as FrameworkElement)?.ApplyTemplate(); + + for (var i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++) + { + var visual = VisualTreeHelper.GetChild(element, i) as Visual; + foundElement = GetDescendantByType(visual, type); + if (foundElement != null) + { + break; + } + } + + return foundElement; + } + + return element; + } + + return null; + } + } +} \ No newline at end of file diff --git a/NLogViewer/Targets/CacheTarget.cs b/NLogViewer/Targets/CacheTarget.cs new file mode 100644 index 0000000..103f1f5 --- /dev/null +++ b/NLogViewer/Targets/CacheTarget.cs @@ -0,0 +1,93 @@ +using System; +using System.Linq; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using DJ.Helper; +using NLog; +using NLog.Config; +using NLog.Targets; + +namespace DJ.Targets +{ + [Target(nameof(CacheTarget))] + public class CacheTarget : Target + { + /// + /// Look in the NLog.config if any target is already defined and returns it, otherwise a new one is registered + /// + /// The maximum entries which should be buffered. Is only used if no target is defined + /// The name of the target you want to link with + /// + public static CacheTarget GetInstance(int defaultMaxCount = 0, string targetName = null) + { + if(LogManager.Configuration == null) + LogManager.Configuration = new LoggingConfiguration(); + + var predicate = PredicateBuilder.True().And(t => t is CacheTarget); + if (!string.IsNullOrEmpty(targetName)) + { + predicate = predicate.And(t => t.Name.Equals(targetName, StringComparison.CurrentCultureIgnoreCase) ||t.Name.Equals($"{targetName}_wrapped", StringComparison.CurrentCultureIgnoreCase)); + } + + var target = (CacheTarget)LogManager.Configuration.AllTargets.FirstOrDefault(predicate.Compile()); + if (target == null) + { + target = new CacheTarget { MaxCount = defaultMaxCount, Name = targetName ?? nameof(CacheTarget)}; + LogManager.Configuration.AddTarget(target.Name, target); + LogManager.Configuration.LoggingRules.Insert(0, new LoggingRule("*", LogLevel.FromString("Trace"), target)); + LogManager.ReconfigExistingLoggers(); + } + return target; + } + + // ############################################################################################################################## + // Properties + // ############################################################################################################################## + + #region Properties + + // ########################################################################################## + // Public Properties + // ########################################################################################## + + /// + /// The maximum amount of entries held in buffer/cache + /// + public int MaxCount { get; set; } = 100; + + public IObservable Cache => _CacheSubject.AsObservable(); + private readonly ReplaySubject _CacheSubject; + + // ########################################################################################## + // Private Properties + // ########################################################################################## + + #endregion + + // ############################################################################################################################## + // Constructor + // ############################################################################################################################## + + #region Constructor + + public CacheTarget() + { + _CacheSubject = new ReplaySubject(MaxCount); + } + + #endregion + + // ############################################################################################################################## + // override + // ############################################################################################################################## + + #region override + + protected override void Write(LogEventInfo logEvent) + { + _CacheSubject.OnNext(logEvent); + } + + #endregion + } +} \ No newline at end of file diff --git a/NLogViewer/XamlMultiValueConverter/ILogEventResolverToStringConverter.cs b/NLogViewer/XamlMultiValueConverter/ILogEventResolverToStringConverter.cs new file mode 100644 index 0000000..e1bf42f --- /dev/null +++ b/NLogViewer/XamlMultiValueConverter/ILogEventResolverToStringConverter.cs @@ -0,0 +1,23 @@ +using System; +using System.Globalization; +using System.Windows.Data; +using DJ.Resolver; +using NLog; + +namespace DJ.XamlMultiValueConverter +{ + public class ILogEventResolverToStringConverter : IMultiValueConverter + { + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + if (values[0] is LogEventInfo logEventInfo && values[1] is ILogEventInfoResolver resolver) + return resolver.Resolve(logEventInfo); + return "####"; + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/NLogViewer/global.json b/NLogViewer/global.json new file mode 100644 index 0000000..f45c840 --- /dev/null +++ b/NLogViewer/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "6.0.101", + "rollForward": "disable" + } +} \ No newline at end of file diff --git a/NLogViewer/nuget.config b/NLogViewer/nuget.config new file mode 100644 index 0000000..029051a --- /dev/null +++ b/NLogViewer/nuget.config @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file