diff --git a/Source/NETworkManager.Controls/GroupExpandedChangedArgs.cs b/Source/NETworkManager.Controls/GroupExpandedChangedArgs.cs
new file mode 100644
index 0000000000..975ec5f6cf
--- /dev/null
+++ b/Source/NETworkManager.Controls/GroupExpandedChangedArgs.cs
@@ -0,0 +1,10 @@
+using System;
+
+namespace NETworkManager.Controls
+{
+ public class GroupExpandedChangedArgs(string groupName, bool isExpanded) : EventArgs
+ {
+ public string GroupName { get; } = groupName;
+ public bool IsExpanded { get; } = isExpanded;
+ }
+}
diff --git a/Source/NETworkManager.Controls/GroupExpander.cs b/Source/NETworkManager.Controls/GroupExpander.cs
index 3bfd0c1f44..d5105f0908 100644
--- a/Source/NETworkManager.Controls/GroupExpander.cs
+++ b/Source/NETworkManager.Controls/GroupExpander.cs
@@ -8,7 +8,7 @@ public class GroupExpander : Expander
{
public static readonly DependencyProperty StateStoreProperty =
DependencyProperty.Register(
- "StateStore",
+ nameof(StateStore),
typeof(GroupExpanderStateStore),
typeof(GroupExpander),
new PropertyMetadata(null, OnStateStoreChanged));
diff --git a/Source/NETworkManager.Controls/GroupExpanderStateStore.cs b/Source/NETworkManager.Controls/GroupExpanderStateStore.cs
index 76eea8e940..2aead547fe 100644
--- a/Source/NETworkManager.Controls/GroupExpanderStateStore.cs
+++ b/Source/NETworkManager.Controls/GroupExpanderStateStore.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
@@ -8,6 +9,11 @@ public class GroupExpanderStateStore : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
+ ///
+ /// Occurs when a single group's expansion state changes via the indexer (not raised by ).
+ ///
+ public event EventHandler GroupExpandedChanged;
+
protected void OnPropertyChanged(string propertyName) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
@@ -40,7 +46,25 @@ public bool this[string groupName]
_states[groupName] = value;
OnPropertyChanged($"Item[{groupName}]");
+ GroupExpandedChanged?.Invoke(this, new GroupExpandedChangedArgs(groupName, value));
}
}
+
+ ///
+ /// Replaces all stored states with the given ones (e.g. bulk-loading previously persisted state).
+ /// Does not raise or - consumers
+ /// (e.g. ) read the current state fresh whenever they bind, so no
+ /// notification is needed, and this avoids a load-triggers-save feedback loop.
+ ///
+ public void LoadState(IReadOnlyDictionary state)
+ {
+ _states.Clear();
+
+ if (state == null)
+ return;
+
+ foreach (var kvp in state)
+ _states[kvp.Key] = kvp.Value;
+ }
}
}
diff --git a/Source/NETworkManager.Profiles/ProfileInfo.cs b/Source/NETworkManager.Profiles/ProfileInfo.cs
index 80cb4937a4..209b19d33b 100644
--- a/Source/NETworkManager.Profiles/ProfileInfo.cs
+++ b/Source/NETworkManager.Profiles/ProfileInfo.cs
@@ -222,10 +222,6 @@ public ProfileInfo(ProfileInfo profile)
SNMP_PrivacyProvider = profile.SNMP_PrivacyProvider;
SNMP_Priv = profile.SNMP_Priv;
- // Firewall
- Firewall_Enabled = profile.Firewall_Enabled;
- //Firewall_Rules = profile.Firewall_Rules;
-
// Wake on LAN
WakeOnLAN_Enabled = profile.WakeOnLAN_Enabled;
WakeOnLAN_MACAddress = profile.WakeOnLAN_MACAddress;
@@ -486,9 +482,6 @@ public ProfileInfo(ProfileInfo profile)
public SNMPV3PrivacyProvider SNMP_PrivacyProvider { get; set; } = GlobalStaticConfiguration.SNMP_PrivacyProvider;
[XmlIgnore] public SecureString SNMP_Priv { get; set; }
- public bool Firewall_Enabled { get; set; }
- //public List Firewall_Rules { get; set; }
-
public bool WakeOnLAN_Enabled { get; set; }
public string WakeOnLAN_MACAddress { get; set; }
public string WakeOnLAN_Broadcast { get; set; }
diff --git a/Source/NETworkManager.Profiles/ProfileManager.cs b/Source/NETworkManager.Profiles/ProfileManager.cs
index 5dc48df2be..38ed36f408 100644
--- a/Source/NETworkManager.Profiles/ProfileManager.cs
+++ b/Source/NETworkManager.Profiles/ProfileManager.cs
@@ -381,6 +381,8 @@ public static void RenameProfileFile(ProfileFileInfo profileFileInfo, string new
File.Copy(profileFileInfo.Path, newProfileFileInfo.Path);
ProfileFiles.Add(newProfileFileInfo);
+ MigrateGroupExpandState(profileFileInfo.Path, newProfileFileInfo.Path);
+
// Switch profile, if it was previously loaded
if (switchProfile)
{
@@ -407,6 +409,8 @@ public static void DeleteProfileFile(ProfileFileInfo profileFileInfo)
if (LoadedProfileFile != null && LoadedProfileFile.Equals(profileFileInfo))
LoadedProfileFileChanged(ProfileFiles.FirstOrDefault(x => !x.Equals(profileFileInfo)));
+ RemoveGroupExpandState(profileFileInfo.Path);
+
File.Delete(profileFileInfo.Path);
ProfileFiles.Remove(profileFileInfo);
}
@@ -478,6 +482,8 @@ public static void EnableEncryption(ProfileFileInfo profileFileInfo, SecureStrin
// Add the new profile
ProfileFiles.Add(newProfileFileInfo);
+ MigrateGroupExpandState(profileFileInfo.Path, newProfileFileInfo.Path);
+
// Switch profile, if it was previously loaded
if (switchProfile)
{
@@ -563,6 +569,8 @@ public static void ChangeMasterPassword(ProfileFileInfo profileFileInfo, SecureS
// Add the new profile
ProfileFiles.Add(newProfileFileInfo);
+ MigrateGroupExpandState(profileFileInfo.Path, newProfileFileInfo.Path);
+
// Switch profile, if it was previously loaded
if (switchProfile)
{
@@ -632,6 +640,8 @@ public static void DisableEncryption(ProfileFileInfo profileFileInfo, SecureStri
// Add the new profile
ProfileFiles.Add(newProfileFileInfo);
+ MigrateGroupExpandState(profileFileInfo.Path, newProfileFileInfo.Path);
+
// Switch profile, if it was previously loaded
if (switchProfile)
{
@@ -739,6 +749,8 @@ private static void Load(ProfileFileInfo profileFileInfo)
// Add the new profile
ProfileFiles.Add(newProfileFileInfo);
+ MigrateGroupExpandState(profileFileInfo.Path, newProfileFileInfo.Path);
+
// Switch profile
Log.Info($"Switching to migrated profile file: {newProfileFileInfo.Path}.");
Switch(newProfileFileInfo, false);
@@ -849,6 +861,70 @@ public static void Switch(ProfileFileInfo info, bool saveLoadedProfiles = true)
#endregion
+ #region Group expand state persistence
+
+ ///
+ /// Moves the persisted group-expand state (see )
+ /// from the old to the new profile file path. No-op if the paths are equal or there is no state for
+ /// the old path.
+ ///
+ private static void MigrateGroupExpandState(string oldPath, string newPath)
+ {
+ if (oldPath == newPath)
+ return;
+
+ if (SettingsManager.Current.Profiles_GroupExpandState.Remove(oldPath, out var state))
+ {
+ SettingsManager.Current.Profiles_GroupExpandState[newPath] = state;
+ SettingsManager.Current.SettingsChanged = true;
+ }
+ }
+
+ ///
+ /// Removes the persisted group-expand state for a profile file that no longer exists.
+ ///
+ private static void RemoveGroupExpandState(string path)
+ {
+ if (SettingsManager.Current.Profiles_GroupExpandState.Remove(path))
+ SettingsManager.Current.SettingsChanged = true;
+ }
+
+ ///
+ /// Renames the group key inside the persisted group-expand state for the currently loaded profile file.
+ ///
+ private static void MigrateGroupExpandStateKey(string oldGroupName, string newGroupName)
+ {
+ if (oldGroupName == newGroupName)
+ return;
+
+ if (LoadedProfileFile == null)
+ return;
+
+ if (!SettingsManager.Current.Profiles_GroupExpandState.TryGetValue(LoadedProfileFile.Path, out var state))
+ return;
+
+ if (!state.Remove(oldGroupName, out var isExpanded))
+ return;
+
+ state[newGroupName] = isExpanded;
+ SettingsManager.Current.SettingsChanged = true;
+ }
+
+ ///
+ /// Removes the group key from the persisted group-expand state for the currently loaded profile file.
+ ///
+ private static void RemoveGroupExpandStateKey(string groupName)
+ {
+ if (LoadedProfileFile == null)
+ return;
+
+ if (SettingsManager.Current.Profiles_GroupExpandState.TryGetValue(LoadedProfileFile.Path, out var state)
+ && state.Remove(groupName))
+ SettingsManager.Current.SettingsChanged = true;
+ }
+
+ #endregion
+
#region Serialize and deserialize
///
@@ -1085,6 +1161,8 @@ public static void ReplaceGroup(GroupInfo oldGroup, GroupInfo newGroup)
LoadedProfileFileData.Groups.Remove(oldGroup);
LoadedProfileFileData.Groups.Add(newGroup);
+ MigrateGroupExpandStateKey(oldGroup.Name, newGroup.Name);
+
ProfilesUpdated();
}
@@ -1099,6 +1177,8 @@ public static void RemoveGroup(GroupInfo group)
LoadedProfileFileData.Groups.Remove(group);
+ RemoveGroupExpandStateKey(group.Name);
+
ProfilesUpdated();
}
diff --git a/Source/NETworkManager.Settings/SettingsInfo.cs b/Source/NETworkManager.Settings/SettingsInfo.cs
index 81931a83df..eb33ec907c 100644
--- a/Source/NETworkManager.Settings/SettingsInfo.cs
+++ b/Source/NETworkManager.Settings/SettingsInfo.cs
@@ -10,6 +10,7 @@
using NETworkManager.Utilities;
using NETworkManager.Utilities.ActiveDirectory;
using System;
+using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
@@ -566,6 +567,25 @@ public string Profiles_LastSelected
}
}
+ ///
+ /// Persisted collapsed/expanded state of profile groups, shared across all tools. Outer key is the
+ /// full profile file path, inner key is the group name. Mutated in place by callers - since nested
+ /// dictionary mutations don't raise , callers must explicitly set
+ /// themselves.
+ ///
+ public Dictionary> Profiles_GroupExpandState
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+
+ field = value;
+ OnPropertyChanged();
+ }
+ } = [];
+
public bool Profiles_IsDailyBackupEnabled
{
get;
@@ -3198,32 +3218,6 @@ public ExportFileType HostsFileEditor_ExportFileType
#endregion
#region Firewall
- public bool Firewall_ExpandProfileView
- {
- get;
- set
- {
- if (value == field)
- return;
-
- field = value;
- OnPropertyChanged();
- }
- } = GlobalStaticConfiguration.Profile_ExpandProfileView;
-
- public double Firewall_ProfileWidth
- {
- get;
- set
- {
- if (Math.Abs(value - field) < GlobalStaticConfiguration.FloatPointFix)
- return;
-
- field = value;
- OnPropertyChanged();
- }
- } = GlobalStaticConfiguration.Profile_DefaultWidthExpanded;
-
public string Firewall_ExportFilePath
{
get;
diff --git a/Source/NETworkManager/ProfileDialogManager.cs b/Source/NETworkManager/ProfileDialogManager.cs
index 4eb52230c1..f2e733c241 100644
--- a/Source/NETworkManager/ProfileDialogManager.cs
+++ b/Source/NETworkManager/ProfileDialogManager.cs
@@ -256,9 +256,6 @@ private static ProfileInfo ParseProfileInfo(ProfileViewModel instance)
? instance.SNMP_Priv
: new SecureString(),
- // Firewall
- Firewall_Enabled = instance.Firewall_Enabled,
-
// Wake on LAN
WakeOnLAN_Enabled = instance.WakeOnLAN_Enabled,
WakeOnLAN_MACAddress = instance.WakeOnLAN_MACAddress?.Trim(),
diff --git a/Source/NETworkManager/ViewModels/FirewallViewModel.cs b/Source/NETworkManager/ViewModels/FirewallViewModel.cs
index 18d8471edd..384759b2a0 100644
--- a/Source/NETworkManager/ViewModels/FirewallViewModel.cs
+++ b/Source/NETworkManager/ViewModels/FirewallViewModel.cs
@@ -1,4 +1,4 @@
-using log4net;
+using log4net;
using MahApps.Metro.Controls;
using MahApps.Metro.SimpleChildWindow;
using NETworkManager.Localization.Resources;
@@ -17,27 +17,19 @@
using System.Windows;
using System.Windows.Data;
using System.Windows.Input;
-using System.Windows.Threading;
namespace NETworkManager.ViewModels;
-using Controls;
-using Profiles;
-using Models;
-
///
/// ViewModel for the Firewall application.
///
-public class FirewallViewModel : ViewModelBase, IProfileManager
+public class FirewallViewModel : ViewModelBase
{
#region Variables
private static readonly ILog Log = LogManager.GetLogger(typeof(FirewallViewModel));
- private readonly DispatcherTimer _searchDispatcherTimer = new();
- private bool _searchDisabled;
private readonly bool _isLoading;
- private bool _isViewActive = true;
#region Rules
@@ -150,208 +142,6 @@ private set
#endregion
- #region Profiles
-
- ///
- /// Gets the collection view of profiles.
- ///
- public ICollectionView Profiles
- {
- get;
- private set
- {
- if (value == field)
- return;
-
- field = value;
- OnPropertyChanged();
- }
- }
-
- ///
- /// Gets or sets the selected profile.
- ///
- public ProfileInfo SelectedProfile
- {
- get;
- set
- {
- if (value == field)
- return;
-
- field = value;
- OnPropertyChanged();
- }
- } = new();
-
- ///
- /// Gets or sets the search text.
- ///
- public string Search
- {
- get;
- set
- {
- if (value == field)
- return;
-
- field = value;
-
- // Start searching...
- if (!_searchDisabled)
- {
- IsSearching = true;
- _searchDispatcherTimer.Start();
- }
-
- OnPropertyChanged();
- }
- }
-
- ///
- /// Gets or sets a value indicating whether a search is in progress.
- ///
- public bool IsSearching
- {
- get;
- set
- {
- if (value == field)
- return;
-
- field = value;
- OnPropertyChanged();
- }
- }
-
- ///
- /// Gets or sets a value indicating whether the profile filter is open.
- ///
- public bool ProfileFilterIsOpen
- {
- get;
- set
- {
- if (value == field)
- return;
-
- field = value;
- OnPropertyChanged();
- }
- }
-
- ///
- /// Gets the collection view for profile filter tags.
- ///
- public ICollectionView ProfileFilterTagsView { get; }
-
- ///
- /// Gets the collection of profile filter tags.
- ///
- private ObservableCollection ProfileFilterTags { get; } = [];
-
- ///
- /// Gets or sets a value indicating whether any tag match is sufficient for filtering.
- ///
- public bool ProfileFilterTagsMatchAny
- {
- get;
- set
- {
- if (value == field)
- return;
-
- field = value;
- OnPropertyChanged();
- }
- } = GlobalStaticConfiguration.Profile_TagsMatchAny;
-
- ///
- /// Gets or sets a value indicating whether all tags must match for filtering.
- ///
- public bool ProfileFilterTagsMatchAll
- {
- get;
- set
- {
- if (value == field)
- return;
-
- field = value;
- OnPropertyChanged();
- }
- }
-
- ///
- /// Gets or sets a value indicating whether a profile filter is set.
- ///
- public bool IsProfileFilterSet
- {
- get;
- set
- {
- if (value == field)
- return;
-
- field = value;
- OnPropertyChanged();
- }
- }
-
- public GroupExpanderStateStore GroupExpanderStateStore { get; } = new();
-
- private bool _canProfileWidthChange = true;
- private double _tempProfileWidth;
-
- ///
- /// Gets or sets a value indicating whether the profile view is expanded.
- ///
- public bool ExpandProfileView
- {
- get;
- set
- {
- if (value == field)
- return;
-
- if (!_isLoading)
- SettingsManager.Current.Firewall_ExpandProfileView = value;
-
- field = value;
-
- if (_canProfileWidthChange)
- ResizeProfile(false);
-
- OnPropertyChanged();
- }
- }
-
- ///
- /// Gets or sets the width of the profile view.
- ///
- public GridLength ProfileWidth
- {
- get;
- set
- {
- if (value == field)
- return;
-
- if (!_isLoading && Math.Abs(value.Value - GlobalStaticConfiguration.Profile_WidthCollapsed) >
- GlobalStaticConfiguration.FloatPointFix) // Do not save the size when collapsed
- SettingsManager.Current.Firewall_ProfileWidth = value.Value;
-
- field = value;
-
- if (_canProfileWidthChange)
- ResizeProfile(true);
-
- OnPropertyChanged();
- }
- }
-
- #endregion
-
#endregion
#region Constructor, load settings
@@ -382,39 +172,9 @@ public FirewallViewModel()
// Load firewall rules
_ = Refresh(true);
- // Profiles
- CreateTags();
-
- ProfileFilterTagsView = CollectionViewSource.GetDefaultView(ProfileFilterTags);
- ProfileFilterTagsView.SortDescriptions.Add(new SortDescription(nameof(ProfileFilterTagsInfo.Name),
- ListSortDirection.Ascending));
-
- SetProfilesView(new ProfileFilterInfo());
-
- ProfileManager.OnProfilesUpdated += ProfileManager_OnProfilesUpdated;
-
- _searchDispatcherTimer.Interval = GlobalStaticConfiguration.SearchDispatcherTimerTimeSpan;
- _searchDispatcherTimer.Tick += SearchDispatcherTimer_Tick;
-
- LoadSettings();
-
_isLoading = false;
}
- ///
- /// Loads the settings.
- ///
- private void LoadSettings()
- {
- ExpandProfileView = SettingsManager.Current.Firewall_ExpandProfileView;
-
- ProfileWidth = ExpandProfileView
- ? new GridLength(SettingsManager.Current.Firewall_ProfileWidth)
- : new GridLength(GlobalStaticConfiguration.Profile_WidthCollapsed);
-
- _tempProfileWidth = SettingsManager.Current.Firewall_ProfileWidth;
- }
-
#endregion
#region ICommand & Actions
@@ -689,159 +449,6 @@ await DialogHelper.ShowMessageAsync(Application.Current.MainWindow, Strings.Erro
return Application.Current.MainWindow.ShowChildWindowAsync(childWindow);
}
- ///
- /// Gets the command to add a new profile.
- ///
- public ICommand AddProfileCommand => new RelayCommand(_ => AddProfileAction());
-
- ///
- /// Action to add a new profile.
- ///
- private void AddProfileAction()
- {
- _ = ProfileDialogManager
- .ShowAddProfileDialog(Application.Current.MainWindow, this, null, null, ApplicationName.Firewall);
- }
-
- ///
- /// Checks if the profile modification commands can be executed.
- ///
- private bool ModifyProfile_CanExecute(object obj)
- {
- return SelectedProfile is { IsDynamic: false };
- }
-
- ///
- /// Gets the command to edit the selected profile.
- ///
- public ICommand EditProfileCommand => new RelayCommand(_ => EditProfileAction(), ModifyProfile_CanExecute);
-
- ///
- /// Action to edit the selected profile.
- ///
- private void EditProfileAction()
- {
- _ = ProfileDialogManager.ShowEditProfileDialog(Application.Current.MainWindow, this, SelectedProfile);
- }
-
- ///
- /// Action to copy the selected profile as a new profile.
- ///
- public ICommand CopyAsProfileCommand => new RelayCommand(_ => CopyAsProfileAction(), ModifyProfile_CanExecute);
-
- ///
- /// Action to copy the selected profile as a new profile.
- ///
- private void CopyAsProfileAction()
- {
- _ = ProfileDialogManager.ShowCopyAsProfileDialog(Application.Current.MainWindow, this, SelectedProfile);
- }
-
- ///
- /// Gets the command to delete the selected profile.
- ///
- public ICommand DeleteProfileCommand => new RelayCommand(_ => DeleteProfileAction(), ModifyProfile_CanExecute);
-
- ///
- /// Action to delete the selected profile.
- ///
- private void DeleteProfileAction()
- {
- _ = ProfileDialogManager
- .ShowDeleteProfileDialog(Application.Current.MainWindow, this, new List { SelectedProfile });
- }
-
- ///
- /// Gets the command to edit a profile group.
- ///
- public ICommand EditGroupCommand => new RelayCommand(EditGroupAction);
-
- ///
- /// Action to edit a profile group.
- ///
- private void EditGroupAction(object group)
- {
- _ = ProfileDialogManager
- .ShowEditGroupDialog(Application.Current.MainWindow, this, ProfileManager.GetGroupByName($"{group}"));
- }
-
- ///
- /// Gets the command to open the profile filter.
- ///
- public ICommand OpenProfileFilterCommand => new RelayCommand(_ => OpenProfileFilterAction());
-
- ///
- /// Action to open the profile filter.
- ///
- private void OpenProfileFilterAction()
- {
- ProfileFilterIsOpen = true;
- }
-
- ///
- /// Gets the command to apply the profile filter.
- ///
- public ICommand ApplyProfileFilterCommand => new RelayCommand(_ => ApplyProfileFilterAction());
-
- ///
- /// Action to apply the profile filter.
- ///
- private void ApplyProfileFilterAction()
- {
- RefreshProfiles();
-
- ProfileFilterIsOpen = false;
- }
-
- ///
- /// Gets the command to clear the profile filter.
- ///
- public ICommand ClearProfileFilterCommand => new RelayCommand(_ => ClearProfileFilterAction());
-
- ///
- /// Action to clear the profile filter.
- ///
- private void ClearProfileFilterAction()
- {
- _searchDisabled = true;
- Search = string.Empty;
- _searchDisabled = false;
-
- foreach (var tag in ProfileFilterTags)
- tag.IsSelected = false;
-
- RefreshProfiles();
-
- IsProfileFilterSet = false;
- ProfileFilterIsOpen = false;
- }
-
- ///
- /// Gets the command to expand all profile groups.
- ///
- public ICommand ExpandAllProfileGroupsCommand => new RelayCommand(_ => ExpandAllProfileGroupsAction());
-
- ///
- /// Action to expand all profile groups.
- ///
- private void ExpandAllProfileGroupsAction()
- {
- SetIsExpandedForAllProfileGroups(true);
- }
-
- ///
- /// Gets the command to collapse all profile groups.
- ///
- public ICommand CollapseAllProfileGroupsCommand => new RelayCommand(_ => CollapseAllProfileGroupsAction());
-
- ///
- /// Action to collapse all profile groups.
- ///
- private void CollapseAllProfileGroupsAction()
- {
- SetIsExpandedForAllProfileGroups(false);
- }
-
#region Additional commands
///
@@ -984,57 +591,11 @@ private static List ParseAddressesString(string value)
return [.. value.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)];
}
- ///
- /// Sets the IsExpanded property for all profile groups.
- ///
- /// The value to set.
- private void SetIsExpandedForAllProfileGroups(bool isExpanded)
- {
- foreach (var group in Profiles.Groups.Cast())
- GroupExpanderStateStore[group.Name.ToString()] = isExpanded;
- }
-
- ///
- /// Resizes the profile view.
- ///
- /// Indicates whether the resize is due to a size change.
- private void ResizeProfile(bool dueToChangedSize)
- {
- _canProfileWidthChange = false;
-
- if (dueToChangedSize)
- {
- ExpandProfileView = Math.Abs(ProfileWidth.Value - GlobalStaticConfiguration.Profile_WidthCollapsed) >
- GlobalStaticConfiguration.FloatPointFix;
- }
- else
- {
- if (ExpandProfileView)
- {
- ProfileWidth =
- Math.Abs(_tempProfileWidth - GlobalStaticConfiguration.Profile_WidthCollapsed) <
- GlobalStaticConfiguration.FloatPointFix
- ? new GridLength(GlobalStaticConfiguration.Profile_DefaultWidthExpanded)
- : new GridLength(_tempProfileWidth);
- }
- else
- {
- _tempProfileWidth = ProfileWidth.Value;
- ProfileWidth = new GridLength(GlobalStaticConfiguration.Profile_WidthCollapsed);
- }
- }
-
- _canProfileWidthChange = true;
- }
-
///
/// Called when the view becomes visible.
///
public void OnViewVisible()
{
- _isViewActive = true;
-
- RefreshProfiles();
}
///
@@ -1042,111 +603,6 @@ public void OnViewVisible()
///
public void OnViewHide()
{
- _isViewActive = false;
- }
-
- ///
- /// Creates the profile filter tags.
- ///
- private void CreateTags()
- {
- var tags = ProfileManager.LoadedProfileFileData.Groups.SelectMany(x => x.Profiles).Where(x => x.Firewall_Enabled)
- .SelectMany(x => x.TagsCollection).Distinct().ToList();
-
- var tagSet = new HashSet(tags);
-
- for (var i = ProfileFilterTags.Count - 1; i >= 0; i--)
- {
- if (!tagSet.Contains(ProfileFilterTags[i].Name))
- ProfileFilterTags.RemoveAt(i);
- }
-
- var existingTagNames = new HashSet(ProfileFilterTags.Select(ft => ft.Name));
-
- foreach (var tag in tags.Where(tag => !existingTagNames.Contains(tag)))
- {
- ProfileFilterTags.Add(new ProfileFilterTagsInfo(false, tag));
- }
- }
-
- ///
- /// Sets the profiles view with the specified filter.
- ///
- /// The profile filter.
- /// The profile to select.
- private void SetProfilesView(ProfileFilterInfo filter, ProfileInfo profile = null)
- {
- Profiles = new CollectionViewSource
- {
- Source = ProfileManager.LoadedProfileFileData.Groups.SelectMany(x => x.Profiles).Where(x => x.Firewall_Enabled && (
- string.IsNullOrEmpty(filter.Search) ||
- x.Name.IndexOf(filter.Search, StringComparison.OrdinalIgnoreCase) > -1) && (
- // If no tags are selected, show all profiles
- (!filter.Tags.Any()) ||
- // Any tag can match
- (filter.TagsFilterMatch == ProfileFilterTagsMatch.Any &&
- filter.Tags.Any(tag => x.TagsCollection.Contains(tag))) ||
- // All tags must match
- (filter.TagsFilterMatch == ProfileFilterTagsMatch.All &&
- filter.Tags.All(tag => x.TagsCollection.Contains(tag))))
- ).OrderBy(x => x.Group).ThenBy(x => x.Name)
- }.View;
-
- Profiles.GroupDescriptions.Add(new PropertyGroupDescription(nameof(ProfileInfo.Group)));
-
- // Set specific profile or first if null
- SelectedProfile = null;
-
- if (profile != null)
- SelectedProfile = Profiles.Cast().FirstOrDefault(x => x.Equals(profile)) ??
- Profiles.Cast().FirstOrDefault();
- else
- SelectedProfile = Profiles.Cast().FirstOrDefault();
- }
-
- ///
- /// Refreshes the profiles.
- ///
- private void RefreshProfiles()
- {
- if (!_isViewActive)
- return;
-
- var filter = new ProfileFilterInfo
- {
- Search = Search,
- Tags = [.. ProfileFilterTags.Where(x => x.IsSelected).Select(x => x.Name)],
- TagsFilterMatch = ProfileFilterTagsMatchAny ? ProfileFilterTagsMatch.Any : ProfileFilterTagsMatch.All
- };
-
- SetProfilesView(filter, SelectedProfile);
-
- IsProfileFilterSet = !string.IsNullOrEmpty(filter.Search) || filter.Tags.Any();
- }
- #endregion
-
- #region Events
-
- ///
- /// Handles the OnProfilesUpdated event of the ProfileManager.
- ///
- private void ProfileManager_OnProfilesUpdated(object sender, EventArgs e)
- {
- CreateTags();
-
- RefreshProfiles();
- }
-
- ///
- /// Handles the Tick event of the search dispatcher timer.
- ///
- private void SearchDispatcherTimer_Tick(object sender, EventArgs e)
- {
- _searchDispatcherTimer.Stop();
-
- RefreshProfiles();
-
- IsSearching = false;
}
#endregion
diff --git a/Source/NETworkManager/ViewModels/ProfileHostViewModelBase.cs b/Source/NETworkManager/ViewModels/ProfileHostViewModelBase.cs
index 1a55ab14df..b8c1d66bf1 100644
--- a/Source/NETworkManager/ViewModels/ProfileHostViewModelBase.cs
+++ b/Source/NETworkManager/ViewModels/ProfileHostViewModelBase.cs
@@ -204,6 +204,8 @@ public bool IsProfileFilterSet
///
protected void InitializeProfileHost()
{
+ LoadGroupExpandState();
+
CreateTags();
ProfileFilterTagsView = CollectionViewSource.GetDefaultView(ProfileFilterTags);
@@ -213,6 +215,7 @@ protected void InitializeProfileHost()
SetProfilesView(new ProfileFilterInfo());
ProfileManager.OnProfilesUpdated += ProfileManager_OnProfilesUpdated;
+ GroupExpanderStateStore.GroupExpandedChanged += GroupExpanderStateStore_GroupExpandedChanged;
_searchDispatcherTimer.Interval = GlobalStaticConfiguration.SearchDispatcherTimerTimeSpan;
_searchDispatcherTimer.Tick += SearchDispatcherTimer_Tick;
@@ -352,6 +355,22 @@ private void SetIsExpandedForAllProfileGroups(bool isExpanded)
GroupExpanderStateStore[group.Name.ToString()] = isExpanded;
}
+ ///
+ /// Loads the persisted group-expand state for the currently loaded profile file into
+ /// . Always called, even with no persisted state, so that
+ /// switching to a different profile file doesn't leak state for a same-named group.
+ ///
+ private void LoadGroupExpandState()
+ {
+ Dictionary state = null;
+
+ if (ProfileManager.LoadedProfileFile != null)
+ SettingsManager.Current.Profiles_GroupExpandState.TryGetValue(
+ ProfileManager.LoadedProfileFile.Path, out state);
+
+ GroupExpanderStateStore.LoadState(state);
+ }
+
///
/// Called when the view becomes visible.
///
@@ -359,6 +378,8 @@ public virtual void OnViewVisible()
{
_isViewActive = true;
+ LoadGroupExpandState();
+
RefreshProfiles();
}
@@ -466,6 +487,23 @@ private void ProfileManager_OnProfilesUpdated(object sender, EventArgs e)
RefreshProfiles();
}
+ private void GroupExpanderStateStore_GroupExpandedChanged(object sender, GroupExpandedChangedArgs e)
+ {
+ if (ProfileManager.LoadedProfileFile == null)
+ return;
+
+ var path = ProfileManager.LoadedProfileFile.Path;
+
+ if (!SettingsManager.Current.Profiles_GroupExpandState.TryGetValue(path, out var state))
+ {
+ state = new Dictionary();
+ SettingsManager.Current.Profiles_GroupExpandState[path] = state;
+ }
+
+ state[e.GroupName] = e.IsExpanded;
+ SettingsManager.Current.SettingsChanged = true;
+ }
+
private void SearchDispatcherTimer_Tick(object sender, EventArgs e)
{
_searchDispatcherTimer.Stop();
diff --git a/Source/NETworkManager/ViewModels/ProfileViewModel.cs b/Source/NETworkManager/ViewModels/ProfileViewModel.cs
index 29b84def39..4c6ea972fb 100644
--- a/Source/NETworkManager/ViewModels/ProfileViewModel.cs
+++ b/Source/NETworkManager/ViewModels/ProfileViewModel.cs
@@ -308,11 +308,6 @@ public ProfileViewModel(Action saveCommand, Action x == profileInfo.SNMP_PrivacyProvider);
SNMP_Priv = profileInfo.SNMP_Priv;
- // Firewall
- Firewall_Enabled = editMode == ProfileEditMode.Add
- ? applicationName == ApplicationName.Firewall
- : profileInfo.Firewall_Enabled;
-
// Wake on LAN
WakeOnLAN_Enabled = editMode == ProfileEditMode.Add
? applicationName == ApplicationName.WakeOnLAN
@@ -2755,24 +2750,6 @@ public SecureString SNMP_Priv
#endregion
- #region Firewall
-
- public bool Firewall_Enabled
- {
- get;
- set
- {
- if (value == field)
- return;
-
- field = value;
- OnPropertyChanged();
- OnPropertyChanged(nameof(Name));
- }
- }
-
- #endregion Firewall
-
#region Wake on LAN
public bool WakeOnLAN_Enabled
diff --git a/Website/docs/changelog/next-release.md b/Website/docs/changelog/next-release.md
index 2df41043a1..839fd794a1 100644
--- a/Website/docs/changelog/next-release.md
+++ b/Website/docs/changelog/next-release.md
@@ -27,6 +27,8 @@ Release date: **xx.xx.2026**
## Improvements
+- The collapsed/expanded state of profile groups (e.g. **linux-server**) is now remembered per profile file and shared across all tools, instead of resetting every time you switch tools or restart the application. [#3539](https://github.com/BornToBeRoot/NETworkManager/pull/3539)
+
## Bug Fixes
## Dependencies, Refactoring & Documentation