improve samsung controller, add NLogViewer

This commit is contained in:
Vincent Maas committed 2023-06-19 16:29:26 +02:00
1 parent 3d7abd991c
commit 3ef21278cd
65 files changed
+2675 -30

No files matched your search

+10
View File
@@ -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
+12 -1
View File
@@ -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);
}
}
}
+9 -2
View File
@@ -47,6 +47,9 @@
<OutputPath>bin\x64\Release\</OutputPath>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<None Remove="XForms\LogWindow.xaml" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Settings.Designer.cs">
<DesignTimeSharedInput>True</DesignTimeSharedInput>
@@ -93,8 +96,6 @@
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="7.0.0" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="7.0.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NLog" Version="5.1.2" />
<PackageReference Include="NStandard" Version="0.24.0" />
@@ -104,6 +105,12 @@
<PackageReference Include="Vanara.PInvoke.IpHlpApi" Version="3.4.13" />
</ItemGroup>
<ItemGroup>
<Page Include="XForms\LogWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NLogViewer\NLogViewer.csproj" />
<ProjectReference Include="..\novideo_srgb\novideo_srgb.csproj" />
<ProjectReference Include="..\Nspector\Nspector.csproj" />
<ProjectReference Include="..\NvAPIWrapper\NvAPIWrapper.csproj" />
+40
View File
@@ -1117,6 +1117,46 @@ The best and suggested method to provide this is via a Windows Service. Only whe
return null;
}
public static List<string> ReadLines(string fileName, bool reverse = false)
{
var exists = File.Exists(fileName);
if (!exists)
{
return null;
}
try
{
var lines = new List<string>();
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);
+2
View File
@@ -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<Module> Modules { get; set; }
public Config()
{
LogLevel = "DEBUG";
DisplaySettingsDelay = 1000;
ScreenSaverShortcut = string.Empty;
FormWidth = 900;
+24 -10
View File
@@ -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;
}
}
+6
View File
@@ -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();
}
}
}
+11 -6
View File
@@ -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<bool> HandleStartupParams(StartUpParams startUpParams, Process existingProcess)
@@ -244,6 +244,10 @@ namespace ColorControl.Services.Samsung
// taskCompletion.TrySetResult(new { clientKey = key });
//}
}
else
{
}
//else if (_tokens.TryGetValue(id, out taskCompletion))
//{
@@ -37,6 +37,8 @@
KEY_9,
KEY_DTV,
KEY_HDMI,
KEY_CONTENTS
KEY_CONTENTS,
KEY_TV_MODE,
KEY_PC_MODE
}
}
+27 -1
View File
@@ -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<IEnumerable<SamsungApp>> 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<SamsungApp>();
}
}
}
@@ -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();
@@ -37,6 +37,7 @@ namespace ColorControl.Services.Samsung
private bool _poweredOffByScreenSaver;
private int _poweredOffByScreenSaverProcessId;
private object _lastTriggeredPreset;
private List<SamsungApp> _samsungApps = new List<SamsungApp>();
public List<SamsungDevice> 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<SamsungPreset> GetDefaultPresets()
{
return new List<SamsungPreset>();
@@ -430,11 +443,27 @@ namespace ColorControl.Services.Samsung
}
}
internal List<SamsungApp> GetApps()
public async Task RefreshAppsAsync(bool force = false)
{
return new List<SamsungApp>();
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<SamsungApp> GetApps()
{
return _samsungApps;
}
public async Task ProcessChanged(object sender, ProcessChangedEventArgs args, CancellationToken token)
{
+40
View File
@@ -0,0 +1,40 @@
<Window x:Class="ColorControl.XForms.LogWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:dj="clr-namespace:DJ;assembly=NLogViewer"
mc:Ignorable="d"
Title="Logging"
ResizeMode="CanResize"
Width="800"
Height="600"
WindowStartupLocation="CenterOwner"
SnapsToDevicePixels="True"
d:DesignHeight="450" d:DesignWidth="800" Loaded="Window_Loaded">
<DockPanel Margin="6 0 6 6" LastChildFill="True">
<GroupBox DockPanel.Dock="Top" HorizontalAlignment="Stretch" Margin="0,6,0,0" VerticalAlignment="Bottom" Header="Log settings" Padding="6">
<StackPanel Orientation="Horizontal">
<TextBlock>Log level:</TextBlock>
<ComboBox Width="117" Margin="6,0,0,0" SelectedIndex="{Binding LogLevelIndex}" SelectionChanged="LogLevel_Changed">
<ComboBoxItem>Trace</ComboBoxItem>
<ComboBoxItem>Debug</ComboBoxItem>
<ComboBoxItem>Info</ComboBoxItem>
<ComboBoxItem>Warning</ComboBoxItem>
<ComboBoxItem>Error</ComboBoxItem>
<ComboBoxItem>Fatal</ComboBoxItem>
</ComboBox>
</StackPanel>
</GroupBox>
<DockPanel DockPanel.Dock="Bottom" HorizontalAlignment="Stretch" Margin="0,6,0,0" VerticalAlignment="Bottom">
<StackPanel Orientation="Horizontal">
<Button Click="LoadOlder_Click" Content="Load older..." Width="75" Margin="0,0,6,0"/>
<Button Width="75" Click="RawLog_Click" Margin="0,0,6,0">View raw log</Button>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Width="75" Click="Close_Click" IsCancel="True">Close</Button>
</StackPanel>
</DockPanel>
<dj:NLogViewer x:Name="logViewer" />
</DockPanel>
</Window>
+119
View File
@@ -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);
}
}
}
+42
View File
@@ -0,0 +1,42 @@
using System;
using System.Windows.Input;
namespace DJ
{
public sealed class ActionCommand : ICommand
{
private readonly Action _Action;
private readonly Action<object> _ObjectAction;
public ActionCommand(Action action)
{
_Action = action;
}
public ActionCommand(Action<object> 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();
}
}
}
@@ -0,0 +1,72 @@
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;
namespace DJ.Extensions
{
public static class DependencyObjectExtensions
{
/// <summary>
/// Analyzes both visual and logical tree in order to find all elements of a given
/// type that are descendants of the <paramref name="source"/> item.
/// </summary>
/// <typeparam name="T">The type of the queried items.</typeparam>
/// <param name="source">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.</param>
/// <param name="uid">The UID of the <see cref="UIElement"/></param>
/// <returns>All descendants of <paramref name="source"/> that match the requested type.</returns>
public static T FindChildByUid<T>(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<T>(child, uid);
if (descendant != null)
return descendant;
}
}
return null;
}
/// <summary>
/// This method is an alternative to WPF's
/// <see cref="VisualTreeHelper.GetChild"/> method, which also
/// supports content elements. Keep in mind that for content elements,
/// this method falls back to the logical tree of the element.
/// </summary>
/// <param name="parent">The item to be processed.</param>
/// <returns>The submitted item's child elements, if available.</returns>
public static IEnumerable<DependencyObject> 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);
}
}
}
}
}
+43
View File
@@ -0,0 +1,43 @@
using System;
using System.Reactive.Linq;
using System.Threading;
using System.Windows.Controls;
using NLog;
namespace DJ.Helper
{
/// <summary>
/// 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
/// </summary>
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);
}
}
}
@@ -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
}
}
@@ -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
}
}
@@ -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
}
}
@@ -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;
}
}
}
@@ -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
}
}
@@ -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
}
}
@@ -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 -------------------------------------------------------------------
+28
View File
@@ -0,0 +1,28 @@
using System;
using System.Linq;
using System.Linq.Expressions;
namespace DJ.Helper
{
public static class PredicateBuilder
{
public static Expression<Func<T, bool>> True<T> () { return f => true; }
public static Expression<Func<T, bool>> False<T> () { return f => false; }
public static Expression<Func<T, bool>> Or<T> (this Expression<Func<T, bool>> expr1,
Expression<Func<T, bool>> expr2)
{
var invokedExpr = Expression.Invoke (expr2, expr1.Parameters.Cast<Expression> ());
return Expression.Lambda<Func<T, bool>>
(Expression.OrElse (expr1.Body, invokedExpr), expr1.Parameters);
}
public static Expression<Func<T, bool>> And<T> (this Expression<Func<T, bool>> expr1,
Expression<Func<T, bool>> expr2)
{
var invokedExpr = Expression.Invoke (expr2, expr1.Parameters.Cast<Expression> ());
return Expression.Lambda<Func<T, bool>>
(Expression.AndAlso (expr1.Body, invokedExpr), expr1.Parameters);
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 264 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 774 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 782 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 943 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 629 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 623 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 813 B

+56
View File
@@ -0,0 +1,56 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0-windows10.0.20348.0</TargetFramework>
<UseWPF>true</UseWPF>
<RootNamespace>DJ</RootNamespace>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageId>Sentinel.NLogViewer</PackageId>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup>
<ItemGroup>
<None Remove="NLogViewer.xaml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="NLog" Version="5.1.2" />
<PackageReference Include="System.Reactive" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<Resource Include="Images\Glyphs\SortDownArrow.png" />
<Resource Include="Images\Glyphs\SortUpArrow.png" />
<Resource Include="Images\Small\Add.png" />
<Resource Include="Images\Small\Clear.png" />
<Resource Include="Images\Small\Clock.png" />
<Resource Include="Images\Small\Context.png" />
<Resource Include="Images\Small\Debug.png" />
<Resource Include="Images\Small\DebugSource.png" />
<Resource Include="Images\Small\Error.png" />
<Resource Include="Images\Small\Exception.png" />
<Resource Include="Images\Small\Exit.png" />
<Resource Include="Images\Small\Export.png" />
<Resource Include="Images\Small\Fatal.png" />
<Resource Include="Images\Small\Info.png" />
<Resource Include="Images\Small\Layout.png" />
<Resource Include="Images\Small\MonoLightning.png" />
<Resource Include="Images\Small\Network.png" />
<Resource Include="Images\Small\Open.png" />
<Resource Include="Images\Small\Pause.png" />
<Resource Include="Images\Small\Save.png" />
<Resource Include="Images\Small\ScrollDown.png" />
<Resource Include="Images\Small\Settings.png" />
<Resource Include="Images\Small\Thread.png" />
<Resource Include="Images\Small\Trace.png" />
<Resource Include="Images\Small\Unknown.png" />
<Resource Include="Images\Small\Warning.png" />
</ItemGroup>
<ItemGroup>
<None Update="NLogView.xaml">
<Generator>MSBuild:Compile</Generator>
</None>
</ItemGroup>
</Project>
+213
View File
@@ -0,0 +1,213 @@
<UserControl x:Class="DJ.NLogViewer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:dj="clr-namespace:DJ"
xmlns:nLog="clr-namespace:NLog;assembly=NLog"
xmlns:listViewLayoutManager="clr-namespace:DJ.Helper.ListViewLayoutManager"
xmlns:xamlMultiValueConverter="clr-namespace:DJ.XamlMultiValueConverter"
xmlns:helper="clr-namespace:DJ.Helper"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel
Grid.Row="1"
Orientation="Horizontal"
Margin="0,5,0,5">
<ToggleButton
Width="120"
Height="20"
IsChecked="{Binding Path=AutoScroll, Mode=TwoWay}">
<ToggleButton.Content>
<StackPanel Orientation="Horizontal">
<Image Source="Images/Small/ScrollDown.png"
RenderOptions.BitmapScalingMode="HighQuality" Stretch="Uniform" />
<TextBlock Text="Auto-Scroll" VerticalAlignment="Center" Margin="5,0,0,0" />
</StackPanel>
</ToggleButton.Content>
</ToggleButton>
<Button
Margin="5,0,0,0"
Width="120"
Height="20"
Command="{Binding Path=ClearCommand}">
<Button.Content>
<StackPanel Orientation="Horizontal">
<Image Source="Images/Small/Clear.png"
RenderOptions.BitmapScalingMode="HighQuality" Stretch="Uniform" />
<TextBlock Text="Clear" VerticalAlignment="Center" Margin="5,0,0,0" />
</StackPanel>
</Button.Content>
</Button>
<ToggleButton
Margin="5,0,0,0"
Width="120"
Height="20"
IsChecked="{Binding Path=Pause, Mode=TwoWay}">
<ToggleButton.Content>
<StackPanel Orientation="Horizontal">
<Image Source="Images/Small/Pause.png"
RenderOptions.BitmapScalingMode="NearestNeighbor" Stretch="Uniform" />
<TextBlock Text="Pause" VerticalAlignment="Center" Margin="5,0,0,0" />
</StackPanel>
</ToggleButton.Content>
</ToggleButton>
</StackPanel>
<ListView
x:Name="ListView"
Grid.Row="2"
ItemsSource="{Binding Path=LogEvents.View, IsAsync=True}"
BorderThickness="0"
ScrollViewer.CanContentScroll="True"
listViewLayoutManager:ListViewLayoutManager.Enabled="true">
<ListView.Resources>
<xamlMultiValueConverter:ILogEventResolverToStringConverter x:Key="ILogEventResolverToStringConverter"/>
</ListView.Resources>
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="BorderBrush" Value="Black" />
<Setter Property="BorderThickness" Value="1" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Level}"
Value="{x:Static nLog:LogLevel.Trace}">
<Setter Property="Background"
Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=dj:NLogViewer}, Path=TraceBackground}" />
<Setter Property="Foreground"
Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=dj:NLogViewer}, Path=TraceForeground}" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=Level}"
Value="{x:Static nLog:LogLevel.Debug}">
<Setter Property="Foreground"
Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=dj:NLogViewer}, Path=DebugForeground}" />
<Setter Property="Background"
Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=dj:NLogViewer}, Path=DebugBackground}" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=Level}"
Value="{x:Static nLog:LogLevel.Info}">
<Setter Property="Foreground"
Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=dj:NLogViewer}, Path=InfoForeground}" />
<Setter Property="Background"
Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=dj:NLogViewer}, Path=InfoBackground}" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=Level}"
Value="{x:Static nLog:LogLevel.Warn}">
<Setter Property="Foreground"
Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=dj:NLogViewer}, Path=WarnForeground}" />
<Setter Property="Background"
Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=dj:NLogViewer}, Path=WarnBackground}" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=Level}"
Value="{x:Static nLog:LogLevel.Error}">
<Setter Property="Foreground"
Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=dj:NLogViewer}, Path=ErrorForeground}" />
<Setter Property="Background"
Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=dj:NLogViewer}, Path=ErrorBackground}" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=Level}"
Value="{x:Static nLog:LogLevel.Fatal}">
<Setter Property="Foreground"
Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=dj:NLogViewer}, Path=FatalForeground}" />
<Setter Property="Background"
Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=dj:NLogViewer}, Path=FatalBackground}" />
</DataTrigger>
</Style.Triggers>
</Style>
</ListView.ItemContainerStyle>
<ListView.View>
<helper:AutoSizedGridView>
<GridViewColumn Header="ID" Width="40">
<GridViewColumn.DisplayMemberBinding>
<MultiBinding Converter="{StaticResource ILogEventResolverToStringConverter}">
<Binding Path="."/>
<Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=dj:NLogViewer}" Path="IdResolver"/>
</MultiBinding>
</GridViewColumn.DisplayMemberBinding>
</GridViewColumn>
<GridViewColumn Header="Level" Width="Auto">
<GridViewColumn.CellTemplate>
<DataTemplate>
<Image x:Name="Image" RenderOptions.BitmapScalingMode="Linear"
Height="18" VerticalAlignment="Center" HorizontalAlignment="Center" />
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=Level}"
Value="{x:Static nLog:LogLevel.Trace}">
<Setter TargetName="Image" Property="Source"
Value="Images/Small/Trace.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=Level}"
Value="{x:Static nLog:LogLevel.Debug}">
<Setter TargetName="Image" Property="Source"
Value="Images/Small/Debug.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=Level}"
Value="{x:Static nLog:LogLevel.Info}">
<Setter TargetName="Image" Property="Source"
Value="Images/Small/Info.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=Level}"
Value="{x:Static nLog:LogLevel.Warn}">
<Setter TargetName="Image" Property="Source"
Value="Images/Small/Warning.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=Level}"
Value="{x:Static nLog:LogLevel.Error}">
<Setter TargetName="Image" Property="Source"
Value="Images/Small/Error.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=Level}"
Value="{x:Static nLog:LogLevel.Fatal}">
<Setter TargetName="Image" Property="Source"
Value="Images/Small/Fatal.png" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="TimeStamp" Width="Auto">
<GridViewColumn.DisplayMemberBinding>
<MultiBinding Converter="{StaticResource ILogEventResolverToStringConverter}">
<Binding Path="."/>
<Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=dj:NLogViewer}" Path="TimeStampResolver"/>
</MultiBinding>
</GridViewColumn.DisplayMemberBinding>
</GridViewColumn>
<GridViewColumn Header="LoggerName" Width="Auto">
<GridViewColumn.DisplayMemberBinding>
<MultiBinding Converter="{StaticResource ILogEventResolverToStringConverter}">
<Binding Path="."/>
<Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=dj:NLogViewer}" Path="LoggerNameResolver"/>
</MultiBinding>
</GridViewColumn.DisplayMemberBinding>
</GridViewColumn>
<GridViewColumn Header="Message" Width="Auto" listViewLayoutManager:RangeColumn.IsFillColumn="True">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBox
IsReadOnly="True"
Background="Transparent"
BorderThickness="0"
TextWrapping="Wrap"
Foreground="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ListViewItem}, Path=Foreground}">
<TextBox.Text>
<MultiBinding Converter="{StaticResource ILogEventResolverToStringConverter}">
<Binding Path="." Mode="OneWay"/>
<Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=dj:NLogViewer}" Path="MessageResolver"/>
</MultiBinding>
</TextBox.Text>
</TextBox>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</helper:AutoSizedGridView>
</ListView.View>
</ListView>
</Grid>
</UserControl>
+590
View File
@@ -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
{
/// <summary>
/// Interaktionslogik für NLogViewer.xaml
/// </summary>
public partial class NLogViewer : UserControl
{
// ##############################################################################################################################
// Dependency Properties
// ##############################################################################################################################
#region Dependency Properties
// ##########################################################################################
// Colors
// ##########################################################################################
#region Colors
/// <summary>
/// The background for the trace output
/// </summary>
[Category("NLogViewerColors")]
public Brush TraceBackground
{
get => (Brush)GetValue(TraceBackgroundProperty);
set => SetValue(TraceBackgroundProperty, value);
}
/// <summary>
/// The <see cref="TraceBackground"/> DependencyProperty.
/// </summary>
public static readonly DependencyProperty TraceBackgroundProperty =
DependencyProperty.Register("TraceBackground", typeof(Brush), typeof(NLogViewer),
new PropertyMetadata((Brush)(new BrushConverter().ConvertFrom("#D3D3D3"))));
/// <summary>
/// The foreground for the trace output
/// </summary>
[Category("NLogViewerColors")]
public Brush TraceForeground
{
get => (Brush)GetValue(TraceForegroundProperty);
set => SetValue(TraceForegroundProperty, value);
}
/// <summary>
/// The <see cref="TraceForeground"/> DependencyProperty.
/// </summary>
public static readonly DependencyProperty TraceForegroundProperty =
DependencyProperty.Register("TraceForeground", typeof(Brush), typeof(NLogViewer),
new PropertyMetadata((Brush)(new BrushConverter().ConvertFrom("#042271"))));
/// <summary>
/// The background for the debug output
/// </summary>
[Category("NLogViewerColors")]
public Brush DebugBackground
{
get => (Brush)GetValue(DebugBackgroundProperty);
set => SetValue(DebugBackgroundProperty, value);
}
/// <summary>
/// The <see cref="DebugBackground"/> DependencyProperty.
/// </summary>
public static readonly DependencyProperty DebugBackgroundProperty =
DependencyProperty.Register("DebugBackground", typeof(Brush), typeof(NLogViewer),
new PropertyMetadata((Brush)(new BrushConverter().ConvertFrom("#90EE90"))));
/// <summary>
/// The foreground for the debug output
/// </summary>
[Category("NLogViewerColors")]
public Brush DebugForeground
{
get => (Brush)GetValue(DebugForegroundProperty);
set => SetValue(DebugForegroundProperty, value);
}
/// <summary>
/// The <see cref="DebugForeground"/> DependencyProperty.
/// </summary>
public static readonly DependencyProperty DebugForegroundProperty =
DependencyProperty.Register("DebugForeground", typeof(Brush), typeof(NLogViewer),
new PropertyMetadata((Brush)(new BrushConverter().ConvertFrom("#042271"))));
/// <summary>
/// The background for the info output
/// </summary>
[Category("NLogViewerColors")]
public Brush InfoBackground
{
get => (Brush)GetValue(InfoBackgroundProperty);
set => SetValue(InfoBackgroundProperty, value);
}
/// <summary>
/// The <see cref="InfoBackground"/> DependencyProperty.
/// </summary>
public static readonly DependencyProperty InfoBackgroundProperty = DependencyProperty.Register("InfoBackground",
typeof(Brush), typeof(NLogViewer),
new PropertyMetadata((Brush)(new BrushConverter().ConvertFrom("#0000FF"))));
/// <summary>
/// The foreground for the info output
/// </summary>
[Category("NLogViewerColors")]
public Brush InfoForeground
{
get => (Brush)GetValue(InfoForegroundProperty);
set => SetValue(InfoForegroundProperty, value);
}
/// <summary>
/// The <see cref="InfoForeground"/> DependencyProperty.
/// </summary>
public static readonly DependencyProperty InfoForegroundProperty = DependencyProperty.Register("InfoForeground",
typeof(Brush), typeof(NLogViewer), new PropertyMetadata(Brushes.White));
/// <summary>
/// The background for the warn output
/// </summary>
[Category("NLogViewerColors")]
public Brush WarnBackground
{
get => (Brush)GetValue(WarnBackgroundProperty);
set => SetValue(WarnBackgroundProperty, value);
}
/// <summary>
/// The <see cref="WarnBackground"/> DependencyProperty.
/// </summary>
public static readonly DependencyProperty WarnBackgroundProperty = DependencyProperty.Register("WarnBackground",
typeof(Brush), typeof(NLogViewer),
new PropertyMetadata((Brush)(new BrushConverter().ConvertFrom("#FFFF00"))));
/// <summary>
/// The foreground for the warn output
/// </summary>
[Category("NLogViewerColors")]
public Brush WarnForeground
{
get => (Brush)GetValue(WarnForegroundProperty);
set => SetValue(WarnForegroundProperty, value);
}
/// <summary>
/// The <see cref="WarnForeground"/> DependencyProperty.
/// </summary>
public static readonly DependencyProperty WarnForegroundProperty = DependencyProperty.Register("WarnForeground",
typeof(Brush), typeof(NLogViewer),
new PropertyMetadata((Brush)(new BrushConverter().ConvertFrom("#324B5C"))));
/// <summary>
/// The background for the error output
/// </summary>
[Category("NLogViewerColors")]
public Brush ErrorBackground
{
get => (Brush)GetValue(ErrorBackgroundProperty);
set => SetValue(ErrorBackgroundProperty, value);
}
/// <summary>
/// The <see cref="ErrorBackground"/> DependencyProperty.
/// </summary>
public static readonly DependencyProperty ErrorBackgroundProperty =
DependencyProperty.Register("ErrorBackground", typeof(Brush), typeof(NLogViewer),
new PropertyMetadata(Brushes.Red));
/// <summary>
/// The foreground for the error output
/// </summary>
[Category("NLogViewerColors")]
public Brush ErrorForeground
{
get => (Brush)GetValue(ErrorForegroundProperty);
set => SetValue(ErrorForegroundProperty, value);
}
/// <summary>
/// The <see cref="ErrorForeground"/> DependencyProperty.
/// </summary>
public static readonly DependencyProperty ErrorForegroundProperty =
DependencyProperty.Register("ErrorForeground", typeof(Brush), typeof(NLogViewer),
new PropertyMetadata(Brushes.White));
/// <summary>
/// The background for the fatal output
/// </summary>
[Category("NLogViewerColors")]
public Brush FatalBackground
{
get => (Brush)GetValue(FatalBackgroundProperty);
set => SetValue(FatalBackgroundProperty, value);
}
/// <summary>
/// The <see cref="FatalBackground"/> DependencyProperty.
/// </summary>
public static readonly DependencyProperty FatalBackgroundProperty =
DependencyProperty.Register("FatalBackground", typeof(Brush), typeof(NLogViewer),
new PropertyMetadata(Brushes.Black));
/// <summary>
/// The foreground for the fatal output
/// </summary>
[Category("NLogViewerColors")]
public Brush FatalForeground
{
get => (Brush)GetValue(FatalForegroundProperty);
set => SetValue(FatalForegroundProperty, value);
}
/// <summary>
/// The <see cref="FatalForeground"/> DependencyProperty.
/// </summary>
public static readonly DependencyProperty FatalForegroundProperty =
DependencyProperty.Register("FatalForeground", typeof(Brush), typeof(NLogViewer),
new PropertyMetadata(Brushes.Yellow));
#endregion
// ##########################################################################################
// NLogViewer
// ##########################################################################################
#region NLogViewer
/// <summary>
/// Is looking if any target with this name is configured and tries to link it
/// </summary>
[Category("NLogViewer")]
public string TargetName
{
get => (string)GetValue(TargetNameProperty);
set => SetValue(TargetNameProperty, value);
}
/// <summary>
/// The <see cref="TargetName"/> DependencyProperty.
/// </summary>
public static readonly DependencyProperty TargetNameProperty = DependencyProperty.Register("TargetName", typeof(string), typeof(NLogViewer), new PropertyMetadata(null));
/// <summary>
/// Private DP to bind to the gui
/// </summary>
[Category("NLogViewer")]
public CollectionViewSource LogEvents
{
get => (CollectionViewSource)GetValue(LogEventsProperty);
private set => SetValue(LogEventsProperty, value);
}
/// <summary>
/// The <see cref="LogEvents"/> DependencyProperty.
/// </summary>
public static readonly DependencyProperty LogEventsProperty = DependencyProperty.Register("LogEvents",
typeof(CollectionViewSource), typeof(NLogViewer), new PropertyMetadata(null));
/// <summary>
/// Automatically scroll to the newest entry
/// </summary>
[Category("NLogViewer")]
public bool AutoScroll
{
get => (bool)GetValue(AutoScrollProperty);
set => SetValue(AutoScrollProperty, value);
}
/// <summary>
/// The <see cref="AutoScroll"/> DependencyProperty.
/// </summary>
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();
}
/// <summary>
/// Delele all entries
/// </summary>
[Category("NLogViewer")]
public ICommand ClearCommand
{
get => (ICommand)GetValue(ClearCommandProperty);
set => SetValue(ClearCommandProperty, value);
}
/// <summary>
/// The <see cref="ClearCommand"/> DependencyProperty.
/// </summary>
public static readonly DependencyProperty ClearCommandProperty = DependencyProperty.Register("ClearCommand",
typeof(ICommand), typeof(NLogViewer), new PropertyMetadata(null));
/// <summary>
/// Stop logging
/// </summary>
[Category("NLogViewer")]
public bool Pause
{
get => (bool)GetValue(PauseProperty);
set => SetValue(PauseProperty, value);
}
/// <summary>
/// The <see cref="Pause"/> DependencyProperty.
/// </summary>
public static readonly DependencyProperty PauseProperty = DependencyProperty.Register("Pause", typeof(bool), typeof(NLogViewer), new PropertyMetadata(false));
/// <summary>
/// The maximum number of entries before automatic cleaning is performed. There is a hysteresis of 100 entries which must be exceeded.
/// Example: <see cref="MaxCount"/> is '1000'. Then after '1100' entries, everything until '1000' is deleted.
/// If set to '0' or less, it is deactivated
/// </summary>
[Category("NLogViewer")]
public int MaxCount
{
get => (int)GetValue(MaxCountProperty);
set => SetValue(MaxCountProperty, value);
}
/// <summary>
/// The <see cref="MaxCount"/> DependencyProperty.
/// </summary>
public static readonly DependencyProperty MaxCountProperty = DependencyProperty.Register("MaxCount", typeof(int), typeof(NLogViewer), new PropertyMetadata(5000));
#endregion
// ##########################################################################################
// Resolver
// ##########################################################################################
#region Resolver
/// <summary>
/// The <see cref="ILogEventInfoResolver"/> to format the id
/// </summary>
[Category("NLogViewerResolver")]
public ILogEventInfoResolver IdResolver
{
get => (ILogEventInfoResolver)GetValue(IdResolverProperty);
set => SetValue(IdResolverProperty, value);
}
/// <summary>
/// The <see cref="IdResolver"/> DependencyProperty.
/// </summary>
public static readonly DependencyProperty IdResolverProperty = DependencyProperty.Register("IdResolver", typeof(ILogEventInfoResolver), typeof(NLogViewer), new PropertyMetadata(new IdResolver()));
/// <summary>
/// The <see cref="ILogEventInfoResolver"/> to format the timestamp output
/// </summary>
[Category("NLogViewerResolver")]
public ILogEventInfoResolver TimeStampResolver
{
get => (ILogEventInfoResolver)GetValue(TimeStampResolverProperty);
set => SetValue(TimeStampResolverProperty, value);
}
/// <summary>
/// The <see cref="TimeStampResolver"/> DependencyProperty.
/// </summary>
public static readonly DependencyProperty TimeStampResolverProperty = DependencyProperty.Register("TimeStampResolver", typeof(ILogEventInfoResolver), typeof(NLogViewer), new PropertyMetadata(new TimeStampResolver()));
/// <summary>
/// The <see cref="ILogEventInfoResolver"/> to format the loggername
/// </summary>
[Category("NLogViewerResolver")]
public ILogEventInfoResolver LoggerNameResolver
{
get => (ILogEventInfoResolver)GetValue(LoggerNameResolverProperty);
set => SetValue(LoggerNameResolverProperty, value);
}
/// <summary>
/// The <see cref="LoggerNameResolver"/> DependencyProperty.
/// </summary>
public static readonly DependencyProperty LoggerNameResolverProperty = DependencyProperty.Register("LoggerNameResolver", typeof(ILogEventInfoResolver), typeof(NLogViewer), new PropertyMetadata(new LoggerNameResolver()));
/// <summary>
/// The <see cref="ILogEventInfoResolver"/> to format the message
/// </summary>
[Category("NLogViewerResolver")]
public ILogEventInfoResolver MessageResolver
{
get => (ILogEventInfoResolver)GetValue(MessageResolverProperty);
set => SetValue(MessageResolverProperty, value);
}
/// <summary>
/// The <see cref="MessageResolver"/> DependencyProperty.
/// </summary>
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<LogEventInfo> _LogEventInfos { get; } = new ObservableCollection<LogEventInfo>();
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<NLogViewer>(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<string> 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
}
}
@@ -0,0 +1,12 @@
using NLog;
namespace DJ.Resolver
{
/// <summary>
/// Default interface to resolve a <see cref="LogEventInfo"/> for your personal needs
/// </summary>
public interface ILogEventInfoResolver
{
string Resolve(LogEventInfo logEventInfo);
}
}
+12
View File
@@ -0,0 +1,12 @@
using NLog;
namespace DJ.Resolver
{
public class IdResolver : ILogEventInfoResolver
{
public string Resolve(LogEventInfo logEventInfo)
{
return logEventInfo.SequenceID.ToString();
}
}
}
+12
View File
@@ -0,0 +1,12 @@
using NLog;
namespace DJ.Resolver
{
public class LoggerNameResolver : ILogEventInfoResolver
{
public string Resolve(LogEventInfo logEventInfo)
{
return logEventInfo.LoggerName;
}
}
}
+21
View File
@@ -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();
}
}
}
+12
View File
@@ -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");
}
}
}
+44
View File
@@ -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;
}
}
}
+93
View File
@@ -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
{
/// <summary>
/// Look in the NLog.config if any target is already defined and returns it, otherwise a new one is registered
/// </summary>
/// <param name="defaultMaxCount">The maximum entries which should be buffered. Is only used if no target is defined</param>
/// <param name="targetName">The name of the target you want to link with</param>
/// <returns></returns>
public static CacheTarget GetInstance(int defaultMaxCount = 0, string targetName = null)
{
if(LogManager.Configuration == null)
LogManager.Configuration = new LoggingConfiguration();
var predicate = PredicateBuilder.True<Target>().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
// ##########################################################################################
/// <summary>
/// The maximum amount of entries held in buffer/cache
/// </summary>
public int MaxCount { get; set; } = 100;
public IObservable<LogEventInfo> Cache => _CacheSubject.AsObservable();
private readonly ReplaySubject<LogEventInfo> _CacheSubject;
// ##########################################################################################
// Private Properties
// ##########################################################################################
#endregion
// ##############################################################################################################################
// Constructor
// ##############################################################################################################################
#region Constructor
public CacheTarget()
{
_CacheSubject = new ReplaySubject<LogEventInfo>(MaxCount);
}
#endregion
// ##############################################################################################################################
// override
// ##############################################################################################################################
#region override
protected override void Write(LogEventInfo logEvent)
{
_CacheSubject.OnNext(logEvent);
}
#endregion
}
}
@@ -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();
}
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"sdk": {
"version": "6.0.101",
"rollForward": "disable"
}
}
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
</packageSources>
<packageRestore>
<!-- The 'automatic' key is set to True when the "Automatically check for missing packages during
build in Visual Studio" checkbox is set. Clearing the box sets this to False and disables
automatic restore. -->
<add key="automatic" value="True" />
</packageRestore>
</configuration>