Skip to content

Commit e6ef255

Browse files
committed
Fix: Copilot feedback
1 parent 8c6e3a9 commit e6ef255

14 files changed

Lines changed: 201 additions & 66 deletions

File tree

‎.github/dependabot.yml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,9 @@ updates:
2525
target-branch: main
2626
commit-message:
2727
prefix: "Dependency (dependabot/npm):"
28+
groups:
29+
# react and react-dom must have the exact same version (otherwise the website build fails)
30+
react:
31+
patterns:
32+
- "react"
33+
- "react-dom"
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
using System;
2+
using System.Windows;
3+
using System.Windows.Controls;
4+
using System.Windows.Threading;
5+
6+
namespace NETworkManager.Controls;
7+
8+
/// <summary>
9+
/// Attached property that keeps the text of an editable <see cref="ComboBox"/> that shows a history (e.g. the hosts of
10+
/// the Traceroute) when its selected entry is removed from the history.
11+
///
12+
/// The history is changed in place (<c>ListHelper.Modify</c>): the oldest entries are removed and, if the number of
13+
/// stored entries is 0, all of them. If the removed entry is the selected item (e.g. the host that was just used, or the
14+
/// host of another tab that shares the history), WPF clears the text of the ComboBox. The text is restored after the
15+
/// ComboBox has updated it. If the entry is only deselected because another text was typed, it is still in the list and
16+
/// nothing is changed.
17+
/// </summary>
18+
public static class ComboBoxHistoryBehavior
19+
{
20+
public static readonly DependencyProperty KeepTextWhenSelectedItemIsRemovedProperty =
21+
DependencyProperty.RegisterAttached(
22+
"KeepTextWhenSelectedItemIsRemoved",
23+
typeof(bool),
24+
typeof(ComboBoxHistoryBehavior),
25+
new PropertyMetadata(false, OnKeepTextWhenSelectedItemIsRemovedChanged));
26+
27+
public static void SetKeepTextWhenSelectedItemIsRemoved(UIElement element, bool value) =>
28+
element.SetValue(KeepTextWhenSelectedItemIsRemovedProperty, value);
29+
30+
public static bool GetKeepTextWhenSelectedItemIsRemoved(UIElement element) =>
31+
(bool)element.GetValue(KeepTextWhenSelectedItemIsRemovedProperty);
32+
33+
private static void OnKeepTextWhenSelectedItemIsRemovedChanged(DependencyObject d,
34+
DependencyPropertyChangedEventArgs e)
35+
{
36+
if (d is not ComboBox comboBox)
37+
return;
38+
39+
if ((bool)e.NewValue)
40+
comboBox.SelectionChanged += ComboBox_SelectionChanged;
41+
else
42+
comboBox.SelectionChanged -= ComboBox_SelectionChanged;
43+
}
44+
45+
private static void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
46+
{
47+
if (sender is not ComboBox { IsEditable: true } comboBox || comboBox.SelectedItem != null ||
48+
e.RemovedItems.Count == 0)
49+
return;
50+
51+
var removedItem = e.RemovedItems[0];
52+
53+
// Deselected because another text was typed: the entry is still in the list.
54+
if (comboBox.Items.Contains(removedItem))
55+
return;
56+
57+
var text = removedItem?.ToString();
58+
59+
if (string.IsNullOrEmpty(text))
60+
return;
61+
62+
// After the ComboBox has cleared its text (and only if it did)
63+
comboBox.Dispatcher.BeginInvoke(DispatcherPriority.DataBind, new Action(delegate
64+
{
65+
if (string.IsNullOrEmpty(comboBox.Text))
66+
comboBox.Text = text;
67+
}));
68+
}
69+
}

‎Source/NETworkManager.Controls/ObservableRangeCollection.cs‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.ObjectModel;
33
using System.Collections.Specialized;
44
using System.ComponentModel;
5+
using System.Linq;
56

67
namespace NETworkManager.Controls;
78

