[Hosts]Warn about duplicated entries (#22606)

* find duplicated entries

* addressed PR feedback

Co-authored-by: Davide <25966642+davidegiacometti@users.noreply.github.com>
This commit is contained in:
Davide Giacometti
2022-12-14 16:52:00 +01:00
committed by GitHub
parent b56e62e5de
commit 5b4e678f14
4 changed files with 134 additions and 59 deletions

View File

@@ -35,6 +35,7 @@ namespace Hosts.Models
{ {
SetProperty(ref _hosts, value); SetProperty(ref _hosts, value);
OnPropertyChanged(nameof(Valid)); OnPropertyChanged(nameof(Valid));
SplittedHosts = _hosts.Split(' ');
} }
} }
@@ -50,8 +51,13 @@ namespace Hosts.Models
[ObservableProperty] [ObservableProperty]
private bool _pinging; private bool _pinging;
[ObservableProperty]
private bool _duplicate;
public bool Valid => ValidationHelper.ValidHosts(_hosts) && (ValidationHelper.ValidIPv4(_address) || ValidationHelper.ValidIPv6(_address)); public bool Valid => ValidationHelper.ValidHosts(_hosts) && (ValidationHelper.ValidIPv4(_address) || ValidationHelper.ValidIPv6(_address));
public string[] SplittedHosts { get; private set; }
public Entry() public Entry()
{ {
} }

View File

@@ -184,6 +184,9 @@
<data name="DeleteDialogAreYouSure.Text" xml:space="preserve"> <data name="DeleteDialogAreYouSure.Text" xml:space="preserve">
<value>Are you sure you want to delete this entry?</value> <value>Are you sure you want to delete this entry?</value>
</data> </data>
<data name="DuplicateEntryIcon.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Duplicate entry</value>
</data>
<data name="Entries.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve"> <data name="Entries.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Entries</value> <value>Entries</value>
</data> </data>
@@ -234,6 +237,10 @@
<value>Ping</value> <value>Ping</value>
<comment>"Ping" refers to the command-line utility, do not loc</comment> <comment>"Ping" refers to the command-line utility, do not loc</comment>
</data> </data>
<data name="PingIcon.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Ping response</value>
<comment>"Ping" refers to the command-line utility, do not loc</comment>
</data>
<data name="Reload.Content" xml:space="preserve"> <data name="Reload.Content" xml:space="preserve">
<value>Reload</value> <value>Reload</value>
</data> </data>
@@ -243,6 +250,9 @@
<data name="SettingsBtn.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve"> <data name="SettingsBtn.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Settings</value> <value>Settings</value>
</data> </data>
<data name="ShowOnlyDuplicates.Header" xml:space="preserve">
<value>Show only duplicates</value>
</data>
<data name="UpdateBtn" xml:space="preserve"> <data name="UpdateBtn" xml:space="preserve">
<value>Update</value> <value>Update</value>
</data> </data>

View File

