[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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 134 additions and 59 deletions

View File

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

View File

@ -184,6 +184,9 @@
<data name="DeleteDialogAreYouSure.Text" xml:space="preserve">
<value>Are you sure you want to delete this entry?</value>
</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">
<value>Entries</value>
</data>
@ -234,6 +237,10 @@
<value>Ping</value>
<comment>"Ping" refers to the command-line utility, do not loc</comment>
</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">
<value>Reload</value>
</data>
@ -243,6 +250,9 @@
<data name="SettingsBtn.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Settings</value>
</data>
<data name="ShowOnlyDuplicates.Header" xml:space="preserve">
<value>Show only duplicates</value>
</data>
<data name="UpdateBtn" xml:space="preserve">
<value>Update</value>
</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.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
@ -13,7 +14,6 @@ using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.WinUI;
using Hosts.Helpers;
using Hosts.Models;
using Hosts.Settings;
using Microsoft.UI.Dispatching;
namespace Hosts.ViewModels
@ -21,7 +21,6 @@ namespace Hosts.ViewModels
public partial class MainViewModel : ObservableObject, IDisposable
{
private readonly IHostsService _hostsService;
private readonly IUserSettings _userSettings;
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
private bool _disposed;
@ -34,6 +33,9 @@ namespace Hosts.ViewModels
[ObservableProperty]
private bool _fileChanged;
[ObservableProperty]
private bool _filtered;
[ObservableProperty]
private string _addressFilter;
@ -44,50 +46,15 @@ namespace Hosts.ViewModels
private string _commentFilter;
[ObservableProperty]
private bool _filtered;
[NotifyPropertyChangedFor(nameof(Entries))]
private bool _showOnlyDuplicates;
[ObservableProperty]
private string _additionalLines;
private ObservableCollection<Entry> _entries;
public ObservableCollection<Entry> 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 ObservableCollection<Entry> Entries => _filtered || _showOnlyDuplicates ? GetFilteredEntries() : _entries;
public ICommand ReadHostsCommand => new RelayCommand(ReadHosts);
@ -99,12 +66,9 @@ namespace Hosts.ViewModels
public ICommand OpenHostsFileCommand => new RelayCommand(OpenHostsFile);
public MainViewModel(
IHostsService hostService,
IUserSettings userSettings)
public MainViewModel(IHostsService hostService)
{
_hostsService = hostService;
_userSettings = userSettings;
_hostsService.FileChanged += (s, e) =>
{
@ -116,24 +80,35 @@ namespace Hosts.ViewModels
{
entry.PropertyChanged += Entry_PropertyChanged;
_entries.Add(entry);
FindDuplicates(entry.Address, entry.SplittedHosts);
OnPropertyChanged(nameof(Entries));
}
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.Comment = entry.Comment;
existingEntry.Hosts = entry.Hosts;
existingEntry.Active = entry.Active;
FindDuplicates(oldAddress, oldHosts);
FindDuplicates(entry.Address, entry.SplittedHosts);
OnPropertyChanged(nameof(Entries));
}
public void DeleteSelected()
{
var address = Selected.Address;
var hosts = Selected.SplittedHosts;
_entries.Remove(Selected);
if (Filtered)
{
OnPropertyChanged(nameof(Entries));
}
FindDuplicates(address, hosts);
OnPropertyChanged(nameof(Entries));
}
public void UpdateAdditionalLines(string lines)
@ -157,7 +132,7 @@ namespace Hosts.ViewModels
await _dispatcherQueue.EnqueueAsync(() =>
{
Entries = new ObservableCollection<Entry>(entries);
_entries = new ObservableCollection<Entry>(entries);
foreach (var e in _entries)
{
@ -165,17 +140,24 @@ namespace Hosts.ViewModels
}
_entries.CollectionChanged += Entries_CollectionChanged;
OnPropertyChanged(nameof(Entries));
FindDuplicates();
});
});
}
public void ApplyFilters()
{
if (_entries != null)
if (_entries == null)
{
Filtered = !string.IsNullOrWhiteSpace(_addressFilter) || !string.IsNullOrWhiteSpace(_hostsFilter) || !string.IsNullOrWhiteSpace(_commentFilter);
OnPropertyChanged(nameof(Entries));
return;
}
Filtered = !string.IsNullOrWhiteSpace(_addressFilter)
|| !string.IsNullOrWhiteSpace(_hostsFilter)
|| !string.IsNullOrWhiteSpace(_commentFilter);
OnPropertyChanged(nameof(Entries));
}
public void ClearFilters()
@ -183,6 +165,7 @@ namespace Hosts.ViewModels
AddressFilter = null;
HostsFilter = null;
CommentFilter = null;
ShowOnlyDuplicates = false;
}
public async Task PingSelectedAsync()
@ -212,8 +195,10 @@ namespace Hosts.ViewModels
private void Entry_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
// Ping should't trigger a file save
if (e.PropertyName == nameof(Entry.Ping) || e.PropertyName == nameof(Entry.Pinging))
// Ping and duplicate should't trigger a file save
if (e.PropertyName == nameof(Entry.Ping)
|| e.PropertyName == nameof(Entry.Pinging)
|| e.PropertyName == nameof(Entry.Duplicate))
{
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)
{
if (!_disposed)

View File

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