@@ -23,9 +24,12 @@ public void ReplaceAll(IEnumerable<T> items)
2324
{
2425
CheckReentrancy();
2526

27+
// Read the items first: if a lazy sequence throws, the collection is unchanged (and in sync with the views).
28+
var newItems = items.ToList();
29+
2630
Items.Clear();
2731

28-
foreach (var item in items)
32+
foreach (var item in newItems)
2933
Items.Add(item);
3034

3135
OnPropertyChanged(new PropertyChangedEventArgs(nameof(Count)));

‎Source/NETworkManager.Models/RunCommand/RunCommandTarget.cs‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,17 @@ namespace NETworkManager.Models.RunCommand;
1313
/// </summary>
1414
public static class RunCommandTarget
1515
{
16+
/// <summary>
17+
/// Checks if the ports of an entry of the port lookup (e.g. "22", "80-90/tcp") are valid: from 1 to 65535, a range
18+
/// from a lower to a higher port.
19+
/// </summary>
20+
private static bool IsValidPortLookupEntry(string entry)
21+
{
22+
var ports = entry.Split('/')[0].Split('-').Select(x => int.TryParse(x, out var port) ? port : 0).ToArray();
23+
24+
return ports.All(x => x is > 0 and < 65536) && (ports.Length == 1 || ports[0] < ports[1]);
25+
}
26+
1627
/// <summary>
1728
/// Gets the kind of the target.
1829
/// </summary>
@@ -33,11 +44,10 @@ public static RunCommandTargetKind GetKind(string target)
3344
if (RegexHelper.MACAddressFirst3BytesWithSeparatorRegex().IsMatch(target))
3445
return RunCommandTargetKind.OUI;
3546

36-
// Only ports from 1 to 65535 (e.g. not "99999", which the port lookup searches as a service)
47+
// Like the port lookup: ports from 1 to 65535 and ranges from a lower to a higher port (e.g. not "99999" or
48+
// "90-80", which the port lookup searches as a service)
3749
if (RegexHelper.PortLookupEntriesRegex().IsMatch(target))
38-
return Regex.Matches(target, "[0-9]+").All(x => int.TryParse(x.Value, out var port) && port is > 0 and < 65536)
39-
? RunCommandTargetKind.Ports
40-
: RunCommandTargetKind.None;
50+
return target.Split(';').All(IsValidPortLookupEntry) ? RunCommandTargetKind.Ports : RunCommandTargetKind.None;
4151

4252
if (RegexHelper.ASNumberWithPrefixRegex().IsMatch(target))
4353
return RunCommandTargetKind.ASN;

‎Source/NETworkManager.Tests/RunCommand/RunCommandTargetTests.cs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ public void HostsWithHostnames(string target)
7272
[InlineData("0")]
7373
[InlineData("99999")]
7474
[InlineData("22;99999")]
75+
// A range from a lower to a higher port, like the port lookup
76+
[InlineData("90-80")]
77+
[InlineData("80-80")]
78+
[InlineData("22;90-80/tcp")]
7579
// A shortened or partly typed IPv4 address
7680
[InlineData("10.1")]
7781
[InlineData("192.168.1")]

‎Source/NETworkManager.Tests/Utilities/ListHelperTests.cs‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,20 @@ public void UsesTheComparerAndKeepsTheLatestSpelling()
117117
Assert.Equal(["Ping 10.0.0.1", "a"], list);
118118
}
119119

120+
// "Number of stored entries" can be set to 0 in the settings.
120121
[Fact]
121-
public void ThrowsIfTheLengthIsNotPositive()
122+
public void LengthZeroStoresNoEntries()
122123
{
123-
Assert.Throws<ArgumentOutOfRangeException>(() => ListHelper.Modify(new ObservableCollection<string>(), "a", 0));
124+
var list = new ObservableCollection<string> { "a", "b" };
125+
126+
ListHelper.Modify(list, "c", 0);
127+
128+
Assert.Empty(list);
129+
}
130+
131+
[Fact]
132+
public void ThrowsIfTheLengthIsNegative()
133+
{
134+
Assert.Throws<ArgumentOutOfRangeException>(() => ListHelper.Modify(new ObservableCollection<string>(), "a", -1));
124135
}
125136
}

