From faf065cba1add4dcefd1d8098ecd68db5e4efff9 Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Thu, 28 May 2026 00:05:24 +0200 Subject: [PATCH 1/4] Feature: Migrate ping to live charts + add tooltip to speedtest --- .../LvlChartsDefaultInfo.cs | 12 +- .../Controls/LiveChartsPingTimeTooltip.xaml | 46 ++++++ .../LiveChartsPingTimeTooltip.xaml.cs | 94 ++++++++++++ .../Controls/LiveChartsSpeedTestTooltip.xaml | 38 +++++ .../LiveChartsSpeedTestTooltip.xaml.cs | 67 +++++++++ .../Controls/LvlChartsPingTimeTooltip.xaml | 48 ------ .../Controls/LvlChartsPingTimeTooltip.xaml.cs | 35 ----- .../ViewModels/PingMonitorViewModel.cs | 141 +++++++++++++----- .../ViewModels/SpeedTestWidgetViewModel.cs | 16 +- .../NETworkManager/Views/PingMonitorView.xaml | 37 ++--- .../Views/SpeedTestWidgetView.xaml | 50 +++++-- 11 files changed, 406 insertions(+), 178 deletions(-) create mode 100644 Source/NETworkManager/Controls/LiveChartsPingTimeTooltip.xaml create mode 100644 Source/NETworkManager/Controls/LiveChartsPingTimeTooltip.xaml.cs create mode 100644 Source/NETworkManager/Controls/LiveChartsSpeedTestTooltip.xaml create mode 100644 Source/NETworkManager/Controls/LiveChartsSpeedTestTooltip.xaml.cs delete mode 100644 Source/NETworkManager/Controls/LvlChartsPingTimeTooltip.xaml delete mode 100644 Source/NETworkManager/Controls/LvlChartsPingTimeTooltip.xaml.cs diff --git a/Source/NETworkManager.Utilities/LvlChartsDefaultInfo.cs b/Source/NETworkManager.Utilities/LvlChartsDefaultInfo.cs index 2761265120..974029e9ee 100644 --- a/Source/NETworkManager.Utilities/LvlChartsDefaultInfo.cs +++ b/Source/NETworkManager.Utilities/LvlChartsDefaultInfo.cs @@ -2,14 +2,8 @@ namespace NETworkManager.Utilities; -public class LvlChartsDefaultInfo +public class LvlChartsDefaultInfo(DateTime dateTime, double value) { - public LvlChartsDefaultInfo(DateTime dateTime, double value) - { - DateTime = dateTime; - Value = value; - } - - public DateTime DateTime { get; } - public double Value { get; set; } + public DateTime DateTime { get; } = dateTime; + public double Value { get; } = value; } \ No newline at end of file diff --git a/Source/NETworkManager/Controls/LiveChartsPingTimeTooltip.xaml b/Source/NETworkManager/Controls/LiveChartsPingTimeTooltip.xaml new file mode 100644 index 0000000000..bfe13c1003 --- /dev/null +++ b/Source/NETworkManager/Controls/LiveChartsPingTimeTooltip.xaml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Source/NETworkManager/Controls/LiveChartsPingTimeTooltip.xaml.cs b/Source/NETworkManager/Controls/LiveChartsPingTimeTooltip.xaml.cs new file mode 100644 index 0000000000..8a802b9e48 --- /dev/null +++ b/Source/NETworkManager/Controls/LiveChartsPingTimeTooltip.xaml.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Windows; +using System.Windows.Controls.Primitives; +using System.Windows.Media; +using LiveChartsCore; +using LiveChartsCore.Kernel; +using LiveChartsCore.Kernel.Sketches; +using LiveChartsCore.SkiaSharpView; +using LiveChartsCore.SkiaSharpView.Painting; +using NETworkManager.Utilities; +using SkiaSharp; + +namespace NETworkManager.Controls; + +public partial class LiveChartsPingTimeTooltip : IChartTooltip, INotifyPropertyChanged +{ + private readonly Popup _popup; + + public LiveChartsPingTimeTooltip() + { + InitializeComponent(); + DataContext = this; + _popup = new Popup + { + AllowsTransparency = true, + Placement = PlacementMode.MousePoint, + StaysOpen = true, + Child = this + }; + } + + public event PropertyChangedEventHandler PropertyChanged; + + public string HeaderText + { + get; + private set + { + if (field == value) return; + field = value; + OnPropertyChanged(); + } + } + + public ObservableCollection TooltipEntries { get; } = []; + + public void Show(IEnumerable tooltipPoints, Chart chart) + { + var points = tooltipPoints.ToList(); + if (points.Count == 0) return; + + if (points[0].Context.DataSource is LvlChartsDefaultInfo firstInfo) + HeaderText = DateTimeHelper.DateTimeToTimeString(firstInfo.DateTime); + + TooltipEntries.Clear(); + foreach (var point in points) + { + var value = point.Context.DataSource is LvlChartsDefaultInfo info + ? $"{info.Value} ms" + : "-/-"; + TooltipEntries.Add(new TooltipEntry( + SkColorToBrush(GetSeriesColor(point)), + value, + point.Context.Series.Name ?? string.Empty)); + } + + _popup.PlacementTarget = chart.View as FrameworkElement; + _popup.IsOpen = true; + } + + public void Hide(Chart chart) + { + _popup.IsOpen = false; + } + + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + + private static SKColor GetSeriesColor(ChartPoint point) + { + if (point.Context.Series is LineSeries ls && ls.Stroke is SolidColorPaint paint) + return paint.Color; + return SKColors.Gray; + } + + private static Brush SkColorToBrush(SKColor color) + => new SolidColorBrush(Color.FromArgb(color.Alpha, color.Red, color.Green, color.Blue)); + + public record TooltipEntry(Brush SeriesColor, string Value, string SeriesName); +} diff --git a/Source/NETworkManager/Controls/LiveChartsSpeedTestTooltip.xaml b/Source/NETworkManager/Controls/LiveChartsSpeedTestTooltip.xaml new file mode 100644 index 0000000000..2b7151b0a6 --- /dev/null +++ b/Source/NETworkManager/Controls/LiveChartsSpeedTestTooltip.xaml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Source/NETworkManager/Controls/LiveChartsSpeedTestTooltip.xaml.cs b/Source/NETworkManager/Controls/LiveChartsSpeedTestTooltip.xaml.cs new file mode 100644 index 0000000000..46c5274518 --- /dev/null +++ b/Source/NETworkManager/Controls/LiveChartsSpeedTestTooltip.xaml.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Windows; +using System.Windows.Controls.Primitives; +using System.Windows.Media; +using LiveChartsCore; +using LiveChartsCore.Kernel; +using LiveChartsCore.Kernel.Sketches; +using LiveChartsCore.SkiaSharpView; +using LiveChartsCore.SkiaSharpView.Painting; +using SkiaSharp; + +namespace NETworkManager.Controls; + +public partial class LiveChartsSpeedTestTooltip : IChartTooltip +{ + private readonly Popup _popup; + + public LiveChartsSpeedTestTooltip() + { + InitializeComponent(); + DataContext = this; + _popup = new Popup + { + AllowsTransparency = true, + Placement = PlacementMode.MousePoint, + StaysOpen = true, + Child = this + }; + } + + public ObservableCollection TooltipEntries { get; } = []; + + public void Show(IEnumerable tooltipPoints, Chart chart) + { + var points = tooltipPoints.ToList(); + if (points.Count == 0) return; + + TooltipEntries.Clear(); + foreach (var point in points) + { + var value = point.Context.DataSource is double mbps ? $"{mbps:F1} Mbps" : "-/-"; + TooltipEntries.Add(new TooltipEntry( + SkColorToBrush(GetSeriesColor(point)), + value, + point.Context.Series.Name ?? string.Empty)); + } + + _popup.PlacementTarget = chart.View as FrameworkElement; + _popup.IsOpen = true; + } + + public void Hide(Chart chart) + { + _popup.IsOpen = false; + } + + private static SKColor GetSeriesColor(ChartPoint point) + { + return point.Context.Series is LineSeries { Stroke: SolidColorPaint paint } ? paint.Color : SKColors.Gray; + } + + private static Brush SkColorToBrush(SKColor color) => new SolidColorBrush(Color.FromArgb(color.Alpha, color.Red, color.Green, color.Blue)); + + public record TooltipEntry(Brush SeriesColor, string Value, string SeriesName); +} diff --git a/Source/NETworkManager/Controls/LvlChartsPingTimeTooltip.xaml b/Source/NETworkManager/Controls/LvlChartsPingTimeTooltip.xaml deleted file mode 100644 index 99c1057773..0000000000 --- a/Source/NETworkManager/Controls/LvlChartsPingTimeTooltip.xaml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Source/NETworkManager/Controls/LvlChartsPingTimeTooltip.xaml.cs b/Source/NETworkManager/Controls/LvlChartsPingTimeTooltip.xaml.cs deleted file mode 100644 index a35c43f0b8..0000000000 --- a/Source/NETworkManager/Controls/LvlChartsPingTimeTooltip.xaml.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.ComponentModel; -using System.Runtime.CompilerServices; -using LiveCharts; -using LiveCharts.Wpf; - -namespace NETworkManager.Controls; - -public partial class LvlChartsPingTimeTooltip : IChartTooltip -{ - public LvlChartsPingTimeTooltip() - { - InitializeComponent(); - - DataContext = this; - } - - public event PropertyChangedEventHandler PropertyChanged; - - public TooltipData Data - { - get; - set - { - field = value; - OnPropertyChanged(); - } - } - - public TooltipSelectionMode? SelectionMode { get; set; } - - protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } -} \ No newline at end of file diff --git a/Source/NETworkManager/ViewModels/PingMonitorViewModel.cs b/Source/NETworkManager/ViewModels/PingMonitorViewModel.cs index bbe3c3bdb8..a3318e8e59 100644 --- a/Source/NETworkManager/ViewModels/PingMonitorViewModel.cs +++ b/Source/NETworkManager/ViewModels/PingMonitorViewModel.cs @@ -1,6 +1,10 @@ -using LiveCharts; -using LiveCharts.Configurations; -using LiveCharts.Wpf; +using LiveChartsCore; +using LiveChartsCore.Drawing; +using LiveChartsCore.Kernel; +using LiveChartsCore.Kernel.Sketches; +using LiveChartsCore.SkiaSharpView; +using LiveChartsCore.SkiaSharpView.Painting; +using LiveChartsCore.SkiaSharpView.Painting.Effects; using log4net; using MahApps.Metro.SimpleChildWindow; using NETworkManager.Localization.Resources; @@ -9,9 +13,11 @@ using NETworkManager.Settings; using NETworkManager.Utilities; using NETworkManager.Views; +using SkiaSharp; using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Linq; using System.Net; using System.Net.NetworkInformation; using System.Threading; @@ -60,6 +66,7 @@ public PingMonitorViewModel(Guid hostId, Action removeHostByGuid, private static readonly ILog Log = LogManager.GetLogger(typeof(PingMonitorViewModel)); private CancellationTokenSource _cancellationTokenSource; + private int _maxPingValues; public readonly Guid HostId; private readonly Action _removeHostByGuid; @@ -258,44 +265,86 @@ public long TimeMs } } + private ObservableCollection _pingValues; + /// /// Initializes the time chart configuration. /// private void InitialTimeChart() { - var dayConfig = Mappers.Xy() - .X(dayModel => (double)dayModel.DateTime.Ticks / TimeSpan.FromHours(1).Ticks) - .Y(dayModel => dayModel.Value); + _pingValues = []; - Series = new SeriesCollection(dayConfig) - { - new LineSeries + var chartColor = SKColor.Parse("#1ba1e2"); + + var labelColor = Application.Current.Resources["MahApps.Brushes.Gray5"] is System.Windows.Media.SolidColorBrush gray5 + ? new SKColor(gray5.Color.R, gray5.Color.G, gray5.Color.B, gray5.Color.A) + : new SKColor(0x68, 0x68, 0x68); + + var separatorColor = Application.Current.Resources["MahApps.Brushes.Gray8"] is System.Windows.Media.SolidColorBrush gray8 + ? new SKColor(gray8.Color.R, gray8.Color.G, gray8.Color.B, gray8.Color.A) + : new SKColor(0x80, 0x80, 0x80); + + Series = + [ + new LineSeries { - Title = "Time", - Values = new ChartValues(), - PointGeometry = null + Name = Strings.Time, + Values = _pingValues, + Mapping = (info, _) => double.IsNaN(info.Value) + ? Coordinate.Empty + : new(info.DateTime.Ticks / (double)TimeSpan.FromHours(1).Ticks, info.Value), + GeometrySize = 0, + LineSmoothness = 0.3, + DataPadding = new LvcPoint(0, 0), + Stroke = new SolidColorPaint(chartColor) { StrokeThickness = 1.5f }, + Fill = new SolidColorPaint(chartColor.WithAlpha(0x33)) } - }; + ]; - FormatterDate = value => - DateTimeHelper.DateTimeToTimeString(new DateTime((long)(value * TimeSpan.FromHours(1).Ticks))); - FormatterPingTime = value => $"{value} ms"; + PingXAxes = + [ + new Axis + { + LabelsPaint = null, + SeparatorsPaint = null, + Padding = new Padding(0) + } + ]; + + PingYAxes = + [ + new Axis + { + MinLimit = 0, + MinStep = 25, + ForceStepToMin = true, + Labeler = value => $"{value} ms", + TextSize = 11, + Padding = new Padding(4, 0), + LabelsPaint = new SolidColorPaint(labelColor), + SeparatorsPaint = new SolidColorPaint(separatorColor) + { + StrokeThickness = 1, + PathEffect = new DashEffect([10f, 10f]) + } + } + ]; } /// - /// Gets or sets the formatter for the date axis. + /// Gets or sets the series collection for the chart. /// - public Func FormatterDate { get; set; } + public ISeries[] Series { get; private set; } /// - /// Gets or sets the formatter for the ping time axis. + /// Gets the X-axes configuration for the ping time chart. /// - public Func FormatterPingTime { get; set; } + public ICartesianAxis[] PingXAxes { get; private set; } /// - /// Gets or sets the series collection for the chart. + /// Gets the Y-axes configuration for the ping time chart. /// - public SeriesCollection Series { get; set; } + public ICartesianAxis[] PingYAxes { get; private set; } /// /// Gets the error message if an error occurs. @@ -403,6 +452,9 @@ public void Start() _cancellationTokenSource?.Dispose(); _cancellationTokenSource = new CancellationTokenSource(); + // How many data points fit into the 60-second window at the configured interval. + _maxPingValues = (int)Math.Ceiling(60_000.0 / SettingsManager.Current.PingMonitor_WaitTime); + var ping = new Ping { Timeout = SettingsManager.Current.PingMonitor_Timeout, @@ -434,21 +486,22 @@ public void Stop() /// /// Resets the time chart with empty values. /// + private static readonly double _hourTicks = (double)TimeSpan.FromHours(1).Ticks; + private void ResetTimeChart() { - if (Series == null) + if (_pingValues == null) return; - Series[0].Values.Clear(); - - var currentDateTime = DateTime.Now; - - for (var i = 30; i > 0; i--) - { - var bandwidthInfo = new LvlChartsDefaultInfo(currentDateTime.AddSeconds(-i), double.NaN); + _pingValues.Clear(); + UpdateXAxisWindow(DateTime.Now); + } - Series[0].Values.Add(bandwidthInfo); - } + private void UpdateXAxisWindow(DateTime now) + { + var axis = (Axis)PingXAxes[0]; + axis.MinLimit = now.AddSeconds(-60).Ticks / _hourTicks; + axis.MaxLimit = now.Ticks / _hourTicks; } /// @@ -543,14 +596,26 @@ private void Ping_PingReceived(object sender, PingReceivedArgs e) PacketLoss = Math.Round((double)Lost / Transmitted * 100, 2); TimeMs = e.Args.Time; - // Null exception may occur when the application is closing - Application.Current?.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate + // Null exception may occur when the application is closing + Application.Current?.Dispatcher.BeginInvoke(DispatcherPriority.Normal, () => { - Series[0].Values.Add(timeInfo); + _pingValues.Add(timeInfo); + if (_pingValues.Count > _maxPingValues) + _pingValues.RemoveAt(0); - if (Series[0].Values.Count > 59) - Series[0].Values.RemoveAt(0); - })); + UpdateXAxisWindow(timeInfo.DateTime); + + // Compute step from a 20% padded max, then set MaxLimit = step * 3 so the + // 4th label lands exactly on MaxLimit and is never cut off by rounding. + var maxVal = _pingValues.Where(p => !double.IsNaN(p.Value)).Select(p => p.Value).DefaultIfEmpty(0).Max(); + if (maxVal > 0) + { + var yAxis = (Axis)PingYAxes[0]; + var step = Math.Ceiling(maxVal * 1.2 / 3.0); + yAxis.MinStep = step; + yAxis.MaxLimit = step * 3; + } + }); // Add to history _pingInfoList.Add(e.Args); diff --git a/Source/NETworkManager/ViewModels/SpeedTestWidgetViewModel.cs b/Source/NETworkManager/ViewModels/SpeedTestWidgetViewModel.cs index 7fa4dce898..277706fd4c 100644 --- a/Source/NETworkManager/ViewModels/SpeedTestWidgetViewModel.cs +++ b/Source/NETworkManager/ViewModels/SpeedTestWidgetViewModel.cs @@ -177,15 +177,13 @@ public SpeedTestWidgetViewModel() [ new LineSeries { + Name = Strings.Download, Values = DownloadSamples, - GeometrySize = 3, + GeometrySize = 1.5f, LineSmoothness = 0.3, DataPadding = new LvcPoint(0, 0), Stroke = new SolidColorPaint(downloadColor) { StrokeThickness = 1.5f }, - Fill = new SolidColorPaint(downloadColor.WithAlpha(0x33)), - GeometryStroke = new SolidColorPaint(downloadColor) { StrokeThickness = 1.5f }, - GeometryFill = new SolidColorPaint(downloadColor), - YToolTipLabelFormatter = point => $"{point.Model:F1} Mbps" + Fill = new SolidColorPaint(downloadColor.WithAlpha(0x33)) } ]; @@ -194,15 +192,13 @@ public SpeedTestWidgetViewModel() [ new LineSeries { + Name = Strings.Upload, Values = UploadSamples, - GeometrySize = 3, + GeometrySize = 1.5f, LineSmoothness = 0.3, DataPadding = new LvcPoint(0, 0), Stroke = new SolidColorPaint(uploadColor) { StrokeThickness = 1.5f }, - Fill = new SolidColorPaint(uploadColor.WithAlpha(0x33)), - GeometryStroke = new SolidColorPaint(uploadColor) { StrokeThickness = 1.5f }, - GeometryFill = new SolidColorPaint(uploadColor), - YToolTipLabelFormatter = point => $"{point.Model:F1} Mbps" + Fill = new SolidColorPaint(uploadColor.WithAlpha(0x33)) } ]; } diff --git a/Source/NETworkManager/Views/PingMonitorView.xaml b/Source/NETworkManager/Views/PingMonitorView.xaml index c34ed071e5..c2e6e2686d 100644 --- a/Source/NETworkManager/Views/PingMonitorView.xaml +++ b/Source/NETworkManager/Views/PingMonitorView.xaml @@ -8,7 +8,7 @@ xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks" xmlns:viewModels="clr-namespace:NETworkManager.ViewModels" xmlns:localization="clr-namespace:NETworkManager.Localization.Resources;assembly=NETworkManager.Localization" - xmlns:liveChart="clr-namespace:LiveCharts.Wpf;assembly=LiveCharts.Wpf" + xmlns:lvc="clr-namespace:LiveChartsCore.SkiaSharpView.WPF;assembly=LiveChartsCore.SkiaSharpView.WPF" mc:Ignorable="d" d:DataContext="{d:DesignInstance viewModels:PingMonitorViewModel}"> @@ -207,8 +207,7 @@ - + @@ -223,27 +222,17 @@ - - - - - - - - - - - - - - - - - - - + + + + + diff --git a/Source/NETworkManager/Views/SpeedTestWidgetView.xaml b/Source/NETworkManager/Views/SpeedTestWidgetView.xaml index 250a2313d5..ef82b999e0 100644 --- a/Source/NETworkManager/Views/SpeedTestWidgetView.xaml +++ b/Source/NETworkManager/Views/SpeedTestWidgetView.xaml @@ -6,6 +6,7 @@ xmlns:localization="clr-namespace:NETworkManager.Localization.Resources;assembly=NETworkManager.Localization" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:lvc="clr-namespace:LiveChartsCore.SkiaSharpView.WPF;assembly=LiveChartsCore.SkiaSharpView.WPF" + xmlns:controls="clr-namespace:NETworkManager.Controls" xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:viewModels="clr-namespace:NETworkManager.ViewModels" @@ -233,8 +234,13 @@ Text="{Binding CurrentJitterMs, Converter={StaticResource NullableDoubleToStringConverter}, ConverterParameter='F1|ms', Mode=OneWay}" /> - - + + + + + + + @@ -244,16 +250,28 @@ Style="{StaticResource MessageTextBlock}" Margin="10,0,0,0" /> - - + DrawMarginFrame="{x:Null}" + RenderOptions.BitmapScalingMode="NearestNeighbor"> + + + + + + - - + + + + + + + + @@ -263,13 +281,17 @@ Style="{StaticResource MessageTextBlock}" Margin="10,0,0,0" /> - - + + + + + + From 5d652427c5b9b55ed67145242a57e98135de7d3d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 22:16:47 +0000 Subject: [PATCH 2/4] Guard PingMonitor resource lookups when Application is null --- Source/NETworkManager/ViewModels/PingMonitorViewModel.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/NETworkManager/ViewModels/PingMonitorViewModel.cs b/Source/NETworkManager/ViewModels/PingMonitorViewModel.cs index a3318e8e59..e0ed70c9fe 100644 --- a/Source/NETworkManager/ViewModels/PingMonitorViewModel.cs +++ b/Source/NETworkManager/ViewModels/PingMonitorViewModel.cs @@ -276,11 +276,11 @@ private void InitialTimeChart() var chartColor = SKColor.Parse("#1ba1e2"); - var labelColor = Application.Current.Resources["MahApps.Brushes.Gray5"] is System.Windows.Media.SolidColorBrush gray5 + var labelColor = Application.Current?.TryFindResource("MahApps.Brushes.Gray5") is System.Windows.Media.SolidColorBrush gray5 ? new SKColor(gray5.Color.R, gray5.Color.G, gray5.Color.B, gray5.Color.A) : new SKColor(0x68, 0x68, 0x68); - var separatorColor = Application.Current.Resources["MahApps.Brushes.Gray8"] is System.Windows.Media.SolidColorBrush gray8 + var separatorColor = Application.Current?.TryFindResource("MahApps.Brushes.Gray8") is System.Windows.Media.SolidColorBrush gray8 ? new SKColor(gray8.Color.R, gray8.Color.G, gray8.Color.B, gray8.Color.A) : new SKColor(0x80, 0x80, 0x80); @@ -659,4 +659,4 @@ private void Ping_PingException(object sender, PingExceptionArgs e) } #endregion -} \ No newline at end of file +} From 58c3dd89d2884370dbeee3e7049eabfd44d8d638 Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Thu, 28 May 2026 21:12:05 +0200 Subject: [PATCH 3/4] Feature: Migrate LiveChart to LiveCharts2 --- README.md | 1 + .../LibraryManager.cs | 42 +++++++++---------- .../{LiveCharts.txt => LiveCharts2.txt} | 4 +- .../NETworkManager.Documentation.csproj | 4 +- .../Resources/Strings.resx | 2 +- Website/docs/changelog/next-release.md | 6 +++ 6 files changed, 33 insertions(+), 26 deletions(-) rename Source/NETworkManager.Documentation/Licenses/{LiveCharts.txt => LiveCharts2.txt} (92%) diff --git a/README.md b/README.md index 3357827d79..062e8c98ab 100644 --- a/README.md +++ b/README.md @@ -227,6 +227,7 @@ A huge thank you to our supporters and contributors who make NETworkManager poss - [Dragablz](https://dragablz.net/) - Tearable TabControl for WPF. - [GongSolutions.Wpf.DragDrop](https://github.com/punker76/gong-wpf-dragdrop) - An easy to use drag'n'drop framework for WPF. - [IPNetwork](https://github.com/lduchosal/ipnetwork) - .NET library for complex network, IP, and subnet calculations. + - [LiveCharts2](https://github.com/Live-Charts/LiveCharts2) - Beautiful, interactive charts, maps, and gauges. - [LoadingIndicators.WPF](https://github.com/zeluisping/LoadingIndicators.WPF) - A collection of loading indicators for WPF. - [MahApps.Metro](https://mahapps.com/) - UI toolkit for WPF applications. - [MahApps.Metro.IconPacks](https://github.com/MahApps/MahApps.Metro.IconPacks) - Awesome icon packs for WPF and UWP in one library. diff --git a/Source/NETworkManager.Documentation/LibraryManager.cs b/Source/NETworkManager.Documentation/LibraryManager.cs index 6a6d788c33..5844e094ac 100644 --- a/Source/NETworkManager.Documentation/LibraryManager.cs +++ b/Source/NETworkManager.Documentation/LibraryManager.cs @@ -20,83 +20,83 @@ public static class LibraryManager /// public static List List => [ - new LibraryInfo("#SNMP Library", "https://github.com/lextudio/sharpsnmplib", + new("#SNMP Library", "https://github.com/lextudio/sharpsnmplib", Strings.Library_SharpSNMP_Description, Strings.License_MITLicense, "https://github.com/lextudio/sharpsnmplib/blob/master/LICENSE"), - new LibraryInfo("AirspaceFixer", "https://github.com/chris84948/AirspaceFixer", + new("AirspaceFixer", "https://github.com/chris84948/AirspaceFixer", Strings.Library_AirspaceFixer_Description, Strings.License_MITLicense, "https://github.com/chris84948/AirspaceFixer/blob/master/LICENSE"), - new LibraryInfo("ControlzEx", "https://github.com/ControlzEx/ControlzEx", + new("ControlzEx", "https://github.com/ControlzEx/ControlzEx", Strings.Library_ControlzEx_Description, Strings.License_MITLicense, "https://github.com/ButchersBoy/Dragablz/blob/master/LICENSE"), - new LibraryInfo("DnsClient.NET", "https://github.com/MichaCo/DnsClient.NET", + new("DnsClient.NET", "https://github.com/MichaCo/DnsClient.NET", Strings.Library_DnsClientNET_Description, Strings.License_ApacheLicense2dot0, "https://github.com/MichaCo/DnsClient.NET/blob/dev/LICENSE"), - new LibraryInfo("Dragablz", "https://github.com/ButchersBoy/Dragablz", + new("Dragablz", "https://github.com/ButchersBoy/Dragablz", Strings.Library_Dragablz_Description, Strings.License_MITLicense, "https://github.com/ButchersBoy/Dragablz/blob/master/LICENSE"), - new LibraryInfo("GongSolutions.Wpf.DragDrop", "https://github.com/punker76/gong-wpf-dragdrop", + new("GongSolutions.Wpf.DragDrop", "https://github.com/punker76/gong-wpf-dragdrop", Strings.Library_GongSolutionsWpfDragDrop_Description, Strings.License_BDS3Clause, "https://github.com/punker76/gong-wpf-dragdrop/blob/develop/LICENSE"), - new LibraryInfo("IPNetwork", "https://github.com/lduchosal/ipnetwork", + new("IPNetwork", "https://github.com/lduchosal/ipnetwork", Strings.Library_IPNetwork_Description, Strings.License_BDS2Clause, "https://github.com/lduchosal/ipnetwork/blob/master/LICENSE"), - new LibraryInfo("LiveCharts", "https://github.com/Live-Charts/Live-Charts", + new("LiveCharts2", "https://github.com/Live-Charts/LiveCharts2", Strings.Library_LiveCharts_Description, Strings.License_MITLicense, - "https://github.com/Live-Charts/Live-Charts/blob/master/LICENSE.TXT"), - new LibraryInfo("LoadingIndicators.WPF", "https://github.com/zeluisping/LoadingIndicators.WPF", + "https://github.com/Live-Charts/LiveCharts2/blob/master/LICENSE"), + new("LoadingIndicators.WPF", "https://github.com/zeluisping/LoadingIndicators.WPF", Strings.Library_LoadingIndicatorsWPF_Description, Strings.License_Unlicense, "https://github.com/zeluisping/LoadingIndicators.WPF/blob/master/LICENSE"), - new LibraryInfo("log4net", "https://logging.apache.org/log4net/", + new("log4net", "https://logging.apache.org/log4net/", Strings.Library_log4net_Description, Strings.License_ApacheLicense2dot0, "https://github.com/apache/logging-log4net/blob/master/LICENSE"), - new LibraryInfo("MahApps.Metro", "https://github.com/mahapps/mahapps.metro", + new("MahApps.Metro", "https://github.com/mahapps/mahapps.metro", Strings.Library_MahAppsMetro_Description, Strings.License_MITLicense, "https://github.com/MahApps/MahApps.Metro/blob/master/LICENSE"), - new LibraryInfo("MahApps.Metro.IconPacks", "https://github.com/MahApps/MahApps.Metro.IconPacks", + new("MahApps.Metro.IconPacks", "https://github.com/MahApps/MahApps.Metro.IconPacks", Strings.Library_MahAppsMetroIconPacks_Description, Strings.License_MITLicense, "https://github.com/MahApps/MahApps.Metro.IconPacks/blob/master/LICENSE"), - new LibraryInfo("Microsoft.PowerShell.SDK", "https://github.com/PowerShell/PowerShell", + new("Microsoft.PowerShell.SDK", "https://github.com/PowerShell/PowerShell", Strings.Library_PowerShellSDK_Description, Strings.License_MITLicense, "https://github.com/PowerShell/PowerShell/blob/master/LICENSE.txt"), - new LibraryInfo("Microsoft.Web.WebView2", "https://docs.microsoft.com/en-us/microsoft-edge/webview2/", + new("Microsoft.Web.WebView2", "https://docs.microsoft.com/en-us/microsoft-edge/webview2/", Strings.Library_WebView2_Description, Strings.License_MicrosoftWebView2License, "https://www.nuget.org/packages/Microsoft.Web.WebView2/1.0.824-prerelease/License"), - new LibraryInfo("Microsoft.Windows.CsWinRT", "https://github.com/microsoft/cswinrt/tree/master/", + new("Microsoft.Windows.CsWinRT", "https://github.com/microsoft/cswinrt/tree/master/", Strings.Library_CsWinRT_Description, Strings.License_MITLicense, "https://github.com/microsoft/CsWinRT/blob/master/LICENSE"), - new LibraryInfo("Microsoft.Xaml.Behaviors.Wpf", "https://github.com/microsoft/XamlBehaviorsWpf", + new("Microsoft.Xaml.Behaviors.Wpf", "https://github.com/microsoft/XamlBehaviorsWpf", Strings.Library_XamlBehaviorsWpf_Description, Strings.License_MITLicense, "https://github.com/microsoft/XamlBehaviorsWpf/blob/master/LICENSE"), - new LibraryInfo("Newtonsoft.Json", "https://github.com/JamesNK/Newtonsoft.Json", + new("Newtonsoft.Json", "https://github.com/JamesNK/Newtonsoft.Json", Strings.Library_NewtonsoftJson_Description, Strings.License_MITLicense, "https://github.com/JamesNK/Newtonsoft.Json/blob/master/LICENSE.md"), - new LibraryInfo("nulastudio.NetBeauty", "https://github.com/nulastudio/NetBeauty2", + new("nulastudio.NetBeauty", "https://github.com/nulastudio/NetBeauty2", Strings.Library_nulastudioNetBeauty_Description, Strings.License_MITLicense, "https://github.com/nulastudio/NetBeauty2/blob/master/LICENSE"), - new LibraryInfo("Octokit", "https://github.com/octokit/octokit.net", + new("Octokit", "https://github.com/octokit/octokit.net", Strings.Library_Octokit_Description, Strings.License_MITLicense, "https://github.com/octokit/octokit.net/blob/master/LICENSE.txt"), - new LibraryInfo("PSDiscoveryProtocol", "https://github.com/lahell/PSDiscoveryProtocol", + new("PSDiscoveryProtocol", "https://github.com/lahell/PSDiscoveryProtocol", Strings.Library_PSDicoveryProtocol_Description, Strings.License_MITLicense, "https://github.com/lahell/PSDiscoveryProtocol/blob/master/LICENSE") diff --git a/Source/NETworkManager.Documentation/Licenses/LiveCharts.txt b/Source/NETworkManager.Documentation/Licenses/LiveCharts2.txt similarity index 92% rename from Source/NETworkManager.Documentation/Licenses/LiveCharts.txt rename to Source/NETworkManager.Documentation/Licenses/LiveCharts2.txt index 65c8efe372..bf5cc2e5ea 100644 --- a/Source/NETworkManager.Documentation/Licenses/LiveCharts.txt +++ b/Source/NETworkManager.Documentation/Licenses/LiveCharts2.txt @@ -1,6 +1,6 @@ -The MIT License (MIT) +MIT License -Copyright (c) 2016 Alberto Rodriguez & LiveCharts contributors +Copyright (c) 2021 Alberto Rodriguez Orozco Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Source/NETworkManager.Documentation/NETworkManager.Documentation.csproj b/Source/NETworkManager.Documentation/NETworkManager.Documentation.csproj index 996f839fac..029e38d24c 100644 --- a/Source/NETworkManager.Documentation/NETworkManager.Documentation.csproj +++ b/Source/NETworkManager.Documentation/NETworkManager.Documentation.csproj @@ -20,7 +20,7 @@ - + @@ -61,7 +61,7 @@ PreserveNewest - + PreserveNewest diff --git a/Source/NETworkManager.Localization/Resources/Strings.resx b/Source/NETworkManager.Localization/Resources/Strings.resx index 9e99aee06d..4cb9414464 100644 --- a/Source/NETworkManager.Localization/Resources/Strings.resx +++ b/Source/NETworkManager.Localization/Resources/Strings.resx @@ -2845,7 +2845,7 @@ Error message: C# library take care of complex network, IP, IPv4, IPv6, netmask, CIDR, subnet, subnetting, supernet, and supernetting calculation for .NET developers. - Simple, flexible, interactive & powerful charts, maps and gauges for .Net + Beautiful, interactive charts, maps, and gauges. A collection of loading indicators for WPF diff --git a/Website/docs/changelog/next-release.md b/Website/docs/changelog/next-release.md index 28d1b77eef..d384d45df3 100644 --- a/Website/docs/changelog/next-release.md +++ b/Website/docs/changelog/next-release.md @@ -68,12 +68,17 @@ Release date: **xx.xx.2025** **Dashboard** - Added a **Refresh** button (with animated feedback) to the **Network Connection**, **IP Geolocation**, and **DNS Resolver** widgets. The global reload button in the Status Window has been removed, as each widget now has its own. [#3447](https://github.com/BornToBeRoot/NETworkManager/pull/3447) +- Added a tooltip to the **Speed Test** widget chart showing download/upload speed values on hover. [#3449](https://github.com/BornToBeRoot/NETworkManager/pull/3449) - Redesign Status Window to make it more compact. [#3359](https://github.com/BornToBeRoot/NETworkManager/pull/3359) **Network Interface** - Added Network Profile (domain, private, public) information to the Network Interface details view, if available. [#3383](https://github.com/BornToBeRoot/NETworkManager/pull/3383) +**Ping Monitor** + +- Migrated charts from LiveCharts to LiveCharts2. Added a tooltip showing the ping time on hover. [#3449](https://github.com/BornToBeRoot/NETworkManager/pull/3449) + **Discovery Protocol** - Added support for `F5` and `Enter` keys to start capturing network packets. [#3383](https://github.com/BornToBeRoot/NETworkManager/pull/3383) @@ -111,6 +116,7 @@ Release date: **xx.xx.2025** ## Dependencies, Refactoring & Documentation +- Migrated from `LiveCharts` to `LiveCharts2` (`LiveChartsCore.SkiaSharpView.WPF`) for chart rendering. [#3449](https://github.com/BornToBeRoot/NETworkManager/pull/3449) - Fixed `CancellationTokenSource` leak in `IPScanner`, `PortScanner`, `Traceroute`, `PingMonitor`, `PingMonitorHost` and `SNMP` ViewModels. The previous instance was never disposed before being overwritten on each run, leaking the underlying `WaitHandle`. [#3448](https://github.com/BornToBeRoot/NETworkManager/pull/3448) - Replace fire-and-forget `.ConfigureAwait(false)` calls with explicit discard assignments (`_ = SomeAsyncOperation()`) across command handlers, startup/load paths and profile callbacks. [#3441](https://github.com/BornToBeRoot/NETworkManager/pull/3441) - Code cleanup & refactoring From 835011cd2f4a850777837b7cb67f0242308a4293 Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Thu, 28 May 2026 21:31:27 +0200 Subject: [PATCH 4/4] Fix: Based on Claude feedback --- .../NETworkManager/ViewModels/PingMonitorViewModel.cs | 10 ++++++++-- Source/NETworkManager/Views/SpeedTestWidgetView.xaml | 3 ++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Source/NETworkManager/ViewModels/PingMonitorViewModel.cs b/Source/NETworkManager/ViewModels/PingMonitorViewModel.cs index e0ed70c9fe..ada5083017 100644 --- a/Source/NETworkManager/ViewModels/PingMonitorViewModel.cs +++ b/Source/NETworkManager/ViewModels/PingMonitorViewModel.cs @@ -67,6 +67,7 @@ public PingMonitorViewModel(Guid hostId, Action removeHostByGuid, private CancellationTokenSource _cancellationTokenSource; private int _maxPingValues; + private int _sessionId; public readonly Guid HostId; private readonly Action _removeHostByGuid; @@ -339,12 +340,12 @@ private void InitialTimeChart() /// /// Gets the X-axes configuration for the ping time chart. /// - public ICartesianAxis[] PingXAxes { get; private set; } + public Axis[] PingXAxes { get; private set; } /// /// Gets the Y-axes configuration for the ping time chart. /// - public ICartesianAxis[] PingYAxes { get; private set; } + public Axis[] PingYAxes { get; private set; } /// /// Gets the error message if an error occurs. @@ -447,6 +448,7 @@ public void Start() PacketLoss = 0; // Reset chart + _sessionId++; ResetTimeChart(); _cancellationTokenSource?.Dispose(); @@ -597,8 +599,12 @@ private void Ping_PingReceived(object sender, PingReceivedArgs e) TimeMs = e.Args.Time; // Null exception may occur when the application is closing + var session = _sessionId; Application.Current?.Dispatcher.BeginInvoke(DispatcherPriority.Normal, () => { + if (_sessionId != session) + return; + _pingValues.Add(timeInfo); if (_pingValues.Count > _maxPingValues) _pingValues.RemoveAt(0); diff --git a/Source/NETworkManager/Views/SpeedTestWidgetView.xaml b/Source/NETworkManager/Views/SpeedTestWidgetView.xaml index ef82b999e0..0760fc8ebc 100644 --- a/Source/NETworkManager/Views/SpeedTestWidgetView.xaml +++ b/Source/NETworkManager/Views/SpeedTestWidgetView.xaml @@ -286,7 +286,8 @@ Series="{Binding UploadSeries}" XAxes="{Binding UploadXAxes}" YAxes="{Binding UploadYAxes}" - DrawMarginFrame="{x:Null}"> + DrawMarginFrame="{x:Null}" + RenderOptions.BitmapScalingMode="NearestNeighbor">