From dddfdd8617e80ec5a2ea0dd94a094b243f355d09 Mon Sep 17 00:00:00 2001 From: Jonatan Gonzalez Date: Tue, 29 Oct 2024 14:51:09 -0700 Subject: [PATCH] [Feature] Render package README from local storage in the PM UI (#6110) --- Directory.Packages.props | 1 + NuGet.Config | 17 +- .../GlobalSuppressions.cs | 2 +- .../Models/DetailControlModel.cs | 9 +- .../Models/DetailedPackageMetadata.cs | 3 + .../Models/PackageMetadataTab.cs | 11 + .../NuGet.PackageManagement.UI.csproj | 3 + .../Resources.Designer.cs | 42 ++ .../NuGet.PackageManagement.UI/Resources.resx | 19 + .../UserSettings.cs | 3 + .../ViewModels/PackageDetailsTabViewModel.cs | 120 ++++ .../ViewModels/ReadmePreviewViewModel.cs | 114 +++ .../ViewModels/TitledPageViewModelBase.cs | 24 + .../ViewModels/ViewModelBase.cs | 13 + .../Xamls/DetailControl.xaml | 53 +- .../Xamls/PackageDetailsTabControl.xaml | 153 ++++ .../Xamls/PackageDetailsTabControl.xaml.cs | 48 ++ .../Xamls/PackageManagerControl.xaml | 16 +- .../Xamls/PackageManagerControl.xaml.cs | 13 +- .../Xamls/PackageManagerTopPanel.xaml | 19 +- .../Xamls/PackageMetadataControl.xaml | 671 +++++++++--------- .../Xamls/PackageMetadataControl.xaml.cs | 7 +- .../Xamls/PackageReadmeControl.xaml | 39 + .../Xamls/PackageReadmeControl.xaml.cs | 113 +++ .../PackageSourceMappingActionControl.xaml | 11 +- .../MultiSourcePackageMetadataProvider.cs | 32 +- .../Services/NuGetPackageFileService.cs | 17 + .../NuGet.VisualStudio.Client/.vsixignore | 5 + .../Services/NuGetFeatureFlagConstants.cs | 1 + .../PackageSearchMetadataContextInfo.cs | 2 + .../INuGetPackageFileService.cs | 1 + .../RecommendedPackageSearchMetadata.cs | 1 + .../TransitivePackageSearchMetadata.cs | 2 + .../Converters/ParserConstants.cs | 1 + .../Model/IPackageSearchMetadata.cs | 1 + .../Model/LocalPackageSearchMetadata.cs | 20 + .../Model/PackageSearchMetadata.cs | 3 + .../Model/PackageSearchMetadataBuilder.cs | 2 + .../Model/PackageSearchMetadataV2Feed.cs | 2 + .../PublicAPI/net472/PublicAPI.Unshipped.txt | 7 + .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 7 + .../netstandard2.0/PublicAPI.Unshipped.txt | 7 + .../NuGetSettingsSerializerTests.cs | 2 +- .../ViewModels/ReadmePreviewViewModelTests.cs | 166 +++++ .../Services/NuGetPackageFileServiceTests.cs | 50 +- .../LocalPackageSearchMetadataTests.cs | 11 + .../LocalPackageSearchMetadataFixture.cs | 1 + .../V3PackageSearchMetadataFixture.cs | 2 + 48 files changed, 1455 insertions(+), 412 deletions(-) create mode 100644 src/NuGet.Clients/NuGet.PackageManagement.UI/Models/PackageMetadataTab.cs create mode 100644 src/NuGet.Clients/NuGet.PackageManagement.UI/ViewModels/PackageDetailsTabViewModel.cs create mode 100644 src/NuGet.Clients/NuGet.PackageManagement.UI/ViewModels/ReadmePreviewViewModel.cs create mode 100644 src/NuGet.Clients/NuGet.PackageManagement.UI/ViewModels/TitledPageViewModelBase.cs create mode 100644 src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/PackageDetailsTabControl.xaml create mode 100644 src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/PackageDetailsTabControl.xaml.cs create mode 100644 src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/PackageReadmeControl.xaml create mode 100644 src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/PackageReadmeControl.xaml.cs create mode 100644 test/NuGet.Clients.Tests/NuGet.PackageManagement.UI.Test/ViewModels/ReadmePreviewViewModelTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 6fcfb7f81..ec9c0e35d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -65,6 +65,7 @@ + diff --git a/NuGet.Config b/NuGet.Config index ca0754e53..3ea1bced5 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -15,7 +15,7 @@ - + @@ -23,6 +23,7 @@ + @@ -45,14 +46,14 @@ - + - + - + @@ -65,7 +66,7 @@ - + @@ -83,13 +84,13 @@ - + - + - + diff --git a/src/NuGet.Clients/NuGet.PackageManagement.UI/GlobalSuppressions.cs b/src/NuGet.Clients/NuGet.PackageManagement.UI/GlobalSuppressions.cs index 31038104f..d1ee59605 100644 --- a/src/NuGet.Clients/NuGet.PackageManagement.UI/GlobalSuppressions.cs +++ b/src/NuGet.Clients/NuGet.PackageManagement.UI/GlobalSuppressions.cs @@ -87,7 +87,7 @@ using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage("Build", "CA1501:'PackageManagerControl' has an object hierarchy '9' levels deep within the defining module. If possible, eliminate base classes within the hierarchy to decrease its hierarchy level below '6': 'UserControl, ContentControl, Control, FrameworkElement, UIElement, Visual, DependencyObject, DispatcherObject, Object'", Justification = "", Scope = "type", Target = "~T:NuGet.PackageManagement.UI.PackageManagerControl")] [assembly: SuppressMessage("Build", "CA1501:'PackageItemDeprecationLabel' has an object hierarchy '9' levels deep within the defining module. If possible, eliminate base classes within the hierarchy to decrease its hierarchy level below '6': 'UserControl, ContentControl, Control, FrameworkElement, UIElement, Visual, DependencyObject, DispatcherObject, Object'", Justification = "Default WPF Hierarchy", Scope = "type", Target = "~T:NuGet.PackageManagement.UI.PackageItemDeprecationLabel")] [assembly: SuppressMessage("Build", "CA1501:'PackageManagerTopPanel' has an object hierarchy '9' levels deep within the defining module. If possible, eliminate base classes within the hierarchy to decrease its hierarchy level below '6': 'UserControl, ContentControl, Control, FrameworkElement, UIElement, Visual, DependencyObject, DispatcherObject, Object'", Justification = "", Scope = "type", Target = "~T:NuGet.PackageManagement.UI.PackageManagerTopPanel")] -[assembly: SuppressMessage("Build", "CA1501:'PackageMetadataControl' has an object hierarchy '9' levels deep within the defining module. If possible, eliminate base classes within the hierarchy to decrease its hierarchy level below '6': 'UserControl, ContentControl, Control, FrameworkElement, UIElement, Visual, DependencyObject, DispatcherObject, Object'", Justification = "", Scope = "type", Target = "~T:NuGet.PackageManagement.UI.PackageMetadataControl")] +[assembly: SuppressMessage("Build", "CA1501:'PackageMetadataControl' has an object hierarchy '9' levels deep within the defining module. If possible, eliminate base classes within the hierarchy to decrease its hierarchy level below '6': 'UserControl, ContentControl, Control, FrameworkElement, UIElement, Visual, DependencyObject, DispatcherObject, Object'", Justification = "", Scope = "type", Target = "~T:NuGet.PackageManagement.UI.PackageDetailsTabControl")] [assembly: SuppressMessage("Build", "CA1501:'PackageRestoreBar' has an object hierarchy '9' levels deep within the defining module. If possible, eliminate base classes within the hierarchy to decrease its hierarchy level below '6': 'UserControl, ContentControl, Control, FrameworkElement, UIElement, Visual, DependencyObject, DispatcherObject, Object'", Justification = "", Scope = "type", Target = "~T:NuGet.PackageManagement.UI.PackageRestoreBar")] [assembly: SuppressMessage("Build", "CA1501:'PreviewWindow' has an object hierarchy '11' levels deep within the defining module. If possible, eliminate base classes within the hierarchy to decrease its hierarchy level below '6': 'DialogWindow, DialogWindowBase, Window, ContentControl, Control, FrameworkElement, UIElement, Visual, DependencyObject, DispatcherObject, Object'", Justification = "", Scope = "type", Target = "~T:NuGet.PackageManagement.UI.PreviewWindow")] [assembly: SuppressMessage("Build", "CA1501:'PRMigratorBar' has an object hierarchy '9' levels deep within the defining module. If possible, eliminate base classes within the hierarchy to decrease its hierarchy level below '6': 'UserControl, ContentControl, Control, FrameworkElement, UIElement, Visual, DependencyObject, DispatcherObject, Object'", Justification = "", Scope = "type", Target = "~T:NuGet.PackageManagement.UI.PRMigratorBar")] diff --git a/src/NuGet.Clients/NuGet.PackageManagement.UI/Models/DetailControlModel.cs b/src/NuGet.Clients/NuGet.PackageManagement.UI/Models/DetailControlModel.cs index 124559b48..1ba6d67d7 100644 --- a/src/NuGet.Clients/NuGet.PackageManagement.UI/Models/DetailControlModel.cs +++ b/src/NuGet.Clients/NuGet.PackageManagement.UI/Models/DetailControlModel.cs @@ -24,7 +24,7 @@ namespace NuGet.PackageManagement.UI /// The base class of PackageDetailControlModel and PackageSolutionDetailControlModel. /// When user selects an action, this triggers version list update. /// - public abstract class DetailControlModel : INotifyPropertyChanged, IDisposable + public abstract class DetailControlModel : TitledPageViewModelBase, IDisposable { private static readonly string StarAll = VersionRangeFormatter.Instance.Format("p", VersionRange.Parse("*"), VersionRangeFormatter.Instance); private static readonly string StarAllFloating = VersionRangeFormatter.Instance.Format("p", VersionRange.Parse("*-*"), VersionRangeFormatter.Instance); @@ -70,6 +70,9 @@ namespace NuGet.PackageManagement.UI _options.SelectedChanged += DependencyBehavior_SelectedChanged; _versions = new ItemsChangeObservableCollection(); + + Title = Resources.Label_PackageDetails; + IsVisible = true; } /// @@ -385,11 +388,9 @@ namespace NuGet.PackageManagement.UI // Called after package install/uninstall. public abstract Task RefreshAsync(CancellationToken cancellationToken); - public event PropertyChangedEventHandler PropertyChanged; - protected void OnPropertyChanged(string propertyName) { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + RaisePropertyChanged(propertyName); } public string Id => _searchResultPackage?.Id; diff --git a/src/NuGet.Clients/NuGet.PackageManagement.UI/Models/DetailedPackageMetadata.cs b/src/NuGet.Clients/NuGet.PackageManagement.UI/Models/DetailedPackageMetadata.cs index 800efffb7..d685df31f 100644 --- a/src/NuGet.Clients/NuGet.PackageManagement.UI/Models/DetailedPackageMetadata.cs +++ b/src/NuGet.Clients/NuGet.PackageManagement.UI/Models/DetailedPackageMetadata.cs @@ -33,6 +33,7 @@ namespace NuGet.PackageManagement.UI IconUrl = serverData.IconUrl; LicenseUrl = serverData.LicenseUrl; ProjectUrl = serverData.ProjectUrl; + ReadmeFileUrl = serverData.ReadmeFileUrl; ReadmeUrl = serverData.ReadmeUrl; ReportAbuseUrl = serverData.ReportAbuseUrl; // Some server implementations send down an array with an empty string, which ends up as an empty string. @@ -95,6 +96,8 @@ namespace NuGet.PackageManagement.UI public Uri? ProjectUrl { get; set; } + public string? ReadmeFileUrl { get; set; } + public Uri? ReadmeUrl { get; set; } public Uri? ReportAbuseUrl { get; set; } diff --git a/src/NuGet.Clients/NuGet.PackageManagement.UI/Models/PackageMetadataTab.cs b/src/NuGet.Clients/NuGet.PackageManagement.UI/Models/PackageMetadataTab.cs new file mode 100644 index 000000000..87447b0df --- /dev/null +++ b/src/NuGet.Clients/NuGet.PackageManagement.UI/Models/PackageMetadataTab.cs @@ -0,0 +1,11 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace NuGet.PackageManagement.UI +{ + public enum PackageMetadataTab + { + Readme, + PackageDetails, + } +} diff --git a/src/NuGet.Clients/NuGet.PackageManagement.UI/NuGet.PackageManagement.UI.csproj b/src/NuGet.Clients/NuGet.PackageManagement.UI/NuGet.PackageManagement.UI.csproj index 541da3a91..d77850135 100644 --- a/src/NuGet.Clients/NuGet.PackageManagement.UI/NuGet.PackageManagement.UI.csproj +++ b/src/NuGet.Clients/NuGet.PackageManagement.UI/NuGet.PackageManagement.UI.csproj @@ -41,6 +41,9 @@ + + + diff --git a/src/NuGet.Clients/NuGet.PackageManagement.UI/Resources.Designer.cs b/src/NuGet.Clients/NuGet.PackageManagement.UI/Resources.Designer.cs index 30a08ee71..b5b2880d9 100644 --- a/src/NuGet.Clients/NuGet.PackageManagement.UI/Resources.Designer.cs +++ b/src/NuGet.Clients/NuGet.PackageManagement.UI/Resources.Designer.cs @@ -807,6 +807,15 @@ namespace NuGet.PackageManagement.UI { } } + /// + /// Looks up a localized string similar to An error occurred while trying to load the README.. + /// + public static string Error_UnableToLoadReadme { + get { + return ResourceManager.GetString("Error_UnableToLoadReadme", resourceCulture); + } + } + /// /// Looks up a localized string similar to Operation failed. /// @@ -1230,6 +1239,15 @@ namespace NuGet.PackageManagement.UI { } } + /// + /// Looks up a localized string similar to Package Details. + /// + public static string Label_PackageDetails { + get { + return ResourceManager.GetString("Label_PackageDetails", resourceCulture); + } + } + /// /// Looks up a localized string similar to NuGet Package Manager: {0}. /// @@ -1284,6 +1302,15 @@ namespace NuGet.PackageManagement.UI { } } + /// + /// Looks up a localized string similar to README. + /// + public static string Label_Readme_Tab { + get { + return ResourceManager.GetString("Label_Readme_Tab", resourceCulture); + } + } + /// /// Looks up a localized string similar to Report Abuse:. /// @@ -2051,6 +2078,21 @@ namespace NuGet.PackageManagement.UI { } } + /// + /// Looks up a localized string similar to Could not read package README from selected package. + /// + ///Only the package maintainer can add a README. + /// + ///If you are not the maintainer, please consider filing an issue or contacting the maintainer to request a README. + /// + ///For instructions on how to add a README, please visit [aka.ms/nuget/readme](https://aka.ms/nuget/readme). + /// + public static string Text_NoReadme { + get { + return ResourceManager.GetString("Text_NoReadme", resourceCulture); + } + } + /// /// Looks up a localized string similar to Not available in this source. /// diff --git a/src/NuGet.Clients/NuGet.PackageManagement.UI/Resources.resx b/src/NuGet.Clients/NuGet.PackageManagement.UI/Resources.resx index 879d025c6..4a40914b2 100644 --- a/src/NuGet.Clients/NuGet.PackageManagement.UI/Resources.resx +++ b/src/NuGet.Clients/NuGet.PackageManagement.UI/Resources.resx @@ -1076,4 +1076,23 @@ Please see https://aka.ms/troubleshoot_nuget_cache for more help. Package Level + + Package Details + + + An error occurred while trying to load the README. + + + README + + + Could not read package README from selected package. + +Only the package maintainer can add a README. + +If you are not the maintainer, please consider filing an issue or contacting the maintainer to request a README. + +For instructions on how to add a README, please visit [aka.ms/nuget/readme](https://aka.ms/nuget/readme) + {Locked="[aka.ms/nuget/readme](https://aka.ms/nuget/readme)"} "[aka.ms/nuget/readme](https://aka.ms/nuget/readme)" this is a URL link and should not be translated + diff --git a/src/NuGet.Clients/NuGet.PackageManagement.UI/UserSettings.cs b/src/NuGet.Clients/NuGet.PackageManagement.UI/UserSettings.cs index 4eca01616..a2e99cf12 100644 --- a/src/NuGet.Clients/NuGet.PackageManagement.UI/UserSettings.cs +++ b/src/NuGet.Clients/NuGet.PackageManagement.UI/UserSettings.cs @@ -23,6 +23,7 @@ namespace NuGet.PackageManagement.UI SelectedFilter = ItemFilter.Installed; DependencyBehavior = DependencyBehavior.Lowest; FileConflictAction = FileConflictAction.PromptUser; + SelectedPackageMetadataTab = PackageMetadataTab.Readme; OptionsExpanded = false; } @@ -40,6 +41,8 @@ namespace NuGet.PackageManagement.UI public ItemFilter SelectedFilter { get; set; } + public PackageMetadataTab SelectedPackageMetadataTab { get; set; } + public DependencyBehavior DependencyBehavior { get; set; } public FileConflictAction FileConflictAction { get; set; } diff --git a/src/NuGet.Clients/NuGet.PackageManagement.UI/ViewModels/PackageDetailsTabViewModel.cs b/src/NuGet.Clients/NuGet.PackageManagement.UI/ViewModels/PackageDetailsTabViewModel.cs new file mode 100644 index 000000000..a51f64f9e --- /dev/null +++ b/src/NuGet.Clients/NuGet.PackageManagement.UI/ViewModels/PackageDetailsTabViewModel.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using NuGet.VisualStudio; +using NuGet.VisualStudio.Internal.Contracts; +using NuGet.VisualStudio.Telemetry; + +namespace NuGet.PackageManagement.UI.ViewModels +{ + public class PackageDetailsTabViewModel : ViewModelBase, IDisposable + { + private bool _disposed = false; + private bool _readmeTabEnabled; + + public ReadmePreviewViewModel ReadmePreviewViewModel { get; private set; } + + private DetailControlModel DetailControlModel { get; set; } + + public ObservableCollection Tabs { get; private set; } + + private TitledPageViewModelBase _selectedTab; + public TitledPageViewModelBase SelectedTab + { + get => _selectedTab; + set => SetAndRaisePropertyChanged(ref _selectedTab, value); + } + + public PackageDetailsTabViewModel() + { + _readmeTabEnabled = true; + Tabs = new ObservableCollection(); + } + + public async Task InitializeAsync(DetailControlModel detailControlModel, INuGetPackageFileService nugetPackageFileService, ItemFilter currentFilter, PackageMetadataTab initialSelectedTab) + { + var nuGetFeatureFlagService = await ServiceLocator.GetComponentModelServiceAsync(); + _readmeTabEnabled = await nuGetFeatureFlagService.IsFeatureEnabledAsync(NuGetFeatureFlagConstants.RenderReadmeInPMUI); + + ReadmePreviewViewModel = new ReadmePreviewViewModel(nugetPackageFileService, currentFilter, _readmeTabEnabled); + DetailControlModel = detailControlModel; + + if (_readmeTabEnabled) + { + Tabs.Add(ReadmePreviewViewModel); + } + + Tabs.Add(DetailControlModel); + + SelectedTab = Tabs.FirstOrDefault(t => t.IsVisible && ConvertFromTabType(t) == initialSelectedTab) ?? Tabs.FirstOrDefault(t => t.IsVisible); + + DetailControlModel.PropertyChanged += DetailControlModel_PropertyChanged; + + foreach (var tab in Tabs) + { + tab.PropertyChanged += IsVisible_PropertyChanged; + } + } + + public static PackageMetadataTab ConvertFromTabType(TitledPageViewModelBase vm) + { + if (vm is DetailControlModel) + { + return PackageMetadataTab.PackageDetails; + } + return PackageMetadataTab.Readme; + } + + public void Dispose() + { + if (_disposed) + { + return; + } + _disposed = true; + DetailControlModel.PropertyChanged -= DetailControlModel_PropertyChanged; + + foreach (var tab in Tabs) + { + tab.PropertyChanged -= IsVisible_PropertyChanged; + } + } + + private void IsVisible_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(TitledPageViewModelBase.IsVisible)) + { + if (_readmeTabEnabled && sender is ReadmePreviewViewModel) + { + if (ReadmePreviewViewModel.IsVisible) + { + Tabs.Insert(0, ReadmePreviewViewModel); + } + else + { + Tabs.Remove(ReadmePreviewViewModel); + } + } + + if (SelectedTab == null || (SelectedTab == sender && !SelectedTab.IsVisible)) + { + SelectedTab = Tabs.FirstOrDefault(t => t.IsVisible); + } + } + } + + private void DetailControlModel_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + NuGetUIThreadHelper.JoinableTaskFactory.RunAsync(async () => + { + if (_readmeTabEnabled) + { + await ReadmePreviewViewModel.SetPackageMetadataAsync(DetailControlModel.PackageMetadata, CancellationToken.None); + } + }).PostOnFailure(nameof(PackageDetailsTabViewModel), nameof(DetailControlModel_PropertyChanged)); + } + } +} diff --git a/src/NuGet.Clients/NuGet.PackageManagement.UI/ViewModels/ReadmePreviewViewModel.cs b/src/NuGet.Clients/NuGet.PackageManagement.UI/ViewModels/ReadmePreviewViewModel.cs new file mode 100644 index 000000000..ff897fe3b --- /dev/null +++ b/src/NuGet.Clients/NuGet.PackageManagement.UI/ViewModels/ReadmePreviewViewModel.cs @@ -0,0 +1,114 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Threading; +using NuGet.VisualStudio.Internal.Contracts; + +namespace NuGet.PackageManagement.UI.ViewModels +{ + public sealed class ReadmePreviewViewModel : TitledPageViewModelBase + { + private bool _errorLoadingReadme; + private INuGetPackageFileService _nugetPackageFileService; + private string _rawReadme; + private DetailedPackageMetadata _packageMetadata; + private bool _canRenderLocalReadme; + + public ReadmePreviewViewModel(INuGetPackageFileService packageFileService, ItemFilter itemFilter, bool isReadmeFeatureEnabled) + { + _nugetPackageFileService = packageFileService ?? throw new ArgumentNullException(nameof(packageFileService)); + _canRenderLocalReadme = CanRenderLocalReadme(itemFilter); + _nugetPackageFileService = packageFileService; + _errorLoadingReadme = false; + _rawReadme = string.Empty; + _packageMetadata = null; + Title = Resources.Label_Readme_Tab; + IsVisible = isReadmeFeatureEnabled; + } + + public bool ErrorLoadingReadme + { + get => _errorLoadingReadme; + set => SetAndRaisePropertyChanged(ref _errorLoadingReadme, value); + } + + public string ReadmeMarkdown + { + get => _rawReadme; + set => SetAndRaisePropertyChanged(ref _rawReadme, value); + } + + public async Task ItemFilterChangedAsync(ItemFilter filter) + { + var oldRenderLocalReadme = _canRenderLocalReadme; + _canRenderLocalReadme = CanRenderLocalReadme(filter); + if (_canRenderLocalReadme != oldRenderLocalReadme) + { + if (_packageMetadata != null) + { + await LoadReadmeAsync(CancellationToken.None); + } + } + } + + public async Task SetPackageMetadataAsync(DetailedPackageMetadata packageMetadata, CancellationToken cancellationToken) + { + if (packageMetadata != null && (!string.Equals(packageMetadata.Id, _packageMetadata?.Id) || packageMetadata.Version != _packageMetadata?.Version)) + { + _packageMetadata = packageMetadata; + await LoadReadmeAsync(cancellationToken); + } + } + + private static bool CanRenderLocalReadme(ItemFilter filter) + { + return filter != ItemFilter.All; + } + + private async Task LoadReadmeAsync(CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(_packageMetadata.ReadmeFileUrl)) + { + ReadmeMarkdown = _canRenderLocalReadme && !string.IsNullOrWhiteSpace(_packageMetadata.PackagePath) ? Resources.Text_NoReadme : string.Empty; + IsVisible = !string.IsNullOrWhiteSpace(ReadmeMarkdown); + ErrorLoadingReadme = false; + return; + } + + var readmeUrl = new Uri(_packageMetadata.ReadmeFileUrl); + if (!_canRenderLocalReadme && readmeUrl.IsFile) + { + ReadmeMarkdown = string.Empty; + IsVisible = false; + ErrorLoadingReadme = false; + return; + } + + var readme = Resources.Text_NoReadme; + await ThreadHelper.JoinableTaskFactory.RunAsync(async () => + { + await TaskScheduler.Default; + using var readmeStream = await _nugetPackageFileService.GetReadmeAsync(readmeUrl, cancellationToken); + if (readmeStream is null) + { + return; + } + + using StreamReader streamReader = new StreamReader(readmeStream); + readme = await streamReader.ReadToEndAsync(); + }); + + if (!cancellationToken.IsCancellationRequested) + { + ReadmeMarkdown = readme; + IsVisible = !string.IsNullOrWhiteSpace(readme); + ErrorLoadingReadme = false; + } + } + } +} diff --git a/src/NuGet.Clients/NuGet.PackageManagement.UI/ViewModels/TitledPageViewModelBase.cs b/src/NuGet.Clients/NuGet.PackageManagement.UI/ViewModels/TitledPageViewModelBase.cs new file mode 100644 index 000000000..313d42c26 --- /dev/null +++ b/src/NuGet.Clients/NuGet.PackageManagement.UI/ViewModels/TitledPageViewModelBase.cs @@ -0,0 +1,24 @@ +namespace NuGet.PackageManagement.UI.ViewModels +{ + public class TitledPageViewModelBase : ViewModelBase + { + private string _title; + public string Title + { + get => _title; + protected set => SetAndRaisePropertyChanged(ref _title, value); + } + + private bool _isVisible; + public bool IsVisible + { + get => _isVisible; + protected set => SetAndRaisePropertyChanged(ref _isVisible, value); + } + + public override string ToString() + { + return Title; + } + } +} diff --git a/src/NuGet.Clients/NuGet.PackageManagement.UI/ViewModels/ViewModelBase.cs b/src/NuGet.Clients/NuGet.PackageManagement.UI/ViewModels/ViewModelBase.cs index 075579e13..cbf50daab 100644 --- a/src/NuGet.Clients/NuGet.PackageManagement.UI/ViewModels/ViewModelBase.cs +++ b/src/NuGet.Clients/NuGet.PackageManagement.UI/ViewModels/ViewModelBase.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.ComponentModel; using System.Runtime.CompilerServices; @@ -11,5 +12,17 @@ namespace NuGet.PackageManagement.UI.ViewModels { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } + + public bool SetAndRaisePropertyChanged(ref T field, T newValue, [CallerMemberName] string propertyName = "") + { + if (!EqualityComparer.Default.Equals(field, newValue)) + { + field = newValue; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + return true; + } + + return false; + } } } diff --git a/src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/DetailControl.xaml b/src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/DetailControl.xaml index 9d7a48df2..30ada73fe 100644 --- a/src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/DetailControl.xaml +++ b/src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/DetailControl.xaml @@ -42,15 +42,7 @@ - - - - + Height="*" /> @@ -158,39 +150,9 @@ BorderBrush="{DynamicResource {x:Static SystemColors.ActiveBorderBrushKey}}" BorderThickness="0,1" /> - - - - - - - - - - - - - - @@ -216,12 +178,11 @@ Visibility="{Binding Path=OptionsBlockedMessage,Converter={StaticResource InverseNullToVisibilityConverter}}" /> - - + + diff --git a/src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/PackageDetailsTabControl.xaml b/src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/PackageDetailsTabControl.xaml new file mode 100644 index 000000000..fd0e47d1c --- /dev/null +++ b/src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/PackageDetailsTabControl.xaml @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/PackageDetailsTabControl.xaml.cs b/src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/PackageDetailsTabControl.xaml.cs new file mode 100644 index 000000000..4bd392589 --- /dev/null +++ b/src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/PackageDetailsTabControl.xaml.cs @@ -0,0 +1,48 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Windows.Controls; +using NuGet.PackageManagement.UI.ViewModels; + +namespace NuGet.PackageManagement.UI +{ + + /// + /// Interaction logic for PackageDetailsTabControl.xaml + /// + public partial class PackageDetailsTabControl : UserControl, IDisposable + { + public PackageDetailsTabViewModel PackageDetailsTabViewModel + { + get => DataContext as PackageDetailsTabViewModel; + } + + private bool _disposed = false; + + public PackageDetailsTabControl() + { + InitializeComponent(); + DataContext = new PackageDetailsTabViewModel(); + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + if (disposing) + { + PackageDetailsTabViewModel.Dispose(); + } + _disposed = true; + } + } +} diff --git a/src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/PackageManagerControl.xaml b/src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/PackageManagerControl.xaml index a418cbbd9..a6a39bceb 100644 --- a/src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/PackageManagerControl.xaml +++ b/src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/PackageManagerControl.xaml @@ -83,8 +83,7 @@ + Width="67*"> @@ -93,17 +92,17 @@ + + Width="157*"/> + Grid.Column="0" Grid.ColumnSpan="2"> @@ -145,11 +144,9 @@ + Grid.ColumnSpan="2" + Margin="4,0,0,0" /> diff --git a/src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/PackageManagerControl.xaml.cs b/src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/PackageManagerControl.xaml.cs index 3f3b9507e..5d71167d5 100644 --- a/src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/PackageManagerControl.xaml.cs +++ b/src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/PackageManagerControl.xaml.cs @@ -67,6 +67,8 @@ namespace NuGet.PackageManagement.UI private IServiceBroker _serviceBroker; private bool _disposed = false; private IPackageVulnerabilityService _packageVulnerabilityService; + private INuGetPackageFileService _nugetPackageFileService; + private PackageManagerInstalledTabData _installedTabTelemetryData; @@ -141,6 +143,12 @@ namespace NuGet.PackageManagement.UI _settingsKey = await GetSettingsKeyAsync(CancellationToken.None); UserSettings settings = LoadSettings(); InitializeFilterList(settings); + + _nugetPackageFileService?.Dispose(); + _nugetPackageFileService = await _serviceBroker.GetProxyAsync(NuGetServices.PackageFileService, CancellationToken.None); + + await _packageDetail._packageDetailsTabControl.PackageDetailsTabViewModel.InitializeAsync(_detailModel, _nugetPackageFileService, _topPanel.Filter, settings.SelectedPackageMetadataTab); + await InitPackageSourcesAsync(settings, CancellationToken.None); ApplySettings(settings, Settings); _initialized = true; @@ -678,7 +686,8 @@ namespace NuGet.PackageManagement.UI FileConflictAction = _detailModel.Options.SelectedFileConflictAction.Action, IncludePrerelease = _topPanel.CheckboxPrerelease.IsChecked == true, SelectedFilter = _topPanel.Filter, - OptionsExpanded = _packageDetail._optionsControl.IsExpanded + OptionsExpanded = _packageDetail._optionsControl.IsExpanded, + SelectedPackageMetadataTab = PackageDetailsTabViewModel.ConvertFromTabType(_packageDetail._packageDetailsTabControl.PackageDetailsTabViewModel.SelectedTab) }; _packageDetail._solutionView.SaveSettings(settings); @@ -1269,6 +1278,7 @@ namespace NuGet.PackageManagement.UI NuGetUIThreadHelper.JoinableTaskFactory.RunAsync(async () => { + await _packageDetail._packageDetailsTabControl.PackageDetailsTabViewModel.ReadmePreviewViewModel.ItemFilterChangedAsync(ActiveFilter); await RunAndEmitRefreshAsync(async () => { await NuGetUIThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); @@ -1836,6 +1846,7 @@ namespace NuGet.PackageManagement.UI if (disposing) { + _nugetPackageFileService.Dispose(); CleanUp(); } diff --git a/src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/PackageManagerTopPanel.xaml b/src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/PackageManagerTopPanel.xaml index 33d888ae1..edeb4d1e4 100644 --- a/src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/PackageManagerTopPanel.xaml +++ b/src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/PackageManagerTopPanel.xaml @@ -180,18 +180,16 @@ - - - + + - - - - + + + + IsChecked="True"> + + - + - - - + + + - - - + AutomationProperties.AutomationId="{Binding Id, Mode=OneWay, StringFormat='LicenseFile'}"> + + + + x:Name="_licenseWarning" + Margin="0,0,4,0" + Visibility="{Binding Text, Converter={StaticResource NullToVisibilityConverter}}" + ToolTip="{Binding Text}" + AutomationProperties.Name="{Binding Text}" + Moniker="{x:Static catalog:KnownMonikers.StatusWarning}" /> + AutomationProperties.LabeledBy="{Binding ElementName=_packageLicenseOperator}" + AutomationProperties.AutomationId="{Binding Id, Mode=OneWay, StringFormat='LicenseOperator_{0}'}" + Text="{Binding Text}" />