Skip to content

Commit 0dc743a

Browse files
authored
Audit Sources Changed event triggers Package Manager UI to refresh (#7022)
1 parent ac7b8f7 commit 0dc743a

5 files changed

Lines changed: 48 additions & 115 deletions

File tree

src/NuGet.Clients/NuGet.PackageManagement.UI/UserInterfaceService/NuGetSourcesServiceWrapper.cs

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ internal sealed class NuGetSourcesServiceWrapper : INuGetSourcesService
1515
private INuGetSourcesService _service = NullNuGetSourcesService.Instance;
1616
private readonly object _syncObject = new object();
1717

18-
public event EventHandler<IReadOnlyList<PackageSourceContextInfo>>? PackageSourcesChanged;
19-
2018
internal INuGetSourcesService Service
2119
{
2220
get
@@ -32,23 +30,14 @@ public INuGetSourcesService Swap(INuGetSourcesService newService)
3230
{
3331
lock (_syncObject)
3432
{
35-
Service.PackageSourcesChanged -= OnPackageSourcesChanged;
36-
3733
INuGetSourcesService oldService = _service;
3834

3935
_service = newService ?? NullNuGetSourcesService.Instance;
4036

41-
Service.PackageSourcesChanged += OnPackageSourcesChanged;
42-
4337
return oldService;
4438
}
4539
}
4640

47-
private void OnPackageSourcesChanged(object sender, IReadOnlyList<PackageSourceContextInfo> e)
48-
{
49-
PackageSourcesChanged?.Invoke(sender, e);
50-
}
51-
5241
public void Dispose()
5342
{
5443
lock (_syncObject)
@@ -84,8 +73,6 @@ public IReadOnlyList<SourceRepository> GetEnabledAuditSources()
8473

8574
private sealed class NullNuGetSourcesService : INuGetSourcesService
8675
{
87-
public event EventHandler<IReadOnlyList<PackageSourceContextInfo>>? PackageSourcesChanged { add { } remove { } }
88-
8976
internal static NullNuGetSourcesService Instance { get; } = new NullNuGetSourcesService();
9077

9178
public void Dispose() { }

src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/PackageManagerControl.xaml.cs

Lines changed: 48 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,13 @@ public partial class PackageManagerControl : UserControl, IVsWindowSearch, IDisp
5252
private bool _initialized;
5353
private IVsWindowSearchHost _windowSearchHost;
5454
private IVsWindowSearchHostFactory _windowSearchHostFactory;
55+
private ISourceRepositoryProvider _sourceRepositoryProvider;
5556
private INuGetUILogger _uiLogger;
5657
private readonly Guid _sessionGuid = Guid.NewGuid();
5758
private Stopwatch _sinceLastRefresh;
5859
private CancellationTokenSource _refreshCts;
5960
// used to prevent starting new search when we update the package sources
60-
// list in response to PackageSourcesChanged event.
61+
// list in response to Package Sources changing events.
6162
private bool _dontStartNewSearch;
6263
// When executing a UI operation, we disable the PM UI and ignore any refresh requests.
6364
// This tells the operation execution part that it needs to trigger a refresh when done.
@@ -115,7 +116,7 @@ private async ValueTask InitializeAsync(PackageManagerModel model, INuGetUILogge
115116
var nuGetFeatureFlagService = await ServiceLocator.GetComponentModelServiceAsync<INuGetFeatureFlagService>();
116117
var editorOptionsFactoryService = await ServiceLocator.GetComponentModelServiceAsync<IEditorOptionsFactoryService>();
117118
NuGetExperimentationService = await ServiceLocator.GetComponentModelServiceAsync<INuGetExperimentationService>();
118-
ISourceRepositoryProvider sourceRepositoryProvider = await ServiceLocator.GetComponentModelServiceAsync<ISourceRepositoryProvider>();
119+
_sourceRepositoryProvider = await ServiceLocator.GetComponentModelServiceAsync<ISourceRepositoryProvider>();
119120
await NuGetUIThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
120121

121122
_serviceBroker = model.Context.ServiceBroker;
@@ -199,10 +200,7 @@ private async ValueTask InitializeAsync(PackageManagerModel model, INuGetUILogge
199200
controller.PackageManagerControl = this;
200201
}
201202

202-
List<SourceRepository> sourceRepositories = sourceRepositoryProvider.GetRepositories().ToList();
203-
204-
var auditSourceRepositories = Model.Context.SourceService.GetEnabledAuditSources();
205-
_packageVulnerabilityService = new PackageVulnerabilityService(sourceRepositories, auditSourceRepositories, _uiLogger);
203+
await SetVulnerabilityService(_sourceRepositoryProvider);
206204

207205
var solutionManager = Model.Context.SolutionManagerService;
208206
solutionManager.ProjectAdded += OnProjectChanged;
@@ -213,8 +211,6 @@ private async ValueTask InitializeAsync(PackageManagerModel model, INuGetUILogge
213211

214212
Model.Context.ProjectActionsExecuted += OnProjectActionsExecuted;
215213

216-
Model.Context.SourceService.PackageSourcesChanged += PackageSourcesChanged;
217-
218214
Unloaded += PackageManagerUnloaded;
219215

220216
if (IsUILegalDisclaimerSuppressed())
@@ -227,10 +223,35 @@ private async ValueTask InitializeAsync(PackageManagerModel model, INuGetUILogge
227223
Settings.SettingsChanged += Settings_SettingsChanged;
228224
}
229225

226+
private async Task SetVulnerabilityService(ISourceRepositoryProvider sourceRepositoryProvider)
227+
{
228+
await TaskScheduler.Default;
229+
List<SourceRepository> sourceRepositories = sourceRepositoryProvider.GetRepositories().ToList();
230+
var auditSourceRepositories = Model.Context.SourceService.GetEnabledAuditSources();
231+
var vulnerabilityService = new PackageVulnerabilityService(sourceRepositories, auditSourceRepositories, _uiLogger);
232+
233+
// Avoid concurrency issues by using the UI thread to synchronize reading and changing _packageVulnerabilityService.
234+
await NuGetUIThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
235+
_packageVulnerabilityService = vulnerabilityService;
236+
}
237+
230238
private void Settings_SettingsChanged(object sender, EventArgs e)
231239
{
232-
_detailModel.PackageSourceMappingViewModel.SettingsChanged();
233-
_detailModel.SetInstalledOrUpdateButtonIsEnabled();
240+
// Set _dontStartNewSearch to true to prevent a new search started in
241+
// _sourceRepoList_SelectionChanged(). This method will start the new
242+
// search when needed by itself.
243+
_dontStartNewSearch = true;
244+
245+
try
246+
{
247+
_detailModel.PackageSourceMappingViewModel.SettingsChanged();
248+
_detailModel.SetInstalledOrUpdateButtonIsEnabled();
249+
RefreshAfterSettingsChanged(sender, e);
250+
}
251+
finally
252+
{
253+
_dontStartNewSearch = false;
254+
}
234255
}
235256

236257
public PackageRestoreBar RestoreBar { get; private set; }
@@ -604,50 +625,22 @@ private void ApplySettings(UserSettings settings, ISettings nugetSettings)
604625
}
605626
}
606627

607-
private void PackageSourcesChanged(object sender, IReadOnlyCollection<PackageSourceContextInfo> e)
628+
private void RefreshAfterSettingsChanged(object sender, EventArgs e)
608629
{
609-
// Set _dontStartNewSearch to true to prevent a new search started in
610-
// _sourceRepoList_SelectionChanged(). This method will start the new
611-
// search when needed by itself.
612-
_dontStartNewSearch = true;
613-
TimeSpan timeSpan = GetTimeSinceLastRefreshAndRestart();
614-
615-
NuGetUIThreadHelper.JoinableTaskFactory.RunAsync(() => PackageSourcesChangedAsync(e, timeSpan))
616-
.PostOnFailure(nameof(PackageManagerControl), nameof(PackageSourcesChanged));
617-
}
618-
619-
private async Task PackageSourcesChangedAsync(IReadOnlyCollection<PackageSourceContextInfo> packageSources, TimeSpan timeSpan)
620-
{
621-
try
630+
NuGetUIThreadHelper.JoinableTaskFactory.RunAsync(async () =>
622631
{
623-
var sw = Stopwatch.StartNew();
624-
IReadOnlyCollection<PackageSourceMoniker> list = await PackageSourceMoniker.PopulateListAsync(packageSources, CancellationToken.None);
632+
await SetVulnerabilityService(_sourceRepositoryProvider);
625633

626634
await NuGetUIThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
627-
// We access UI components in these calls
628-
PackageSourceMoniker prevSelectedItem = SelectedSource;
635+
var settings = SaveSettings();
629636

630-
await PopulatePackageSourcesAsync(list, optionalSelectSourceName: null, cancellationToken: CancellationToken.None);
637+
// Re-initialize package sources first, since a change to them will affect package search results.
638+
await InitPackageSourcesAsync(settings, CancellationToken.None);
631639

632-
// force a new search explicitly only if active source has changed
633-
if (prevSelectedItem == SelectedSource)
634-
{
635-
sw.Stop();
636-
EmitRefreshEvent(timeSpan, RefreshOperationSource.PackageSourcesChanged, RefreshOperationStatus.NotApplicable, isUIFiltering: false, duration: sw.Elapsed.TotalMilliseconds);
637-
}
638-
else
639-
{
640-
await RunAndEmitRefreshAsync(async () =>
641-
{
642-
SaveSettings();
643-
await SearchPackagesAndRefreshUpdateCountAsync(useCacheForUpdates: false);
644-
}, RefreshOperationSource.PackageSourcesChanged, timeSpan, sw);
645-
}
646-
}
647-
finally
648-
{
649-
_dontStartNewSearch = false;
650-
}
640+
// Refresh package search results, which will also reflect any NuGet Audit settings changes.
641+
await SearchPackagesAndRefreshUpdateCountAsync(useCacheForUpdates: false);
642+
})
643+
.PostOnFailure(nameof(PackageManagerControl), nameof(RefreshAfterSettingsChanged));
651644
}
652645

653646
private async Task IsCentralPackageManagementEnabledAsync(CancellationToken cancellationToken)
@@ -703,7 +696,7 @@ private static string GetProjectSettingsKey(string projectName)
703696
// Save the settings of this doc window in the UIContext. Note that the settings
704697
// are not guaranteed to be persisted. We need to call Model.Context.SaveSettings()
705698
// to persist the settings.
706-
public void SaveSettings()
699+
public UserSettings SaveSettings()
707700
{
708701
Assumes.NotNullOrEmpty(_settingsKey);
709702

@@ -724,6 +717,8 @@ public void SaveSettings()
724717
_packageDetail._solutionView.SaveSettings(settings);
725718

726719
Model.Context.UserSettingsManager.AddSettings(_settingsKey, settings);
720+
721+
return settings;
727722
}
728723

729724
private UserSettings LoadSettings()
@@ -1033,7 +1028,7 @@ private async ValueTask RefreshInstalledAndUpdatesTabsAsync()
10331028
Interlocked.Exchange(ref _refreshCts, refreshCts)?.Cancel();
10341029

10351030
// Update installed tab warning icon
1036-
(int vulnerablePackages, int deprecatedPackages) = await GetInstalledVulnerableAndDeprecatedPackagesCountAsync(loadContext, SelectedSource.PackageSources, refreshCts.Token);
1031+
(int vulnerablePackages, int deprecatedPackages) = await GetInstalledVulnerableAndDeprecatedPackagesCountAsync(loadContext, SelectedSource.PackageSources, _packageVulnerabilityService, refreshCts.Token);
10371032
_topPanel.UpdateWarningStatusOnInstalledTab(vulnerablePackages, deprecatedPackages);
10381033

10391034
// Update updates tab count
@@ -1047,7 +1042,7 @@ private async ValueTask RefreshInstalledAndUpdatesTabsAsync()
10471042
_topPanel.UpdateCountOnUpdatesTab(Model.CachedUpdates.Packages.Count);
10481043
}
10491044

1050-
private async Task<(int, int)> GetInstalledVulnerableAndDeprecatedPackagesCountAsync(PackageLoadContext loadContext, IReadOnlyCollection<PackageSourceContextInfo> packageSources, CancellationToken token)
1045+
private async Task<(int, int)> GetInstalledVulnerableAndDeprecatedPackagesCountAsync(PackageLoadContext loadContext, IReadOnlyCollection<PackageSourceContextInfo> packageSources, IPackageVulnerabilityService vulnerabilityService, CancellationToken token)
10511046
{
10521047
// Switch off the UI thread before fetching installed packages and deprecation metadata.
10531048
await TaskScheduler.Default;
@@ -1060,7 +1055,7 @@ private async ValueTask RefreshInstalledAndUpdatesTabsAsync()
10601055
installedPackageCollection = PackageCollection.FromPackageReferences(installedAndTransitivePackages.InstalledPackages);
10611056
PackageCollection transitivePackageCollection = PackageCollection.FromPackageReferences(installedAndTransitivePackages.TransitivePackages.Where(p => p.TransitiveOrigins.Any()));
10621057
//Use ShutdownToken to ensure the operation is canceled if it's still running when VS shuts down.
1063-
IEnumerable<PackageVulnerabilityMetadataContextInfo>[] transitivePackageVulnerabilities = await Task.WhenAll(transitivePackageCollection.Select(p => _packageVulnerabilityService.GetVulnerabilityInfoAsync(p, VsShellUtilities.ShutdownToken)));
1058+
IEnumerable<PackageVulnerabilityMetadataContextInfo>[] transitivePackageVulnerabilities = await Task.WhenAll(transitivePackageCollection.Select(p => vulnerabilityService.GetVulnerabilityInfoAsync(p, VsShellUtilities.ShutdownToken)));
10641059

10651060
foreach (IEnumerable<PackageVulnerabilityMetadataContextInfo> vulnerabilityInfo in transitivePackageVulnerabilities)
10661061
{
@@ -1612,8 +1607,6 @@ private void CleanUp()
16121607

16131608
Model.Context.ProjectActionsExecuted -= OnProjectActionsExecuted;
16141609

1615-
Model.Context.SourceService.PackageSourcesChanged -= PackageSourcesChanged;
1616-
16171610
Settings.SettingsChanged -= Settings_SettingsChanged;
16181611

16191612
Model.Dispose();
@@ -1806,6 +1799,7 @@ private void ExecuteSearchPackageCommand(object sender, ExecutedRoutedEventArgs
18061799

18071800
private async Task ExecuteRestartSearchCommandAsync()
18081801
{
1802+
await NuGetUIThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
18091803
_packageVulnerabilityService?.ResetVulnerabilityData();
18101804
await SearchPackagesAndRefreshUpdateCountAsync(useCacheForUpdates: false);
18111805
await RefreshConsolidatablePackagesCountAsync();

src/NuGet.Clients/NuGet.PackageManagement.VisualStudio/Services/NuGetSourcesService.cs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ public sealed class NuGetSourcesService : INuGetSourcesService
2323
private readonly AuthorizationServiceClient _authorizationServiceClient;
2424
private readonly IPackageSourceProvider _packageSourceProvider;
2525

26-
public event EventHandler<IReadOnlyList<PackageSourceContextInfo>>? PackageSourcesChanged;
27-
2826
public NuGetSourcesService(
2927
ServiceActivationOptions options,
3028
IServiceBroker serviceBroker,
@@ -39,7 +37,6 @@ public NuGetSourcesService(
3937
_serviceBroker = serviceBroker;
4038
_authorizationServiceClient = authorizationServiceClient;
4139
_packageSourceProvider = packageSourceProvider;
42-
_packageSourceProvider.PackageSourcesChanged += PackageSourceProvider_PackageSourcesChanged;
4340
}
4441

4542
public ValueTask<IReadOnlyList<PackageSourceContextInfo>> GetPackageSourcesAsync(CancellationToken cancellationToken)
@@ -84,17 +81,10 @@ public ValueTask SavePackageSourceContextInfosAsync(IReadOnlyList<PackageSourceC
8481

8582
public void Dispose()
8683
{
87-
_packageSourceProvider.PackageSourcesChanged -= PackageSourceProvider_PackageSourcesChanged;
8884
_authorizationServiceClient.Dispose();
8985
GC.SuppressFinalize(this);
9086
}
9187

92-
private void PackageSourceProvider_PackageSourcesChanged(object sender, EventArgs e)
93-
{
94-
List<PackageSourceContextInfo> packageSources = _packageSourceProvider.LoadPackageSources().Select(PackageSourceContextInfo.Create).ToList();
95-
PackageSourcesChanged?.Invoke(this, packageSources);
96-
}
97-
9888
private IReadOnlyList<PackageSource> GetPackageSourcesToUpdate(IReadOnlyList<PackageSourceContextInfo> packageSourceContextInfos)
9989
{
10090
Dictionary<string, PackageSource>? packageSources = _packageSourceProvider.LoadPackageSources()

src/NuGet.Clients/NuGet.VisualStudio.Internal.Contracts/INuGetSourcesService.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,6 @@ namespace NuGet.VisualStudio.Internal.Contracts
1111
{
1212
public interface INuGetSourcesService : IDisposable
1313
{
14-
/// <remarks> First available in version 1.0.1 </remarks>
15-
event EventHandler<IReadOnlyList<PackageSourceContextInfo>>? PackageSourcesChanged;
16-
1714
/// <remarks> First available in version 1.0.1 </remarks>
1815
ValueTask<string?> GetActivePackageSourceNameAsync(CancellationToken cancellationToken);
1916

test/NuGet.Clients.Tests/NuGet.PackageManagement.UI.Test/NuGetSourcesServiceWrapperTests.cs

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -57,34 +57,6 @@ public void Swap_WhenNewServiceIsNonNull_ReturnsPreviousService()
5757
}
5858
}
5959

60-
[Fact]
61-
public void PackageSourcesChanged_Always_ForwardsEvent()
62-
{
63-
var service = new TestNuGetSourcesService();
64-
var packageSources = new List<PackageSourceContextInfo>
65-
{
66-
new PackageSourceContextInfo("a")
67-
};
68-
69-
using (_wrapper.Swap(service))
70-
{
71-
}
72-
73-
var eventRaised = false;
74-
75-
_wrapper.PackageSourcesChanged += (sender, e) =>
76-
{
77-
Assert.Same(packageSources, e);
78-
79-
eventRaised = true;
80-
};
81-
82-
service.RaisePackageSourcesChanged(packageSources);
83-
84-
Assert.True(eventRaised);
85-
}
86-
87-
8860
[Fact]
8961
public async Task GetActivePackageSourceNameAsync_Always_ReturnsActivePackageSourceName()
9062
{
@@ -104,20 +76,13 @@ public async Task GetActivePackageSourceNameAsync_Always_ReturnsActivePackageSou
10476

10577
private sealed class TestNuGetSourcesService : INuGetSourcesService
10678
{
107-
public event EventHandler<IReadOnlyList<PackageSourceContextInfo>> PackageSourcesChanged;
108-
10979
internal string ActivePackageSourceName { get; set; }
11080
internal IReadOnlyList<PackageSourceContextInfo> PackageSources { get; set; }
11181

11282
public void Dispose()
11383
{
11484
}
11585

116-
internal void RaisePackageSourcesChanged(IReadOnlyList<PackageSourceContextInfo> packageSources)
117-
{
118-
PackageSourcesChanged?.Invoke(this, packageSources);
119-
}
120-
12186
public ValueTask<string> GetActivePackageSourceNameAsync(CancellationToken cancellationToken)
12287
{
12388
return new ValueTask<string>(ActivePackageSourceName);

0 commit comments

Comments
 (0)