@@ -1,8 +1,9 @@
// Copyright (c) Microsoft Corporation // Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -13,7 +14,6 @@ using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.WinUI; using CommunityToolkit.WinUI;
using Hosts.Helpers; using Hosts.Helpers;
using Hosts.Models; using Hosts.Models;
using Hosts.Settings;
using Microsoft.UI.Dispatching; using Microsoft.UI.Dispatching;
namespace Hosts.ViewModels namespace Hosts.ViewModels
@@ -21,7 +21,6 @@ namespace Hosts.ViewModels
public partial class MainViewModel : ObservableObject, IDisposable public partial class MainViewModel : ObservableObject, IDisposable
{ {
private readonly IHostsService _hostsService; private readonly IHostsService _hostsService;
private readonly IUserSettings _userSettings;
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
private bool _disposed; private bool _disposed;
@@ -34,6 +33,9 @@ namespace Hosts.ViewModels
[ObservableProperty] [ObservableProperty]
private bool _fileChanged; private bool _fileChanged;
[ObservableProperty]
private bool _filtered;
[ObservableProperty] [ObservableProperty]
private string _addressFilter; private string _addressFilter;
@@ -44,50 +46,15 @@ namespace Hosts.ViewModels
private string _commentFilter; private string _commentFilter;
[ObservableProperty] [ObservableProperty]
private bool _filtered; [NotifyPropertyChangedFor(nameof(Entries))]
private bool _showOnlyDuplicates;
[ObservableProperty] [ObservableProperty]
private string _additionalLines; private string _additionalLines;
private ObservableCollection<Entry> _entries; private ObservableCollection<Entry> _entries;
public ObservableCollection<Entry> Entries public ObservableCollection<Entry> Entries => _filtered || _showOnlyDuplicates ? GetFilteredEntries() : _entries;
{
get
{
if (_filtered)
{
var filter = _entries.AsEnumerable();
if (!string.IsNullOrWhiteSpace(_addressFilter))
{
filter = filter.Where(e => e.Address.Contains(_addressFilter, StringComparison.OrdinalIgnoreCase));
}
if (!string.IsNullOrWhiteSpace(_hostsFilter))
{
filter = filter.Where(e => e.Hosts.Contains(_hostsFilter, StringComparison.OrdinalIgnoreCase));
}
if (!string.IsNullOrWhiteSpace(_commentFilter))
{
filter = filter.Where(e => e.Comment.Contains(_commentFilter, StringComparison.OrdinalIgnoreCase));
}
return new ObservableCollection<Entry>(filter);
}
else
{
return _entries;
}
}
set
{
_entries = value;
OnPropertyChanged(nameof(Entries));
}
}
public ICommand ReadHostsCommand => new RelayCommand(ReadHosts); public ICommand ReadHostsCommand => new RelayCommand(ReadHosts);
@@ -99,12 +66,9 @@ namespace Hosts.ViewModels
public ICommand OpenHostsFileCommand => new RelayCommand(OpenHostsFile); public ICommand OpenHostsFileCommand => new RelayCommand(OpenHostsFile);
public MainViewModel( public MainViewModel(IHostsService hostService)
IHostsService hostService,
IUserSettings userSettings)
{ {
_hostsService = hostService; _hostsService = hostService;
_userSettings = userSettings;
_hostsService.FileChanged += (s, e) => _hostsService.FileChanged += (s, e) =>
{ {
@@ -116,24 +80,35 @@ namespace Hosts.ViewModels
{ {
entry.PropertyChanged += Entry_PropertyChanged; entry.PropertyChanged += Entry_PropertyChanged;
_entries.Add(entry); _entries.Add(entry);
FindDuplicates(entry.Address, entry.SplittedHosts);
OnPropertyChanged(nameof(Entries));
} }
public void Update(int index, Entry entry) public void Update(int index, Entry entry)
{ {
var existingEntry = _entries.ElementAt(index); var existingEntry = Entries.ElementAt(index);
var oldAddress = existingEntry.Address;
var oldHosts = existingEntry.SplittedHosts;
existingEntry.Address = entry.Address; existingEntry.Address = entry.Address;
existingEntry.Comment = entry.Comment; existingEntry.Comment = entry.Comment;
existingEntry.Hosts = entry.Hosts; existingEntry.Hosts = entry.Hosts;
existingEntry.Active = entry.Active; existingEntry.Active = entry.Active;
FindDuplicates(oldAddress, oldHosts);
FindDuplicates(entry.Address, entry.SplittedHosts);
OnPropertyChanged(nameof(Entries));
} }
public void DeleteSelected() public void DeleteSelected()
{ {
var address = Selected.Address;
var hosts = Selected.SplittedHosts;
_entries.Remove(Selected); _entries.Remove(Selected);
if (Filtered)
{ FindDuplicates(address, hosts);
OnPropertyChanged(nameof(Entries)); OnPropertyChanged(nameof(Entries));
}
} }
public void UpdateAdditionalLines(string lines) public void UpdateAdditionalLines(string lines)
@@ -157,7 +132,7 @@ namespace Hosts.ViewModels
await _dispatcherQueue.EnqueueAsync(() => await _dispatcherQueue.EnqueueAsync(() =>
{ {
Entries = new ObservableCollection<Entry>(entries); _entries = new ObservableCollection<Entry>(entries);
foreach (var e in _entries) foreach (var e in _entries)
{ {
@@ -165,17 +140,24 @@ namespace Hosts.ViewModels
} }
_entries.CollectionChanged += Entries_CollectionChanged; _entries.CollectionChanged += Entries_CollectionChanged;
OnPropertyChanged(nameof(Entries));
FindDuplicates();
}); });
}); });
} }
public void ApplyFilters() public void ApplyFilters()
{ {
if (_entries != null) if (_entries == null)
{ {
Filtered = !string.IsNullOrWhiteSpace(_addressFilter) || !string.IsNullOrWhiteSpace(_hostsFilter) || !string.IsNullOrWhiteSpace(_commentFilter); return;
OnPropertyChanged(nameof(Entries));
} }
Filtered = !string.IsNullOrWhiteSpace(_addressFilter)
|| !string.IsNullOrWhiteSpace(_hostsFilter)
|| !string.IsNullOrWhiteSpace(_commentFilter);
OnPropertyChanged(nameof(Entries));
} }
public void ClearFilters() public void ClearFilters()
@@ -183,6 +165,7 @@ namespace Hosts.ViewModels
AddressFilter = null; AddressFilter = null;
HostsFilter = null; HostsFilter = null;
CommentFilter = null; CommentFilter = null;
ShowOnlyDuplicates = false;
} }
public async Task PingSelectedAsync() public async Task PingSelectedAsync()
@@ -212,8 +195,10 @@ namespace Hosts.ViewModels
private void Entry_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) private void Entry_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{ {
// Ping should't trigger a file save // Ping and duplicate should't trigger a file save
if (e.PropertyName == nameof(Entry.Ping) || e.PropertyName == nameof(Entry.Pinging)) if (e.PropertyName == nameof(Entry.Ping)
|| e.PropertyName == nameof(Entry.Pinging)
|| e.PropertyName == nameof(Entry.Duplicate))
{ {
return; return;
} }
@@ -234,6 +219,68 @@ namespace Hosts.ViewModels
}); });
} }
private void FindDuplicates()
{
foreach (var entry in _entries)
{
SetDuplicate(entry);
}
}
private void FindDuplicates(string address, IEnumerable<string> hosts)
{
var entries = _entries.Where(e =>
string.Equals(e.Address, address, StringComparison.InvariantCultureIgnoreCase)
|| hosts.Intersect(e.SplittedHosts, StringComparer.InvariantCultureIgnoreCase).Any());
foreach (var entry in entries)
{
SetDuplicate(entry);
}
}
private void SetDuplicate(Entry entry)
{
var hosts = entry.SplittedHosts;
entry.Duplicate = _entries.FirstOrDefault(e =>
e != entry
&& (string.Equals(e.Address, entry.Address, StringComparison.InvariantCultureIgnoreCase)
|| hosts.Intersect(e.SplittedHosts, StringComparer.InvariantCultureIgnoreCase).Any())) != null;
}
private ObservableCollection<Entry> GetFilteredEntries()
{
if (_entries == null)
{
return new ObservableCollection<Entry>();
}
var filter = _entries.AsEnumerable();
if (!string.IsNullOrWhiteSpace(_addressFilter))
{
filter = filter.Where(e => e.Address.Contains(_addressFilter, StringComparison.OrdinalIgnoreCase));
}
if (!string.IsNullOrWhiteSpace(_hostsFilter))
{
filter = filter.Where(e => e.Hosts.Contains(_hostsFilter, StringComparison.OrdinalIgnoreCase));
}
if (!string.IsNullOrWhiteSpace(_commentFilter))
{
filter = filter.Where(e => e.Comment.Contains(_commentFilter, StringComparison.OrdinalIgnoreCase));
}
if (_showOnlyDuplicates)
{
filter = filter.Where(e => e.Duplicate);
}
return new ObservableCollection<Entry>(filter);
}
protected virtual void Dispose(bool disposing) protected virtual void Dispose(bool disposing)
{ {
if (!_disposed) if (!_disposed)

View File

@@ -116,6 +116,9 @@
</ic:EventTriggerBehavior> </ic:EventTriggerBehavior>
</i:Interaction.Behaviors> </i:Interaction.Behaviors>
</AutoSuggestBox> </AutoSuggestBox>
<ToggleSwitch
x:Uid="ShowOnlyDuplicates"
IsOn="{x:Bind ViewModel.ShowOnlyDuplicates, Mode=TwoWay}" />
<Button <Button
x:Uid="ClearFiltersBtn" x:Uid="ClearFiltersBtn"
Margin="0,6,0,0" Margin="0,6,0,0"
@@ -151,7 +154,6 @@
</StackPanel> </StackPanel>
</Grid> </Grid>
<StackPanel <StackPanel
Grid.Row="2" Grid.Row="2"
Orientation="Vertical" Orientation="Vertical"
@@ -205,6 +207,7 @@
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<FlyoutBase.AttachedFlyout> <FlyoutBase.AttachedFlyout>
<MenuFlyout> <MenuFlyout>
@@ -254,6 +257,7 @@
Margin="0,0,8,0" Margin="0,0,8,0"
IsActive="{x:Bind Pinging, Mode=OneWay}" /> IsActive="{x:Bind Pinging, Mode=OneWay}" />
<FontIcon <FontIcon
x:Uid="PingIcon"
x:Name="PingIcon" x:Name="PingIcon"
Grid.Column="2" Grid.Column="2"
Margin="0,0,8,0" Margin="0,0,8,0"
@@ -306,9 +310,18 @@
</ic:DataTriggerBehavior> </ic:DataTriggerBehavior>
</i:Interaction.Behaviors> </i:Interaction.Behaviors>
</FontIcon> </FontIcon>
<FontIcon
x:Uid="DuplicateEntryIcon"
Grid.Column="3"
Margin="0,0,8,0"
Foreground="{StaticResource SystemControlErrorTextForegroundBrush}"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="18"
Glyph="&#xe7BA;"
Visibility="{x:Bind Duplicate, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
<ToggleSwitch <ToggleSwitch
x:Uid="ActiveToggle" x:Uid="ActiveToggle"
Grid.Column="3" Grid.Column="4"
Width="40" Width="40"
MinWidth="0" MinWidth="0"
HorizontalAlignment="Right" HorizontalAlignment="Right"
@@ -384,7 +397,6 @@
ScrollViewer.VerticalScrollBarVisibility="Visible" ScrollViewer.VerticalScrollBarVisibility="Visible"
ScrollViewer.VerticalScrollMode="Enabled" ScrollViewer.VerticalScrollMode="Enabled"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
</ContentDialog> </ContentDialog>
</Grid> </Grid>
</Page> </Page>