‎Source/NETworkManager.Utilities/ListHelper.cs‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,29 @@ public static class ListHelper
1818
/// </summary>
1919
/// <param name="list">List to modify (e.g. a history of the settings).</param>
2020
/// <param name="entry">Entry to add to the list.</param>
21-
/// <param name="length">Maximum length of the list. Oldest entries will be removed if the list exceeds this length.</param>
21+
/// <param name="length">
22+
/// Maximum length of the list. Oldest entries will be removed if the list exceeds this length. 0 stores no
23+
/// entries (e.g. "Number of stored entries" is set to 0), the list is emptied.
24+
/// </param>
2225
/// <param name="comparer">Comparer for existing entries (e.g. case-insensitive). If null, the default comparer is used.</param>
2326
/// <typeparam name="T">Type of the list entries. Currently <see cref="string"/> or <see cref="int"/>.</typeparam>
2427
public static void Modify<T>(ObservableCollection<T> list, T entry, int length,
2528
IEqualityComparer<T> comparer = null)
2629
{
2730
comparer ??= EqualityComparer<T>.Default;
2831

29-
if (length <= 0)
30-
throw new ArgumentOutOfRangeException(nameof(length), "Length must be greater than zero.");
32+
if (length < 0)
33+
throw new ArgumentOutOfRangeException(nameof(length), "Length must not be negative.");
34+
35+
// No history: all entries are removed, one by one like the oldest entries below. An editable ComboBox that shows
36+
// the list keeps its text (ComboBoxHistoryBehavior in the HistoryComboBox style).
37+
if (length == 0)
38+
{
39+
while (list.Count > 0)
40+
list.RemoveAt(list.Count - 1);
41+
42+
return;
43+
}
3144

3245
if (EqualityComparer<T>.Default.Equals(entry, default) || entry is string text && string.IsNullOrEmpty(text))
3346
return;

‎Source/NETworkManager/Resources/Styles/ComboBoxStyles.xaml‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
22
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
3-
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls">
3+
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
4+
xmlns:controls="clr-namespace:NETworkManager.Controls;assembly=NETworkManager.Controls">
45
<Style x:Key="DefaultComboBox" TargetType="{x:Type ComboBox}" BasedOn="{StaticResource MahApps.Styles.ComboBox}">
56
<Setter Property="FontSize" Value="14" />
67
<!--<Setter Property="BorderThickness" Value="0,0,0,1" />-->
@@ -17,6 +18,8 @@
1718
</Style>
1819
<Style x:Key="HistoryComboBox" TargetType="{x:Type ComboBox}" BasedOn="{StaticResource DefaultComboBox}">
1920
<Setter Property="IsEditable" Value="True" />
21+
<!-- The history is changed in place, removing the selected entry would clear the text -->
22+
<Setter Property="controls:ComboBoxHistoryBehavior.KeepTextWhenSelectedItemIsRemoved" Value="True" />
2023
<Setter Property="ContextMenu" Value="{StaticResource CutCopyPasteContextMenu}" />
2124
</Style>
2225
</ResourceDictionary>

‎Source/NETworkManager/RunCommandManager.cs‎

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -51,31 +51,27 @@ public static class RunCommandManager
5151
/// </summary>
5252
private static readonly Dictionary<RunCommandTargetKind, ApplicationName[]> TargetApplications = new()
5353
{
54+
// The Port Scanner is not suggested, since it needs ports (or a port profile).
5455
[RunCommandTargetKind.Host] =
5556
[
5657
ApplicationName.PingMonitor, ApplicationName.Traceroute, ApplicationName.DNSLookup,
57-
ApplicationName.PortScanner, ApplicationName.RemoteDesktop, ApplicationName.PuTTY,
58-
ApplicationName.PowerShell, ApplicationName.TigerVNC, ApplicationName.IPGeolocation, ApplicationName.Whois,
59-
ApplicationName.RDAP, ApplicationName.SNMP, ApplicationName.IPScanner
58+
ApplicationName.RemoteDesktop, ApplicationName.PuTTY, ApplicationName.PowerShell, ApplicationName.TigerVNC,
59+
ApplicationName.IPGeolocation, ApplicationName.Whois, ApplicationName.RDAP, ApplicationName.SNMP,
60+
ApplicationName.IPScanner
6061
],
6162
// Without a domain (e.g. in a company network): no tools for public hosts (Whois, RDAP, IP Geolocation)
6263
[RunCommandTargetKind.Hostname] =
6364
[
6465
ApplicationName.PingMonitor, ApplicationName.Traceroute, ApplicationName.DNSLookup,
65-
ApplicationName.PortScanner, ApplicationName.RemoteDesktop, ApplicationName.PuTTY,
66-
ApplicationName.PowerShell, ApplicationName.TigerVNC, ApplicationName.SNMP, ApplicationName.IPScanner
67-
],
68-
[RunCommandTargetKind.Hosts] =
69-
[
70-
ApplicationName.PingMonitor, ApplicationName.DNSLookup, ApplicationName.PortScanner,
71-
ApplicationName.IPScanner
66+
ApplicationName.RemoteDesktop, ApplicationName.PuTTY, ApplicationName.PowerShell, ApplicationName.TigerVNC,
67+
ApplicationName.SNMP, ApplicationName.IPScanner
7268
],
73-
[RunCommandTargetKind.IPRange] =
74-
[ApplicationName.IPScanner, ApplicationName.PortScanner, ApplicationName.PingMonitor],
69+
[RunCommandTargetKind.Hosts] = [ApplicationName.PingMonitor, ApplicationName.DNSLookup, ApplicationName.IPScanner],
70+
[RunCommandTargetKind.IPRange] = [ApplicationName.IPScanner, ApplicationName.PingMonitor],
7571
[RunCommandTargetKind.Subnet] =
7672
[
77-
ApplicationName.IPScanner, ApplicationName.SubnetCalculator, ApplicationName.PortScanner,
78-
ApplicationName.PingMonitor, ApplicationName.RDAP
73+
ApplicationName.IPScanner, ApplicationName.SubnetCalculator, ApplicationName.PingMonitor,
74+
ApplicationName.RDAP
7975
],
8076
[RunCommandTargetKind.MACAddress] = [ApplicationName.WakeOnLAN, ApplicationName.Lookup],
8177
[RunCommandTargetKind.OUI] = [ApplicationName.Lookup],

0 commit comments

Comments
 (0)