From a6857a06271044d40dac8e51bde16c1128da7f93 Mon Sep 17 00:00:00 2001
From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com>
Date: Sun, 6 Jul 2025 02:52:27 +0200
Subject: [PATCH 1/2] Feature: HostsFileEditor delete entry added
---
.../Resources/Strings.Designer.cs | 11 ++
.../Resources/Strings.resx | 5 +
.../HostsFileEditor/HostsFileEditor.cs | 132 ++++++++++++++++--
.../HostsFileEntryModifyResult.cs | 28 ++++
.../ViewModels/HostsFileEditorViewModel.cs | 57 +++++++-
.../ViewModels/SNTPLookupSettingsViewModel.cs | 17 ++-
.../Views/ExportChildWindow.xaml | 2 +-
.../Views/GroupChildWindow.xaml | 2 +-
.../Views/OKCancelInfoMessageChildWindow.xaml | 7 +-
.../Views/ProfileChildWindow.xaml | 2 +-
10 files changed, 232 insertions(+), 31 deletions(-)
create mode 100644 Source/NETworkManager.Models/HostsFileEditor/HostsFileEntryModifyResult.cs
diff --git a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
index 0b8f1f0128..1b033dfbba 100644
--- a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
+++ b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
@@ -2591,6 +2591,17 @@ public static string DeleteGroupMessage {
}
}
+ ///
+ /// Sucht eine lokalisierte Zeichenfolge, die The selected entry is permanently deleted:
+ ///
+ ///{0} {1} {2} ähnelt.
+ ///
+ public static string DeleteHostsFileEntryMessage {
+ get {
+ return ResourceManager.GetString("DeleteHostsFileEntryMessage", resourceCulture);
+ }
+ }
+
///
/// Sucht eine lokalisierte Zeichenfolge, die Delete OID profile ähnelt.
///
diff --git a/Source/NETworkManager.Localization/Resources/Strings.resx b/Source/NETworkManager.Localization/Resources/Strings.resx
index f8293f0385..11315b1d1e 100644
--- a/Source/NETworkManager.Localization/Resources/Strings.resx
+++ b/Source/NETworkManager.Localization/Resources/Strings.resx
@@ -3933,4 +3933,9 @@ Right-click for more options.
Entries
+
+ The selected entry is permanently deleted:
+
+{0} {1} {2}
+
\ No newline at end of file
diff --git a/Source/NETworkManager.Models/HostsFileEditor/HostsFileEditor.cs b/Source/NETworkManager.Models/HostsFileEditor/HostsFileEditor.cs
index a692908df7..34ae2f28c6 100644
--- a/Source/NETworkManager.Models/HostsFileEditor/HostsFileEditor.cs
+++ b/Source/NETworkManager.Models/HostsFileEditor/HostsFileEditor.cs
@@ -164,27 +164,52 @@ private static IEnumerable GetHostsFileEntries()
return entries;
}
- public static Task EnableEntryAsync(HostsFileEntry entry)
+ ///
+ /// Enable a hosts file entry asynchronously.
+ ///
+ /// Entry to enable.
+ /// if the entry was enabled successfully, otherwise an error result.
+ public static Task EnableEntryAsync(HostsFileEntry entry)
{
return Task.Run(() => EnableEntry(entry));
}
- private static bool EnableEntry(HostsFileEntry entry)
+ ///
+ /// Enable a hosts file entry.
+ ///
+ /// Entry to enable.
+ /// if the entry was enabled successfully, otherwise an error result.
+ private static HostsFileEntryModifyResult EnableEntry(HostsFileEntry entry)
{
// Create a backup of the hosts file before making changes
if (CreateBackup() == false)
{
Log.Error("EnableEntry - Failed to create backup before enabling entry.");
- return false;
+ return HostsFileEntryModifyResult.BackupError;
}
// Replace the entry in the hosts file
var hostsFileLines = File.ReadAllLines(HostsFilePath).ToList();
+ bool entryFound = false;
+
for (var i = 0; i < hostsFileLines.Count; i++)
{
if (hostsFileLines[i] == entry.Line)
+ {
+ entryFound = true;
+
hostsFileLines[i] = entry.Line.TrimStart('#', ' ');
+
+ break;
+ }
+ }
+
+ if (!entryFound)
+ {
+ Log.Warn($"EnableEntry - Entry not found in hosts file: {entry.Line}");
+
+ return HostsFileEntryModifyResult.NotFound;
}
try
@@ -195,34 +220,57 @@ private static bool EnableEntry(HostsFileEntry entry)
catch (Exception ex)
{
Log.Error($"EnableEntry - Failed to write changes to hosts file: {HostsFilePath}", ex);
-
- return false;
+ return HostsFileEntryModifyResult.WriteError;
}
- return true;
+ return HostsFileEntryModifyResult.Success;
}
- public static Task DisableEntryAsync(HostsFileEntry entry)
+ ///
+ /// Disable a hosts file entry asynchronously.
+ ///
+ /// Entry to disable.
+ /// if the entry was enabled successfully, otherwise an error result.
+ public static Task DisableEntryAsync(HostsFileEntry entry)
{
return Task.Run(() => DisableEntry(entry));
}
- private static bool DisableEntry(HostsFileEntry entry)
+ ///
+ /// Disable a hosts file entry.
+ ///
+ /// Entry to disable.
+ /// if the entry was enabled successfully, otherwise an error result.
+ private static HostsFileEntryModifyResult DisableEntry(HostsFileEntry entry)
{
// Create a backup of the hosts file before making changes
if (CreateBackup() == false)
{
Log.Error("DisableEntry - Failed to create backup before disabling entry.");
- return false;
+ return HostsFileEntryModifyResult.BackupError;
}
// Replace the entry in the hosts file
var hostsFileLines = File.ReadAllLines(HostsFilePath).ToList();
+ bool entryFound = false;
+
for (var i = 0; i < hostsFileLines.Count; i++)
{
if (hostsFileLines[i] == entry.Line)
+ {
+ entryFound = true;
+
hostsFileLines[i] = "# " + entry.Line;
+
+ break;
+ }
+ }
+
+ if (!entryFound)
+ {
+ Log.Warn($"DisableEntry - Entry not found in hosts file: {entry.Line}");
+ return HostsFileEntryModifyResult.NotFound;
}
try
@@ -233,15 +281,75 @@ private static bool DisableEntry(HostsFileEntry entry)
catch (Exception ex)
{
Log.Error($"DisableEntry - Failed to write changes to hosts file: {HostsFilePath}", ex);
+ return HostsFileEntryModifyResult.WriteError;
+ }
- return false;
+ return HostsFileEntryModifyResult.Success;
+ }
+
+ ///
+ /// Delete a hosts file entry asynchronously.
+ ///
+ /// Entry to delete."/>
+ /// if the entry was enabled successfully, otherwise an error result.s
+ public static Task DeleteEntryAsync(HostsFileEntry entry)
+ {
+ return Task.Run(() => DeleteEntry(entry));
+ }
+
+ ///
+ /// Delete a hosts file entry.
+ ///
+ /// Entry to delete."/>
+ /// if the entry was enabled successfully, otherwise an error result.
+ private static HostsFileEntryModifyResult DeleteEntry(HostsFileEntry entry)
+ {
+ // Create a backup of the hosts file before making changes
+ if (CreateBackup() == false)
+ {
+ Log.Error("DeleteEntry - Failed to create backup before deleting entry.");
+ return HostsFileEntryModifyResult.BackupError;
}
- return true;
+ // Remove the entry from the hosts file
+ var hostsFileLines = File.ReadAllLines(HostsFilePath).ToList();
+
+ bool entryFound = false;
+
+ for (var i = 0; i < hostsFileLines.Count; i++)
+ {
+ if (hostsFileLines[i] == entry.Line)
+ {
+ entryFound = true;
+
+ hostsFileLines.RemoveAt(i);
+
+ break;
+ }
+ }
+
+ if (!entryFound)
+ {
+ Log.Warn($"DeleteEntry - Entry not found in hosts file: {entry.Line}");
+ return HostsFileEntryModifyResult.NotFound;
+ }
+
+ try
+ {
+ Log.Debug($"DeleteEntry - Writing changes to hosts file: {HostsFilePath}");
+ File.WriteAllLines(HostsFilePath, hostsFileLines);
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"DeleteEntry - Failed to write changes to hosts file: {HostsFilePath}", ex);
+ return HostsFileEntryModifyResult.WriteError;
+ }
+ OnHostsFileChanged();
+ return HostsFileEntryModifyResult.Success;
}
///
- /// Create a daily backup of the hosts file (before making a change).
+ /// Create a "daily" backup of the hosts file (before making a change).
///
private static bool CreateBackup()
{
diff --git a/Source/NETworkManager.Models/HostsFileEditor/HostsFileEntryModifyResult.cs b/Source/NETworkManager.Models/HostsFileEditor/HostsFileEntryModifyResult.cs
new file mode 100644
index 0000000000..365ee39b86
--- /dev/null
+++ b/Source/NETworkManager.Models/HostsFileEditor/HostsFileEntryModifyResult.cs
@@ -0,0 +1,28 @@
+namespace NETworkManager.Models.HostsFileEditor
+{
+ ///
+ /// Represents the result of an attempt to modify a hosts file entry.
+ ///
+ public enum HostsFileEntryModifyResult
+ {
+ ///
+ /// The entry was modified successfully and the hosts file was updated.
+ ///
+ Success,
+
+ ///
+ /// The entry was not found in the hosts file.
+ ///
+ NotFound,
+
+ ///
+ /// An error occurred while writing to the hosts file.
+ ///
+ WriteError,
+
+ ///
+ /// An error occurred while backing up the hosts file.
+ ///
+ BackupError,
+ }
+}
diff --git a/Source/NETworkManager/ViewModels/HostsFileEditorViewModel.cs b/Source/NETworkManager/ViewModels/HostsFileEditorViewModel.cs
index 9baa98157f..30cdb00ca6 100644
--- a/Source/NETworkManager/ViewModels/HostsFileEditorViewModel.cs
+++ b/Source/NETworkManager/ViewModels/HostsFileEditorViewModel.cs
@@ -93,6 +93,21 @@ public IList SelectedResults
}
}
+ private bool _isModifying;
+
+ public bool IsModifying
+ {
+ get => _isModifying;
+ set
+ {
+ if (value == _isModifying)
+ return;
+
+ _isModifying = value;
+ OnPropertyChanged();
+ }
+ }
+
private bool _isRefreshing;
public bool IsRefreshing
@@ -192,7 +207,8 @@ private bool Refresh_CanExecute(object parameter)
return Application.Current.MainWindow != null &&
!((MetroWindow)Application.Current.MainWindow).IsAnyDialogOpen &&
!ConfigurationManager.Current.IsChildWindowOpen &&
- !IsRefreshing;
+ !IsRefreshing &&
+ !IsModifying;
}
private async Task RefreshAction()
@@ -253,14 +269,22 @@ await _dialogCoordinator.ShowMessageAsync(this, Strings.Error,
private async Task EnableEntryAction()
{
+ IsModifying = true;
+
await HostsFileEditor.EnableEntryAsync(SelectedResult);
+
+ IsModifying = false;
}
public ICommand DisableEntryCommand => new RelayCommand(_ => DisableEntryAction().ConfigureAwait(false), ModifyEntry_CanExecute);
private async Task DisableEntryAction()
{
+ IsModifying = true;
+
await HostsFileEditor.DisableEntryAsync(SelectedResult);
+
+ IsModifying = false;
}
public ICommand AddEntryCommand => new RelayCommand(_ => AddEntryAction().ConfigureAwait(false), ModifyEntry_CanExecute);
@@ -274,7 +298,33 @@ private async Task AddEntryAction()
private async Task DeleteEntryAction()
{
- MessageBox.Show("Delete entry action is not implemented yet.", "Delete Entry", MessageBoxButton.OK, MessageBoxImage.Information);
+ IsModifying = true;
+
+ var childWindow = new OKCancelInfoMessageChildWindow();
+
+ var childWindowViewModel = new OKCancelInfoMessageViewModel(async _ =>
+ {
+ childWindow.IsOpen = false;
+ ConfigurationManager.Current.IsChildWindowOpen = false;
+
+ await HostsFileEditor.DeleteEntryAsync(SelectedResult);
+
+ IsModifying = false;
+ }, _ =>
+ {
+ childWindow.IsOpen = false;
+ ConfigurationManager.Current.IsChildWindowOpen = false;
+
+ IsModifying = false;
+ }, string.Format(Strings.DeleteHostsFileEntryMessage, SelectedResult.IPAddress, SelectedResult.Hostname, string.IsNullOrEmpty(SelectedResult.Comment) ? "" : $"# {SelectedResult.Comment}"));
+
+ childWindow.Title = Strings.DeleteEntry;
+
+ childWindow.DataContext = childWindowViewModel;
+
+ ConfigurationManager.Current.IsChildWindowOpen = true;
+
+ await (Application.Current.MainWindow as MainWindow).ShowChildWindowAsync(childWindow);
}
public ICommand EditEntryCommand => new RelayCommand(_ => EditEntryAction().ConfigureAwait(false), ModifyEntry_CanExecute);
@@ -290,7 +340,8 @@ private bool ModifyEntry_CanExecute(object obj)
Application.Current.MainWindow != null &&
!((MetroWindow)Application.Current.MainWindow).IsAnyDialogOpen &&
!ConfigurationManager.Current.IsChildWindowOpen &&
- !IsRefreshing;
+ !IsRefreshing &&
+ !IsModifying;
}
public ICommand RestartAsAdminCommand => new RelayCommand(_ => RestartAsAdminAction().ConfigureAwait(false));
diff --git a/Source/NETworkManager/ViewModels/SNTPLookupSettingsViewModel.cs b/Source/NETworkManager/ViewModels/SNTPLookupSettingsViewModel.cs
index 46a8e4ed4e..8dc6df77d3 100644
--- a/Source/NETworkManager/ViewModels/SNTPLookupSettingsViewModel.cs
+++ b/Source/NETworkManager/ViewModels/SNTPLookupSettingsViewModel.cs
@@ -1,17 +1,17 @@
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Linq;
-using System.Threading.Tasks;
-using System.Windows;
-using System.Windows.Data;
-using System.Windows.Input;
-using MahApps.Metro.Controls.Dialogs;
+using MahApps.Metro.Controls.Dialogs;
using MahApps.Metro.SimpleChildWindow;
using NETworkManager.Localization.Resources;
using NETworkManager.Models.Network;
using NETworkManager.Settings;
using NETworkManager.Utilities;
using NETworkManager.Views;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Data;
+using System.Windows.Input;
namespace NETworkManager.ViewModels;
@@ -189,7 +189,6 @@ private Task DeleteServer()
{
var childWindow = new OKCancelInfoMessageChildWindow();
-
var childWindowViewModel = new OKCancelInfoMessageViewModel(_ =>
{
childWindow.IsOpen = false;
diff --git a/Source/NETworkManager/Views/ExportChildWindow.xaml b/Source/NETworkManager/Views/ExportChildWindow.xaml
index 3a9eb50508..896de4c21f 100644
--- a/Source/NETworkManager/Views/ExportChildWindow.xaml
+++ b/Source/NETworkManager/Views/ExportChildWindow.xaml
@@ -30,7 +30,7 @@
-
+
diff --git a/Source/NETworkManager/Views/GroupChildWindow.xaml b/Source/NETworkManager/Views/GroupChildWindow.xaml
index 4328ceb7dd..633e30aff4 100644
--- a/Source/NETworkManager/Views/GroupChildWindow.xaml
+++ b/Source/NETworkManager/Views/GroupChildWindow.xaml
@@ -43,7 +43,7 @@
-
+
diff --git a/Source/NETworkManager/Views/OKCancelInfoMessageChildWindow.xaml b/Source/NETworkManager/Views/OKCancelInfoMessageChildWindow.xaml
index 1fd3d8456a..1453bdcd92 100644
--- a/Source/NETworkManager/Views/OKCancelInfoMessageChildWindow.xaml
+++ b/Source/NETworkManager/Views/OKCancelInfoMessageChildWindow.xaml
@@ -17,12 +17,12 @@
AllowMove="True"
TitleForeground="{DynamicResource MahApps.Brushes.Gray3}"
CloseByEscape="False"
- OverlayBrush="{DynamicResource ResourceKey=MahApps.Brushes.Gray.SemiTransparent}"
+ OverlayBrush="{DynamicResource ResourceKey=MahApps.Brushes.Gray.SemiTransparent}"
mc:Ignorable="d" d:DataContext="{d:DesignInstance viewModels:OKCancelInfoMessageViewModel}">
-
+
+ Style="{StaticResource ResourceKey=WrapTextBlock}"/>
diff --git a/Source/NETworkManager/Views/ProfileChildWindow.xaml b/Source/NETworkManager/Views/ProfileChildWindow.xaml
index 6cffd4ac6b..5b306f6177 100644
--- a/Source/NETworkManager/Views/ProfileChildWindow.xaml
+++ b/Source/NETworkManager/Views/ProfileChildWindow.xaml
@@ -46,7 +46,7 @@
-
+
From 22437dc639b25ba0b8aa0c29a759b29b56f00525 Mon Sep 17 00:00:00 2001
From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com>
Date: Sun, 6 Jul 2025 02:55:17 +0200
Subject: [PATCH 2/2] Docs: #3094
---
Website/docs/changelog/next-release.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Website/docs/changelog/next-release.md b/Website/docs/changelog/next-release.md
index 102ec081a5..630157d855 100644
--- a/Website/docs/changelog/next-release.md
+++ b/Website/docs/changelog/next-release.md
@@ -23,7 +23,7 @@ Release date: **xx.xx.2025**
**Hosts File Editor**
-- New feature to display (and edit) the `hosts` file. (See [documentation](https://borntoberoot.net/NETworkManager/docs/application/hosts-file-editor) for more details) [#3012](https://github.com/BornToBeRoot/NETworkManager/pull/3012) [#3092](https://github.com/BornToBeRoot/NETworkManager/pull/3092)
+- New feature to display (and edit) the `hosts` file. (See [documentation](https://borntoberoot.net/NETworkManager/docs/application/hosts-file-editor) for more details) [#3012](https://github.com/BornToBeRoot/NETworkManager/pull/3012) [#3092](https://github.com/BornToBeRoot/NETworkManager/pull/3092) [#3094](https://github.com/BornToBeRoot/NETworkManager/pull/3094)
:::info