[Feature] Render package README from local storage in the PM UI (#6110)

This commit is contained in:
Jonatan Gonzalez authored and GitHub committed 2024-10-29 14:51:09 -07:00
1 parent fad3153291
commit dddfdd8617
48 files changed
+1455 -412

No files matched your search

+1
View File
@@ -65,6 +65,7 @@
<PackageVersion Include="Microsoft.Test.Apex.VisualStudio" Version="17.11.35005.70" />
<PackageVersion Include="Microsoft.TestPlatform.Portable" Version="17.1.0" />
<PackageVersion Include="Microsoft.VisualStudio.LanguageServices" Version="4.3.1" />
<PackageVersion Include="Microsoft.VisualStudio.Markdown.Platform" Version="17.11.119-preview" />
<PackageVersion Include="Microsoft.VisualStudio.ProjectSystem" Version="17.4.221-pre" />
<PackageVersion Include="Microsoft.VisualStudio.ProjectSystem.Managed" Version="17.2.0-beta1-20502-01" />
<PackageVersion Include="Microsoft.VisualStudio.ProjectSystem.Managed.VS" Version="17.2.0-beta1-20502-01" />
+9 -8
View File
@@ -15,7 +15,7 @@
</auditSources>
<packageSourceMapping>
<clear />
<packageSource key = "dotnet-public">
<packageSource key="dotnet-public">
<package pattern="Azure.Core " />
<package pattern="ben.demystifier" />
<package pattern="castle.core" />
@@ -23,6 +23,7 @@
<package pattern="Humanizer.Core" />
<package pattern="ilmerge" />
<package pattern="lucene.net" />
<package pattern="Markdig.Signed" />
<package pattern="messagepack" />
<package pattern="messagepack.annotations" />
<package pattern="messagepackanalyzer" />
@@ -45,14 +46,14 @@
<package pattern="xunit.*" />
<package pattern="xunitxml.testlogger" />
</packageSource>
<packageSource key = "dotnet-eng">
<packageSource key="dotnet-eng">
<package pattern="Microsoft.DotNet.*" />
</packageSource>
<packageSource key = "nuget-build">
<packageSource key="nuget-build">
<package pattern="nuget.client.endtoend.testdata" />
<package pattern="nugetvalidator" />
</packageSource>
<packageSource key = "vside">
<packageSource key="vside">
<package pattern="envdte" />
<package pattern="envdte100" />
<package pattern="envdte80" />
@@ -65,7 +66,7 @@
<package pattern="Microsoft.ServiceHub.Framework" />
<package pattern="Microsoft.TeamFoundationServer.ExtendedClient" />
<package pattern="microsoft.test.apex.visualstudio" />
<package pattern="microsoft.visualstudio.*" />
<package pattern="Microsoft.VisualStudio.*" />
<package pattern="Microsoft.VisualStudioEng.MicroBuild.Core" />
<package pattern="Microsoft.VSSDK.BuildTools" />
<package pattern="stdole" />
@@ -83,13 +84,13 @@
<package pattern="vslangproj90" />
<package pattern="vswebsite.interop" />
</packageSource>
<packageSource key = "myget-legacy@Local">
<packageSource key="myget-legacy@Local">
<package pattern="Microsoft.Extensions.CommandLineUtils.Sources" />
</packageSource>
<packageSource key = "VS">
<packageSource key="VS">
<package pattern="Microsoft.DevDiv.Validation.TestPlatform.Settings.Tasks" />
</packageSource>
<packageSource key = "dotnet-libraries">
<packageSource key="dotnet-libraries">
<package pattern="System.CommandLine" />
</packageSource>
</packageSourceMapping>
@@ -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 = "<Pending>", 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 = "<Pending>", 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 = "<Pending>", 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 = "<Pending>", 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 = "<Pending>", 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 = "<Pending>", 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 = "<Pending>", Scope = "type", Target = "~T:NuGet.PackageManagement.UI.PRMigratorBar")]
@@ -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.
/// </summary>
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<DisplayVersion>();
Title = Resources.Label_PackageDetails;
IsVisible = true;
}
/// <summary>
@@ -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;
@@ -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; }
@@ -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,
}
}
@@ -41,6 +41,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Build.Framework" />
<PackageReference Include="Microsoft.NET.StringTools" />
<PackageReference Include="Microsoft.VisualStudio.Markdown.Platform" />
<PackageReference Include="Microsoft.VisualStudio.Sdk" />
</ItemGroup>
@@ -807,6 +807,15 @@ namespace NuGet.PackageManagement.UI {
}
}
/// <summary>
/// Looks up a localized string similar to An error occurred while trying to load the README..
/// </summary>
public static string Error_UnableToLoadReadme {
get {
return ResourceManager.GetString("Error_UnableToLoadReadme", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Operation failed.
/// </summary>
@@ -1230,6 +1239,15 @@ namespace NuGet.PackageManagement.UI {
}
}
/// <summary>
/// Looks up a localized string similar to Package Details.
/// </summary>
public static string Label_PackageDetails {
get {
return ResourceManager.GetString("Label_PackageDetails", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to NuGet Package Manager: {0}.
/// </summary>
@@ -1284,6 +1302,15 @@ namespace NuGet.PackageManagement.UI {
}
}
/// <summary>
/// Looks up a localized string similar to README.
/// </summary>
public static string Label_Readme_Tab {
get {
return ResourceManager.GetString("Label_Readme_Tab", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Report Abuse:.
/// </summary>
@@ -2051,6 +2078,21 @@ namespace NuGet.PackageManagement.UI {
}
}
/// <summary>
/// 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).
/// </summary>
public static string Text_NoReadme {
get {
return ResourceManager.GetString("Text_NoReadme", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Not available in this source.
/// </summary>
@@ -1076,4 +1076,23 @@ Please see https://aka.ms/troubleshoot_nuget_cache for more help.</value>
<data name="ColumnHeader_PackageLevel" xml:space="preserve">
<value>Package Level</value>
</data>
<data name="Label_PackageDetails" xml:space="preserve">
<value>Package Details</value>
</data>
<data name="Error_UnableToLoadReadme" xml:space="preserve">
<value>An error occurred while trying to load the README.</value>
</data>
<data name="Label_Readme_Tab" xml:space="preserve">
<value>README</value>
</data>
<data name="Text_NoReadme" xml:space="preserve">
<value>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)</value>
<comment>{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</comment>
</data>
</root>
@@ -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; }
@@ -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<TitledPageViewModelBase> Tabs { get; private set; }
private TitledPageViewModelBase _selectedTab;
public TitledPageViewModelBase SelectedTab
{
get => _selectedTab;
set => SetAndRaisePropertyChanged(ref _selectedTab, value);
}
public PackageDetailsTabViewModel()
{
_readmeTabEnabled = true;
Tabs = new ObservableCollection<TitledPageViewModelBase>();
}
public async Task InitializeAsync(DetailControlModel detailControlModel, INuGetPackageFileService nugetPackageFileService, ItemFilter currentFilter, PackageMetadataTab initialSelectedTab)
{
var nuGetFeatureFlagService = await ServiceLocator.GetComponentModelServiceAsync<INuGetFeatureFlagService>();
_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));
}
}
}
@@ -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;
}
}
}
}
@@ -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;
}
}
}
@@ -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<T>(ref T field, T newValue, [CallerMemberName] string propertyName = "")
{
if (!EqualityComparer<T>.Default.Equals(field, newValue))
{
field = newValue;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
return true;
}
return false;
}
}
}
@@ -42,15 +42,7 @@
<RowDefinition
Height="Auto" />
<RowDefinition
Height="Auto" />
<RowDefinition
Height="Auto" />
<RowDefinition
Height="Auto" />
<RowDefinition
Height="Auto" />
<RowDefinition
Height="Auto" />
Height="*" />
</Grid.RowDefinitions>
<!-- Icon and Id -->
@@ -158,39 +150,9 @@
BorderBrush="{DynamicResource {x:Static SystemColors.ActiveBorderBrushKey}}"
BorderThickness="0,1" />
<!-- vulnerabilities info -->
<Border
Grid.Row="3"
Margin="0,11,0,0"
BorderBrush="{DynamicResource {x:Static SystemColors.ActiveBorderBrushKey}}"
Visibility="{Binding Path=IsPackageVulnerable,Converter={StaticResource BooleanToVisibilityConverter}}"
BorderThickness="0,1,0,0">
<Grid>
<nuget:VulnerabilitiesControl
x:Name="_vulnerabilitiesControl"
DataContext="{Binding}"
AutomationProperties.Name="{x:Static nuget:Resources.Accessibility_PackageVulnerabilities}" />
</Grid>
</Border>
<!-- deprecation info -->
<Border
Grid.Row="4"
Margin="0,11,0,0"
BorderBrush="{DynamicResource {x:Static SystemColors.ActiveBorderBrushKey}}"
Visibility="{Binding Path=IsPackageDeprecated,Converter={StaticResource BooleanToVisibilityConverter}}"
BorderThickness="0,1,0,0">
<Grid>
<nuget:DeprecationControl
x:Name="_deprecationControl"
DataContext="{Binding}"
AutomationProperties.Name="{x:Static nuget:Resources.Accessibility_PackageDeprecation}" />
</Grid>
</Border>
<!-- options -->
<Border
Grid.Row="5"
Grid.Row="3"
BorderBrush="{DynamicResource {x:Static SystemColors.ActiveBorderBrushKey}}"
BorderThickness="0,1,0,1">
<Grid>
@@ -216,12 +178,11 @@
Visibility="{Binding Path=OptionsBlockedMessage,Converter={StaticResource InverseNullToVisibilityConverter}}" />
</Grid>
</Border>
<nuget:PackageMetadataControl
Grid.Row="7"
AutomationProperties.Name="{x:Static nuget:Resources.Accessibility_PackageMetadata}"
Margin="0,12,0,0"
DataContext="{Binding PackageMetadata}" />
<nuget:PackageDetailsTabControl
x:Name="_packageDetailsTabControl"
Grid.Row="4"
Margin="0,12,0,0"/>
</Grid>
</ScrollViewer>
</UserControl>
@@ -0,0 +1,153 @@
<UserControl
x:Class="NuGet.PackageManagement.UI.PackageDetailsTabControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:nuget="clr-namespace:NuGet.PackageManagement.UI"
xmlns:vm="clr-namespace:NuGet.PackageManagement.UI.ViewModels"
xmlns:imaging="clr-namespace:Microsoft.VisualStudio.Imaging;assembly=Microsoft.VisualStudio.Imaging"
xmlns:catalog="clr-namespace:Microsoft.VisualStudio.Imaging;assembly=Microsoft.VisualStudio.ImageCatalog"
xmlns:glob="clr-namespace:System.Globalization;assembly=mscorlib"
Background="{DynamicResource {x:Static nuget:Brushes.DetailPaneBackground}}"
Foreground="{DynamicResource {x:Static nuget:Brushes.UIText}}"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="300">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<nuget:SharedResources />
</ResourceDictionary.MergedDictionaries>
<nuget:AccessibleConverter x:Key="AccNameWorkaround" />
</ResourceDictionary>
</UserControl.Resources>
<TabControl
Grid.Row="0"
ItemsSource="{Binding Tabs}"
SelectedItem="{Binding SelectedTab}"
x:Name="tabsPackageDetails">
<TabControl.Resources>
<Style TargetType="TabItem">
<Setter Property="Background" Value="{DynamicResource {x:Static nuget:Brushes.HeaderBackground}}" />
<Setter Property="FontSize" Value="{Binding FontSize, RelativeSource={RelativeSource AncestorType=UserControl},Converter={StaticResource Font122PercentSizeConverter}}"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static nuget:Brushes.UIText}}" />
<Setter Property="Padding" Value="20,0,20,0" />
<Setter Property="FocusVisualStyle" Value="{DynamicResource ControlsFocusVisualStyle}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabItem}">
<Grid x:Name="templateRoot" SnapsToDevicePixels="true">
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<Border x:Name="mainBorder" Background="{TemplateBinding Background}" BorderThickness="0" Margin="0">
</Border>
<ContentPresenter Grid.Row="0" x:Name="contentPresenter" ContentSource="Header" Focusable="False" HorizontalAlignment="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
<!--the line under the text to indicate that this label is selected-->
<Rectangle
x:Name="_underline"
Grid.Row="1"
Height="3"
Visibility="Collapsed"
Width="{Binding ActualWidth, ElementName=contentPresenter}"
HorizontalAlignment="Center"
Fill="{DynamicResource {x:Static nuget:Brushes.TabSelectedIndicatorBrushKey}}"/>
</Grid>
<ControlTemplate.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsSelected, RelativeSource={RelativeSource Self}}" Value="true"/>
</MultiDataTrigger.Conditions>
<Setter Property="Visibility" TargetName="_underline" Value="Visible" />
<Setter Property="Foreground" Value="{DynamicResource {x:Static nuget:Brushes.TabSelectedTextBrushKey}}" />
</MultiDataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsKeyboardFocused, RelativeSource={RelativeSource Self}}" Value="true" />
</MultiDataTrigger.Conditions>
<Setter Property="Foreground" Value="{DynamicResource {x:Static nuget:Brushes.TabTextFocusedBrushKey}}" />
</MultiDataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsMouseOver, RelativeSource={RelativeSource Self}}" Value="true" />
</MultiDataTrigger.Conditions>
<Setter Property="Foreground" Value="{DynamicResource {x:Static nuget:Brushes.TabTextHoverBrushKey}}" />
</MultiDataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type TabControl}">
<Setter Property="Padding" Value="2"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Template" >
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabControl}">
<Grid x:Name="templateRoot" ClipToBounds="True" SnapsToDevicePixels="true" KeyboardNavigation.TabNavigation="Local">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="ColumnDefinition0"/>
<ColumnDefinition x:Name="ColumnDefinition1" Width="0"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition x:Name="RowDefinition0" Height="Auto"/>
<RowDefinition x:Name="RowDefinition1" Height="Auto"/>
<RowDefinition x:Name="RowDefinition2"/>
</Grid.RowDefinitions>
<TabPanel x:Name="headerPanel" Background="Transparent" Grid.Column="0" IsItemsHost="true" Margin="0,0,0,-1" Grid.Row="0" KeyboardNavigation.TabIndex="1" Panel.ZIndex="1"/>
<Rectangle
x:Name="gridBorder"
Grid.Row="1"
Height="1"
Panel.ZIndex="-1"
Margin="0,1,0,0"
HorizontalAlignment="Stretch"
Fill="{DynamicResource {x:Static SystemColors.ActiveBorderBrushKey}}"/>
<ContentPresenter
Grid.Row="2"
ClipToBounds="True"
x:Name="PART_SelectedContentHost"
ContentSource="SelectedContent"
HorizontalAlignment="Stretch"
Margin="{TemplateBinding Padding}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
VerticalAlignment="Stretch"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</TabControl.Resources>
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Title}" />
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsVisible}" Value="False">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<!-- Customize the content of the tabs here -->
<ContentControl Content="{Binding}" Focusable="False">
<ContentControl.Resources>
<DataTemplate DataType="{x:Type nuget:DetailControlModel}">
<nuget:PackageMetadataControl
Focusable="True"
Margin="0,12,0,0"/>
</DataTemplate>
<DataTemplate DataType="{x:Type vm:ReadmePreviewViewModel}">
<nuget:PackageReadmeControl
x:Name="_packageReadmeControl"/>
</DataTemplate>
</ContentControl.Resources>
</ContentControl>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</UserControl>
@@ -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
{
/// <summary>
/// Interaction logic for PackageDetailsTabControl.xaml
/// </summary>
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;
}
}
}
@@ -83,8 +83,7 @@
<Grid.ColumnDefinitions>
<ColumnDefinition
x:Name="_leftSideGridColumn"
Width="65*"
MinWidth="300">
Width="67*">
<ColumnDefinition.MaxWidth>
<MultiBinding Converter="{StaticResource SubstractionConverter}">
<Binding Path="ActualWidth" ElementName="_root" />
@@ -93,17 +92,17 @@
</MultiBinding>
</ColumnDefinition.MaxWidth>
</ColumnDefinition>
<ColumnDefinition Width="224*"/>
<ColumnDefinition Width="auto" />
<ColumnDefinition
x:Name="_rightSideGridColumn"
Width="35*"
MinWidth="382" />
Width="157*"/>
</Grid.ColumnDefinitions>
<!-- left side -->
<Grid
Name="_leftSide"
Grid.Column="0">
Grid.Column="0" Grid.ColumnSpan="2">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
@@ -145,11 +144,9 @@
<GridSplitter
x:Name="_gridSplitter"
Grid.Column="1"
Grid.Column="2"
Width="4"
Grid.RowSpan="2"
HorizontalAlignment="Center"
VerticalAlignment="Stretch"
AutomationProperties.Name="{x:Static nuget:Resources.Accessibility_ThumbName}"
AutomationProperties.HelpText="{x:Static nuget:Resources.Accessibility_ThumbHelp}"
Background="{DynamicResource {x:Static nuget:Brushes.SplitterBackgroundKey}}"
@@ -161,7 +158,8 @@
x:Name="_packageDetail"
AutomationProperties.Name="{x:Static nuget:Resources.Accessibility_PackagesDetails}"
Grid.Column="2"
Grid.RowSpan="2" />
Grid.ColumnSpan="2"
Margin="4,0,0,0" />
</Grid>
</Border>
</DockPanel>
@@ -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<INuGetPackageFileService>(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();
}
@@ -180,18 +180,16 @@
<!-- search control and include prerelease checkbox -->
<Grid Grid.Row="2" Margin="20,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MinWidth="269"/>
<ColumnDefinition Width="0.01*" MinWidth="5" />
<ColumnDefinition Width="*" MinWidth="192" />
<ColumnDefinition Width="2*"/>
<ColumnDefinition MinWidth="192" />
</Grid.ColumnDefinitions>
<Grid
Grid.Column="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MaxWidth="320" MinWidth="118"/>
<ColumnDefinition Width="auto" />
<ColumnDefinition Width="auto" />
<ColumnDefinition MaxWidth="320" MinWidth="118"/>
<ColumnDefinition Width="auto" />
<ColumnDefinition MaxWidth="125" />
<ColumnDefinition MaxWidth="150"/>
</Grid.ColumnDefinitions>
<!-- container of the search control -->
<Border
@@ -226,12 +224,13 @@
AutomationProperties.AutomationId="CheckBox_Prerelease"
Margin="3,0"
VerticalAlignment="Center"
Content="{x:Static nuget:Resources.Checkbox_IncludePrerelease}"
Foreground="{DynamicResource {x:Static nuget:Brushes.UIText}}"
VerticalContentAlignment="Center"
Checked="_checkboxPrerelease_Checked"
Unchecked="_checkboxPrerelease_Unchecked"
IsChecked="True" />
IsChecked="True">
<TextBlock TextTrimming="CharacterEllipsis" Text="{x:Static nuget:Resources.Checkbox_IncludePrerelease}" />
</CheckBox>
<!-- show vulnerabilities checkbox -->
<CheckBox
@@ -246,7 +245,7 @@
Unchecked="_checkboxVulnerabilities_Unchecked"
Visibility="Hidden"
IsChecked="False">
<TextBlock Text="{x:Static nuget:Resources.Checkbox_Show_Vulnerable_Only}" />
<TextBlock TextTrimming="CharacterEllipsis" Text="{x:Static nuget:Resources.Checkbox_Show_Vulnerable_Only}" />
</CheckBox>
</Grid>
<Grid
@@ -22,46 +22,46 @@
<nuget:AccessibleConverter x:Key="AccNameWorkaround" />
<DataTemplate DataType="{x:Type nuget:LicenseText}">
<TextBlock>
<Hyperlink
Style="{StaticResource HyperlinkStyle}"
Command="{x:Static nuget:PackageManagerControlCommands.OpenExternalLink}"
CommandParameter="{x:Static nugettel:HyperlinkType.License}"
ToolTip="{Binding Link}"
NavigateUri="{Binding Link}"
AutomationProperties.Name="{x:Static nuget:Resources.Label_License}">
<Run Text="{Binding Text}"></Run>
</Hyperlink>
<Hyperlink
Style="{StaticResource HyperlinkStyle}"
Command="{x:Static nuget:PackageManagerControlCommands.OpenExternalLink}"
CommandParameter="{x:Static nugettel:HyperlinkType.License}"
ToolTip="{Binding Link}"
NavigateUri="{Binding Link}"
AutomationProperties.Name="{x:Static nuget:Resources.Label_License}">
<Run Text="{Binding Text}"></Run>
</Hyperlink>
</TextBlock>
</DataTemplate>
<DataTemplate DataType="{x:Type nuget:LicenseFileText}">
<TextBlock
AutomationProperties.AutomationId="{Binding Id, Mode=OneWay, StringFormat='LicenseFile'}">
<Hyperlink
AutomationProperties.AutomationId="{Binding Id, Mode=OneWay, StringFormat='LicenseFileLink'}"
AutomationProperties.Name="{x:Static nuget:Resources.Hyperlink_License}"
Style="{StaticResource WindowHyperlinkStyle}"
Click="ViewLicense_Click">
<Run Text="{Binding Text}"/>
</Hyperlink>
AutomationProperties.AutomationId="{Binding Id, Mode=OneWay, StringFormat='LicenseFile'}">
<Hyperlink
AutomationProperties.AutomationId="{Binding Id, Mode=OneWay, StringFormat='LicenseFileLink'}"
AutomationProperties.Name="{x:Static nuget:Resources.Hyperlink_License}"
Style="{StaticResource WindowHyperlinkStyle}"
Click="ViewLicense_Click">
<Run Text="{Binding Text}"/>
</Hyperlink>
</TextBlock>
</DataTemplate>
<DataTemplate DataType="{x:Type nuget:WarningText}">
<imaging:CrispImage
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}" />
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}" />
</DataTemplate>
<DataTemplate DataType="{x:Type nuget:FreeText}">
<TextBlock
AutomationProperties.LabeledBy="{Binding ElementName=_packageLicenseOperator}"
AutomationProperties.AutomationId="{Binding Id, Mode=OneWay, StringFormat='LicenseOperator_{0}'}"
Text="{Binding Text}" />
AutomationProperties.LabeledBy="{Binding ElementName=_packageLicenseOperator}"
AutomationProperties.AutomationId="{Binding Id, Mode=OneWay, StringFormat='LicenseOperator_{0}'}"
Text="{Binding Text}" />
</DataTemplate>
<Style x:Key="TreeViewItemHeaderStyle" TargetType="TreeViewItem">
@@ -78,304 +78,338 @@
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition
Height="Auto" />
<RowDefinition
Height="Auto" />
<RowDefinition
Height="Auto" />
<RowDefinition
Height="Auto" />
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- vulnerabilities info -->
<Border
Grid.Row="0"
BorderBrush="{DynamicResource {x:Static SystemColors.ActiveBorderBrushKey}}"
Visibility="{Binding Path=IsPackageVulnerable,Converter={StaticResource BooleanToVisibilityConverter}}"
BorderThickness="0,0,0,1">
<Grid>
<nuget:VulnerabilitiesControl
x:Name="_vulnerabilitiesControl"
DataContext="{Binding}"
AutomationProperties.Name="{x:Static nuget:Resources.Accessibility_PackageVulnerabilities}" />
</Grid>
</Border>
<!-- descriptions -->
<TextBlock
Grid.Row="0"
x:Name="_descriptionLabel"
Text="{x:Static nuget:Resources.Label_Description}"
FontWeight="Bold" />
<TextBox
Style="{DynamicResource SelectableTextBlockStyle}"
<!-- deprecation info -->
<Border
Grid.Row="1"
x:Name="_description"
AutomationProperties.LabeledBy="{Binding ElementName=_descriptionLabel}"
Margin="0,8,0,0"
TextWrapping="Wrap"
Text="{Binding Path=Description}" />
<!-- metadata -->
BorderBrush="{DynamicResource {x:Static SystemColors.ActiveBorderBrushKey}}"
Visibility="{Binding Path=IsPackageDeprecated,Converter={StaticResource BooleanToVisibilityConverter}}"
BorderThickness="0,0,0,1">
<Grid>
<nuget:DeprecationControl
x:Name="_deprecationControl"
DataContext="{Binding}"
AutomationProperties.Name="{x:Static nuget:Resources.Accessibility_PackageDeprecation}" />
</Grid>
</Border>
<Grid
Grid.Row="2"
x:Name="_metadataGrid"
Margin="0,8">
Grid.Row="2">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition
Height="Auto" />
<RowDefinition
Height="Auto" />
<RowDefinition
Height="Auto" />
<RowDefinition
Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition
Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<!-- version -->
<!-- descriptions -->
<TextBlock
Visibility="{Binding Path=Version,Converter={StaticResource NullToVisibilityConverter}}"
Grid.Row="0"
Grid.Column="0"
Margin="0,8,0,0"
FontWeight="Bold"
x:Name="_versionLabel"
AutomationProperties.Name="{Binding Text}"
Text="{x:Static nuget:Resources.Label_Version}" />
x:Name="_descriptionLabel"
Text="{x:Static nuget:Resources.Label_Description}"
FontWeight="Bold" />
<TextBox
Style="{DynamicResource SelectableTextBlockStyle}"
Grid.Row="0"
Grid.Column="1"
Visibility="{Binding Path=Version,Converter={StaticResource NullToVisibilityConverter}}"
AutomationProperties.LabeledBy="{Binding ElementName=_versionLabel}"
Text="{Binding Path=Version,Converter={StaticResource VersionToStringConverter},ConverterParameter=F}"
Margin="8,8,0,0"
TextWrapping="Wrap" />
<!-- Owner(s) -->
<TextBlock
Visibility="{Binding Path=KnownOwnerViewModels,Converter={StaticResource NullToVisibilityConverter}}"
Style="{DynamicResource SelectableTextBlockStyle}"
Grid.Row="1"
Grid.Column="0"
x:Name="_description"
AutomationProperties.LabeledBy="{Binding ElementName=_descriptionLabel}"
Margin="0,8,0,0"
FontWeight="Bold"
x:Name="_ownersLabel"
AutomationProperties.Name="{Binding Text}"
Text="{x:Static nuget:Resources.Label_Owners}" />
<ItemsControl
x:Name="_panelOwners"
Grid.Row="1"
Grid.Column="1"
ItemsSource="{Binding KnownOwnerViewModels}"
Visibility="{Binding Path=KnownOwnerViewModels,Converter={StaticResource NullToVisibilityConverter}}"
AutomationProperties.LabeledBy="{Binding ElementName=_ownersLabel}"
Focusable="False"
Margin="8,8,0,0"
AlternationCount="{Binding RelativeSource={RelativeSource Self}, Path=Items.Count}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock TextWrapping="Wrap">
<Hyperlink
NavigateUri="{Binding Link, Mode=OneWay}"
ToolTip="{Binding Link, Mode=OneWay}"
Style="{StaticResource HyperlinkStyle}"
AutomationProperties.Name="{x:Static nuget:Resources.Hyperlink_Owner}"
Command="{x:Static nuget:PackageManagerControlCommands.OpenExternalLink}"
CommandParameter="{x:Static nugettel:HyperlinkType.OwnerProfileDetailsPane}">
<Run Text="{Binding Name, Mode=OneTime}" />
</Hyperlink><TextBlock Margin="0,0,2,0" Text=",">
<TextBlock.Visibility>
<MultiBinding Converter="{StaticResource LastItemToVisibilityConverter}">
<Binding RelativeSource="{RelativeSource Mode=TemplatedParent}" Path="(ItemsControl.AlternationIndex)" />
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}" Path="Items.Count" />
</MultiBinding>
</TextBlock.Visibility>
TextWrapping="Wrap"
Text="{Binding Path=PackageMetadata.Description}" />
<!-- metadata -->
<Grid
Grid.Row="2"
x:Name="_metadataGrid"
Margin="0,8">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition
Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<!-- version -->
<TextBlock
Visibility="{Binding Path=PackageMetadata.Version,Converter={StaticResource NullToVisibilityConverter}}"
Grid.Row="0"
Grid.Column="0"
Margin="0,8,0,0"
FontWeight="Bold"
x:Name="_versionLabel"
AutomationProperties.Name="{Binding Text}"
Text="{x:Static nuget:Resources.Label_Version}" />
<TextBox
Style="{DynamicResource SelectableTextBlockStyle}"
Grid.Row="0"
Grid.Column="1"
Visibility="{Binding Path=PackageMetadata.Version,Converter={StaticResource NullToVisibilityConverter}}"
AutomationProperties.LabeledBy="{Binding ElementName=_versionLabel}"
Text="{Binding Path=PackageMetadata.Version,Converter={StaticResource VersionToStringConverter},ConverterParameter=F}"
Margin="8,8,0,0"
TextWrapping="Wrap" />
<!-- Owner(s) -->
<TextBlock
Visibility="{Binding Path=PackageMetadata.KnownOwnerViewModels,Converter={StaticResource NullToVisibilityConverter}}"
Grid.Row="1"
Grid.Column="0"
Margin="0,8,0,0"
FontWeight="Bold"
x:Name="_ownersLabel"
AutomationProperties.Name="{Binding Text}"
Text="{x:Static nuget:Resources.Label_Owners}" />
<ItemsControl
x:Name="_panelOwners"
Grid.Row="1"
Grid.Column="1"
ItemsSource="{Binding PackageMetadata.KnownOwnerViewModels}"
Visibility="{Binding Path=PackageMetadata.KnownOwnerViewModels,Converter={StaticResource NullToVisibilityConverter}}"
AutomationProperties.LabeledBy="{Binding ElementName=_ownersLabel}"
Focusable="False"
Margin="8,8,0,0"
AlternationCount="{Binding RelativeSource={RelativeSource Self}, Path=Items.Count}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock TextWrapping="Wrap">
<Hyperlink
NavigateUri="{Binding Link, Mode=OneWay}"
ToolTip="{Binding Link, Mode=OneWay}"
Style="{StaticResource HyperlinkStyle}"
AutomationProperties.Name="{x:Static nuget:Resources.Hyperlink_Owner}"
Command="{x:Static nuget:PackageManagerControlCommands.OpenExternalLink}"
CommandParameter="{x:Static nugettel:HyperlinkType.OwnerProfileDetailsPane}">
<Run Text="{Binding Name, Mode=OneTime}" />
</Hyperlink><TextBlock Margin="0,0,2,0" Text=",">
<TextBlock.Visibility>
<MultiBinding Converter="{StaticResource LastItemToVisibilityConverter}">
<Binding RelativeSource="{RelativeSource Mode=TemplatedParent}" Path="(ItemsControl.AlternationIndex)" />
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}" Path="Items.Count" />
</MultiBinding>
</TextBlock.Visibility>
</TextBlock>
</TextBlock>
</TextBlock>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- Author(s) -->
<TextBlock
Visibility="{Binding Path=Authors,Converter={StaticResource NullToVisibilityConverter}}"
Grid.Row="2"
Grid.Column="0"
Margin="0,8,0,0"
FontWeight="Bold"
x:Name="_authorsLabel"
AutomationProperties.Name="{Binding Text}"
Text="{x:Static nuget:Resources.Label_Authors}" />
<TextBox
Style="{DynamicResource SelectableTextBlockStyle}"
Visibility="{Binding Path=Authors,Converter={StaticResource NullToVisibilityConverter}}"
AutomationProperties.LabeledBy="{Binding ElementName=_authorsLabel}"
Text="{Binding Path=Authors}"
Margin="8,8,0,0"
TextWrapping="Wrap"
Grid.Row="2"
Grid.Column="1" />
<!-- Author(s) -->
<TextBlock
Visibility="{Binding Path=PackageMetadata.Authors,Converter={StaticResource NullToVisibilityConverter}}"
Grid.Row="2"
Grid.Column="0"
Margin="0,8,0,0"
FontWeight="Bold"
x:Name="_authorsLabel"
AutomationProperties.Name="{Binding Text}"
Text="{x:Static nuget:Resources.Label_Authors}" />
<TextBox
Style="{DynamicResource SelectableTextBlockStyle}"
Visibility="{Binding Path=PackageMetadata.Authors,Converter={StaticResource NullToVisibilityConverter}}"
AutomationProperties.LabeledBy="{Binding ElementName=_authorsLabel}"
Text="{Binding Path=PackageMetadata.Authors}"
Margin="8,8,0,0"
TextWrapping="Wrap"
Grid.Row="2"
Grid.Column="1" />
<!-- License -->
<TextBlock
Visibility="{Binding Path=LicenseUrl,Converter={StaticResource NullToVisibilityConverter}}"
Grid.Row="3"
Grid.Column="0"
FontWeight="Bold"
Margin="0,8,0,0"
x:Name="_licenseLabel"
AutomationProperties.Name="{Binding Text}"
Text="{x:Static nuget:Resources.Label_License}"/>
<!-- License -->
<TextBlock
Visibility="{Binding Path=PackageMetadata.LicenseUrl,Converter={StaticResource NullToVisibilityConverter}}"
Grid.Row="3"
Grid.Column="0"
FontWeight="Bold"
Margin="0,8,0,0"
x:Name="_licenseLabel"
AutomationProperties.Name="{Binding Text}"
Text="{x:Static nuget:Resources.Label_License}"/>
<ItemsControl Grid.Row="3" Grid.Column="1" ItemsSource="{Binding LicenseLinks}" IsTabStop="False">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Margin="8,8,0,0" Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<ItemsControl Grid.Row="3" Grid.Column="1" ItemsSource="{Binding PackageMetadata.LicenseLinks}" IsTabStop="False">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Margin="8,8,0,0" Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<!-- Readme -->
<TextBlock
Visibility="{Binding Path=ReadmeUrl,Converter={StaticResource NullToVisibilityConverter}}"
Grid.Row="4"
Grid.Column="0"
FontWeight="Bold"
Margin="0,8,0,0"
x:Name="_readmeLabel"
AutomationProperties.Name="{Binding Text}"
Text="{x:Static nuget:Resources.Label_Readme}"/>
<!-- Readme -->
<TextBlock
Visibility="{Binding Path=PackageMetadata.ReadmeUrl,Converter={StaticResource NullToVisibilityConverter}}"
Grid.Row="4"
Grid.Column="0"
FontWeight="Bold"
Margin="0,8,0,0"
x:Name="_readmeLabel"
AutomationProperties.Name="{Binding Text}"
Text="{x:Static nuget:Resources.Label_Readme}"/>
<TextBlock
Visibility="{Binding Path=ReadmeUrl,Converter={StaticResource NullToVisibilityConverter}}"
TextWrapping="Wrap"
Margin="8,8,0,0"
Grid.Row="4"
Grid.Column="1">
<Hyperlink
NavigateUri="{Binding Path=ReadmeUrl}"
Style="{StaticResource HyperlinkStyle}"
Command="{x:Static nuget:PackageManagerControlCommands.OpenExternalLink}"
CommandParameter="{x:Static nugettel:HyperlinkType.Readme}"
ToolTip="{Binding RelativeSource={RelativeSource Self}, Path=NavigateUri}"
AutomationProperties.Name="{x:Static nuget:Resources.Hyperlink_Readme}">
<Run Text="{x:Static nuget:Resources.Text_ViewReadme}" />
</Hyperlink>
</TextBlock>
<TextBlock
Visibility="{Binding Path=PackageMetadata.ReadmeUrl,Converter={StaticResource NullToVisibilityConverter}}"
TextWrapping="Wrap"
Margin="8,8,0,0"
Grid.Row="4"
Grid.Column="1">
<Hyperlink
NavigateUri="{Binding Path=PackageMetadata.ReadmeUrl}"
Style="{StaticResource HyperlinkStyle}"
Command="{x:Static nuget:PackageManagerControlCommands.OpenExternalLink}"
CommandParameter="{x:Static nugettel:HyperlinkType.Readme}"
ToolTip="{Binding RelativeSource={RelativeSource Self}, Path=NavigateUri}"
AutomationProperties.Name="{x:Static nuget:Resources.Hyperlink_Readme}">
<Run Text="{x:Static nuget:Resources.Text_ViewReadme}" />
</Hyperlink>
</TextBlock>
<!-- downloads -->
<TextBlock
Visibility="{Binding Path=DownloadCount,Converter={StaticResource GreaterThanThresholdToVisibilityConverter},ConverterParameter=-1}"
Grid.Row="5"
Grid.Column="0"
FontWeight="Bold"
Margin="0,8,0,0"
x:Name="_downloadsLabel"
Text="{x:Static nuget:Resources.Label_Downloads}" />
<TextBox
Style="{DynamicResource SelectableTextBlockStyle}"
Visibility="{Binding Path=DownloadCount,Converter={StaticResource GreaterThanThresholdToVisibilityConverter},ConverterParameter=-1}"
AutomationProperties.LabeledBy="{Binding ElementName=_downloadsLabel}"
Text="{Binding Path=DownloadCount,StringFormat={}{0:N0}}"
Margin="8,8,0,0"
TextWrapping="Wrap"
Grid.Row="5"
Grid.Column="1" />
<!-- downloads -->
<TextBlock
Visibility="{Binding Path=PackageMetadata.DownloadCount,Converter={StaticResource GreaterThanThresholdToVisibilityConverter},ConverterParameter=-1}"
Grid.Row="5"
Grid.Column="0"
FontWeight="Bold"
Margin="0,8,0,0"
x:Name="_downloadsLabel"
Text="{x:Static nuget:Resources.Label_Downloads}" />
<TextBox
Style="{DynamicResource SelectableTextBlockStyle}"
Visibility="{Binding Path=PackageMetadata.DownloadCount,Converter={StaticResource GreaterThanThresholdToVisibilityConverter},ConverterParameter=-1}"
AutomationProperties.LabeledBy="{Binding ElementName=_downloadsLabel}"
Text="{Binding Path=PackageMetadata.DownloadCount,StringFormat={}{0:N0}}"
Margin="8,8,0,0"
TextWrapping="Wrap"
Grid.Row="5"
Grid.Column="1" />
<!--Date Published -->
<TextBlock
Visibility="{Binding Path=Published,Converter={StaticResource NullToVisibilityConverter}}"
Grid.Row="6"
Grid.Column="0"
FontWeight="Bold"
Margin="0,8,0,0"
x:Name="_datePublishedLabel"
Text="{x:Static nuget:Resources.Label_DatePublished}" />
<TextBox
Name="datePublished"
Style="{DynamicResource SelectableTextBlockStyle}"
Visibility="{Binding Path=Published,Converter={StaticResource NullToVisibilityConverter}}"
Text="{Binding Path=Published,ConverterCulture={x:Static glob:CultureInfo.CurrentCulture},StringFormat={}{0:D} ({0:d})}"
AutomationProperties.LabeledBy="{Binding ElementName=_datePublishedLabel}"
Margin="8,8,0,0"
TextWrapping="Wrap"
Grid.Row="6"
Grid.Column="1" />
<!--Date Published -->
<TextBlock
Visibility="{Binding Path=PackageMetadata.Published,Converter={StaticResource NullToVisibilityConverter}}"
Grid.Row="6"
Grid.Column="0"
FontWeight="Bold"
Margin="0,8,0,0"
x:Name="_datePublishedLabel"
Text="{x:Static nuget:Resources.Label_DatePublished}" />
<TextBox
Name="datePublished"
Style="{DynamicResource SelectableTextBlockStyle}"
Visibility="{Binding Path=PackageMetadata.Published,Converter={StaticResource NullToVisibilityConverter}}"
Text="{Binding Path=PackageMetadata.Published,ConverterCulture={x:Static glob:CultureInfo.CurrentCulture},StringFormat={}{0:D} ({0:d})}"
AutomationProperties.LabeledBy="{Binding ElementName=_datePublishedLabel}"
Margin="8,8,0,0"
TextWrapping="Wrap"
Grid.Row="6"
Grid.Column="1" />
<!-- Project Url -->
<TextBlock
Visibility="{Binding Path=ProjectUrl,Converter={StaticResource NullToVisibilityConverter}}"
Grid.Row="7"
Grid.Column="0"
FontWeight="Bold"
Margin="0,8,0,0"
x:Name="_projectUrlLabel"
AutomationProperties.Name="{Binding Text}"
Text="{x:Static nuget:Resources.Label_ProjectUrl}" />
<!-- Project Url -->
<TextBlock
Visibility="{Binding Path=PackageMetadata.ProjectUrl,Converter={StaticResource NullToVisibilityConverter}}"
Grid.Row="7"
Grid.Column="0"
FontWeight="Bold"
Margin="0,8,0,0"
x:Name="_projectUrlLabel"
AutomationProperties.Name="{Binding Text}"
Text="{x:Static nuget:Resources.Label_ProjectUrl}" />
<TextBlock
Visibility="{Binding Path=ProjectUrl,Converter={StaticResource NullToVisibilityConverter}}"
TextWrapping="Wrap"
Margin="8,8,0,0"
Grid.Row="7"
Grid.Column="1">
<Hyperlink
NavigateUri="{Binding Path=ProjectUrl}"
Style="{StaticResource HyperlinkStyle}"
Command="{x:Static nuget:PackageManagerControlCommands.OpenExternalLink}"
CommandParameter="{x:Static nugettel:HyperlinkType.ProjectUri}"
AutomationProperties.Name="{x:Static nuget:Resources.Hyperlink_ProjectUrl}">
<Run Text="{Binding Path=ProjectUrl}" />
</Hyperlink>
</TextBlock>
<TextBlock
Visibility="{Binding Path=PackageMetadata.ProjectUrl,Converter={StaticResource NullToVisibilityConverter}}"
TextWrapping="Wrap"
Margin="8,8,0,0"
Grid.Row="7"
Grid.Column="1">
<Hyperlink
NavigateUri="{Binding Path=PackageMetadata.ProjectUrl}"
Style="{StaticResource HyperlinkStyle}"
Command="{x:Static nuget:PackageManagerControlCommands.OpenExternalLink}"
CommandParameter="{x:Static nugettel:HyperlinkType.ProjectUri}"
AutomationProperties.Name="{x:Static nuget:Resources.Hyperlink_ProjectUrl}">
<Run Text="{Binding Path=PackageMetadata.ProjectUrl}" />
</Hyperlink>
</TextBlock>
<!-- Report abuse Url -->
<TextBlock
Visibility="{Binding Path=ReportAbuseUrl,Converter={StaticResource NullToVisibilityConverter}}"
Grid.Row="8"
Grid.Column="0"
FontWeight="Bold"
Margin="0,8,0,0"
x:Name="_reportAbuseLabel"
Text="{x:Static nuget:Resources.Label_ReportAbuse}" />
<!-- Report abuse Url -->
<TextBlock
Visibility="{Binding Path=PackageMetadata.ReportAbuseUrl,Converter={StaticResource NullToVisibilityConverter}}"
Grid.Row="8"
Grid.Column="0"
FontWeight="Bold"
Margin="0,8,0,0"
x:Name="_reportAbuseLabel"
Text="{x:Static nuget:Resources.Label_ReportAbuse}" />
<TextBlock
Visibility="{Binding Path=ReportAbuseUrl,Converter={StaticResource NullToVisibilityConverter}}"
TextWrapping="Wrap"
Margin="8,8,0,0"
Grid.Row="8"
Grid.Column="1">
<Hyperlink
NavigateUri="{Binding Path=ReportAbuseUrl}"
Style="{StaticResource HyperlinkStyle}"
Command="{x:Static nuget:PackageManagerControlCommands.OpenExternalLink}"
CommandParameter="{x:Static nugettel:HyperlinkType.ReportAbuse}"
AutomationProperties.Name="{x:Static nuget:Resources.Hyperlink_ReportAbuse}">
<Run Text="{Binding Path=ReportAbuseUrl}" />
</Hyperlink>
</TextBlock>
<TextBlock
Visibility="{Binding Path=PackageMetadata.ReportAbuseUrl,Converter={StaticResource NullToVisibilityConverter}}"
TextWrapping="Wrap"
Margin="8,8,0,0"
Grid.Row="8"
Grid.Column="1">
<Hyperlink
NavigateUri="{Binding Path=PackageMetadata.ReportAbuseUrl}"
Style="{StaticResource HyperlinkStyle}"
Command="{x:Static nuget:PackageManagerControlCommands.OpenExternalLink}"
CommandParameter="{x:Static nugettel:HyperlinkType.ReportAbuse}"
AutomationProperties.Name="{x:Static nuget:Resources.Hyperlink_ReportAbuse}">
<Run Text="{Binding Path=PackageMetadata.ReportAbuseUrl}" />
</Hyperlink>
</TextBlock>
<!-- Tags -->
<TextBlock
Visibility="{Binding Path=Tags,Converter={StaticResource NullToVisibilityConverter}}"
Grid.Row="9"
Grid.Column="0"
FontWeight="Bold"
Margin="0,8,0,0"
x:Name="_tagsLabel"
Text="{x:Static nuget:Resources.Label_Tags}" />
<TextBox
Style="{DynamicResource SelectableTextBlockStyle}"
Visibility="{Binding Path=Tags,Converter={StaticResource NullToVisibilityConverter}}"
AutomationProperties.LabeledBy="{Binding ElementName=_tagsLabel}"
Text="{Binding Path=Tags}"
Margin="8,8,0,0"
TextWrapping="Wrap"
Grid.Row="9"
Grid.Column="1" />
</Grid>
<!-- Tags -->
<TextBlock
Visibility="{Binding Path=PackageMetadata.Tags,Converter={StaticResource NullToVisibilityConverter}}"
Grid.Row="9"
Grid.Column="0"
FontWeight="Bold"
Margin="0,8,0,0"
x:Name="_tagsLabel"
Text="{x:Static nuget:Resources.Label_Tags}" />
<TextBox
Style="{DynamicResource SelectableTextBlockStyle}"
Visibility="{Binding Path=PackageMetadata.Tags,Converter={StaticResource NullToVisibilityConverter}}"
AutomationProperties.LabeledBy="{Binding ElementName=_tagsLabel}"
Text="{Binding Path=PackageMetadata.Tags}"
Margin="8,8,0,0"
TextWrapping="Wrap"
Grid.Row="9"
Grid.Column="1" />
</Grid>
<!-- dependencies treeview -->
<TreeView
<!-- dependencies treeview -->
<TreeView
x:Name="_dependencies"
Grid.Row="3"
Margin="-21,0,0,0"
@@ -385,28 +419,29 @@
LostFocus="TreeView_LostFocus"
AutomationProperties.Name="{x:Static nuget:Resources.Label_Dependencies}"
FocusVisualStyle="{StaticResource ControlsFocusVisualStyle}">
<TreeView.Template>
<!--
<TreeView.Template>
<!--
Replaces default control template for this treeview with a slimmer control template.
Specifically, this one has no ScrollViewer...which wasn't being used, and was making mousewheel not work for
the larget detail scrollable area while the mouse was over the treeview.
-->
<ControlTemplate TargetType="TreeView">
<ItemsPresenter />
</ControlTemplate>
</TreeView.Template>
<TreeView.Resources>
<HierarchicalDataTemplate ItemsSource="{Binding Dependencies}" DataType="{x:Type nuget:PackageDependencySetMetadata}">
<TextBlock Text="{Binding TargetFrameworkDisplay}" Padding="0,2,0,0" />
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type nuget:PackageDependencyMetadata}">
<TextBlock Text="{Binding}" ToolTip="{Binding}" />
</DataTemplate>
</TreeView.Resources>
<TreeViewItem Header="{x:Static nuget:Resources.Label_Dependencies}"
ItemsSource="{Binding Path=DependencySets}"
Style="{StaticResource TreeViewItemHeaderStyle}">
</TreeViewItem>
</TreeView>
-->
<ControlTemplate TargetType="TreeView">
<ItemsPresenter />
</ControlTemplate>
</TreeView.Template>
<TreeView.Resources>
<HierarchicalDataTemplate ItemsSource="{Binding Dependencies}" DataType="{x:Type nuget:PackageDependencySetMetadata}">
<TextBlock Text="{Binding TargetFrameworkDisplay}" Padding="0,2,0,0" />
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type nuget:PackageDependencyMetadata}">
<TextBlock Text="{Binding}" ToolTip="{Binding}" />
</DataTemplate>
</TreeView.Resources>
<TreeViewItem Header="{x:Static nuget:Resources.Label_Dependencies}"
ItemsSource="{Binding Path=PackageMetadata.DependencySets}"
Style="{StaticResource TreeViewItemHeaderStyle}">
</TreeViewItem>
</TreeView>
</Grid>
</Grid>
</UserControl>
@@ -27,8 +27,9 @@ namespace NuGet.PackageManagement.UI
private void ViewLicense_Click(object sender, RoutedEventArgs e)
{
if (DataContext is DetailedPackageMetadata metadata)
if (DataContext is DetailControlModel detailControlModel)
{
var metadata = detailControlModel.PackageMetadata;
var window = new LicenseFileWindow()
{
DataContext = new LicenseFileData
@@ -46,7 +47,7 @@ namespace NuGet.PackageManagement.UI
flowDoc.Blocks.AddRange(PackageLicenseUtilities.GenerateParagraphs(content));
await NuGetUIThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
(window.DataContext as LicenseFileData).LicenseText = flowDoc;
}).PostOnFailure(nameof(PackageMetadataControl), nameof(ViewLicense_Click));
}).PostOnFailure(nameof(PackageDetailsTabControl), nameof(ViewLicense_Click));
window.ShowModal();
}
@@ -54,7 +55,7 @@ namespace NuGet.PackageManagement.UI
private void PackageMetadataControl_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (DataContext is DetailedPackageMetadata)
if (DataContext is DetailControlModel)
{
Visibility = Visibility.Visible;
}
@@ -0,0 +1,39 @@
<UserControl
x:Class="NuGet.PackageManagement.UI.PackageReadmeControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:nuget="clr-namespace:NuGet.PackageManagement.UI"
xmlns:imaging="clr-namespace:Microsoft.VisualStudio.Imaging;assembly=Microsoft.VisualStudio.Imaging"
xmlns:imagingTheme="clr-namespace:Microsoft.VisualStudio.PlatformUI;assembly=Microsoft.VisualStudio.Imaging"
xmlns:catalog="clr-namespace:Microsoft.VisualStudio.Imaging;assembly=Microsoft.VisualStudio.ImageCatalog"
xmlns:ui="clr-namespace:Microsoft.VisualStudio.PlatformUI;assembly=Microsoft.VisualStudio.Utilities"
DataContextChanged="UserControl_DataContextChanged"
Unloaded="PackageReadmeControl_Unloaded"
Loaded="PackageReadmeControl_Loaded"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="300">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<nuget:SharedResources/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<ContentControl
x:Name="descriptionMarkdownPreview"
x:Uid="descriptionMarkdownPreview"
ClipToBounds="True"
Focusable="False"
Visibility="{Binding Path=ErrorLoadingReadme, Mode=OneWay, Converter={StaticResource NegatedBooleanToVisibilityConverter}}"
/>
<TextBlock
TextWrapping="Wrap"
Text="{x:Static nuget:Resources.Error_UnableToLoadReadme}"
Visibility="{Binding Path=ErrorLoadingReadme, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}"
/>
</Grid>
</UserControl>
@@ -0,0 +1,113 @@
// 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.ComponentModel;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using Microsoft.VisualStudio.Markdown.Platform;
using Microsoft.VisualStudio.Shell;
using NuGet.PackageManagement.UI.ViewModels;
using NuGet.VisualStudio;
using NuGet.VisualStudio.Telemetry;
namespace NuGet.PackageManagement.UI
{
/// <summary>
/// Interaction logic for PackageReadmeControl.xaml
/// </summary>
public partial class PackageReadmeControl : UserControl, IDisposable
{
#pragma warning disable CS0618 // Type or member is obsolete
private IMarkdownPreview _markdownPreview;
#pragma warning restore CS0618 // Type or member is obsolete
private bool _disposed = false;
public PackageReadmeControl()
{
InitializeComponent();
#pragma warning disable CS0618 // Type or member is obsolete
_markdownPreview = new PreviewBuilder().Build();
#pragma warning restore CS0618 // Type or member is obsolete
descriptionMarkdownPreview.Content = _markdownPreview.VisualElement;
}
public ReadmePreviewViewModel ReadmeViewModel { get => (ReadmePreviewViewModel)DataContext; }
private void ReadmeViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(ReadmePreviewViewModel.ReadmeMarkdown))
{
NuGetUIThreadHelper.JoinableTaskFactory.Run(UpdateMarkdownAsync);
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing)
{
_markdownPreview?.Dispose();
if (ReadmeViewModel is not null)
{
ReadmeViewModel.PropertyChanged -= ReadmeViewModel_PropertyChanged;
}
}
_disposed = true;
}
private async Task UpdateMarkdownAsync()
{
try
{
if (!string.IsNullOrWhiteSpace(ReadmeViewModel.ReadmeMarkdown))
{
await _markdownPreview.UpdateContentAsync(ReadmeViewModel.ReadmeMarkdown, ScrollHint.None);
}
}
catch (Exception ex) when (ex is ArgumentException || ex is InvalidOperationException)
{
ReadmeViewModel.ErrorLoadingReadme = true;
ReadmeViewModel.ReadmeMarkdown = string.Empty;
await TelemetryUtility.PostFaultAsync(ex, nameof(ReadmePreviewViewModel));
}
}
private void UserControl_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (e.OldValue is ReadmePreviewViewModel oldMetadata)
{
oldMetadata.PropertyChanged -= ReadmeViewModel_PropertyChanged;
}
if (ReadmeViewModel is not null)
{
ReadmeViewModel.PropertyChanged += ReadmeViewModel_PropertyChanged;
}
}
private void PackageReadmeControl_Unloaded(object sender, RoutedEventArgs e)
{
Dispose();
}
private void PackageReadmeControl_Loaded(object sender, RoutedEventArgs e)
{
ThreadHelper.JoinableTaskFactory.Run(async () =>
{
await UpdateMarkdownAsync();
});
}
}
}
@@ -18,12 +18,16 @@
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<StackPanel Orientation="Horizontal">
<imaging:CrispImage
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<imaging:CrispImage
Grid.Column="0"
Margin="0,0,2,0"
AutomationProperties.Name="{Binding MappingStatus, Mode=OneWay}"
Moniker="{Binding MappingStatusIcon, Mode=OneWay}" />
<TextBlock x:Name="_textStatus" Margin="0,0,9,0" TextWrapping="Wrap">
<TextBlock Grid.Column="1" x:Name="_textStatus" Margin="0,0,9,0" TextWrapping="Wrap">
<Run Text="{Binding MappingStatus, Mode=OneWay}" />
<Control Margin="6,0,0,0" IsTabStop="False" />
<controls:ButtonHyperlink Click="SettingsButtonClicked"
@@ -32,6 +36,5 @@
<TextBlock TextWrapping="Wrap" Text="{x:Static nuget:Resources.Hyperlink_PackageSourceMappingSettings}" />
</controls:ButtonHyperlink>
</TextBlock>
</StackPanel>
</Grid>
</UserControl>
@@ -7,6 +7,7 @@ using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.Services.Common;
using NuGet.Common;
using NuGet.Packaging.Core;
using NuGet.ProjectManagement;
@@ -255,15 +256,38 @@ namespace NuGet.PackageManagement.VisualStudio
tasks.Add(_localRepository.GetPackageMetadataFromLocalSourceAsync(identity, cancellationToken));
}
if (_globalLocalRepositories != null)
{
_globalLocalRepositories.ForEach(x =>
tasks.Add(x.GetPackageMetadataFromLocalSourceAsync(identity, cancellationToken)));
}
IEnumerable<IPackageSearchMetadata> completed = (await Task.WhenAll(tasks))
.Where(m => m != null);
IPackageSearchMetadata master = completed.FirstOrDefault(m => !string.IsNullOrEmpty(m.Summary))
IPackageSearchMetadata packageSearchMetadataBase = (completed.FirstOrDefault(m => !string.IsNullOrEmpty(m.Summary))
?? completed.FirstOrDefault()
?? PackageSearchMetadataBuilder.FromIdentity(identity).Build();
?? PackageSearchMetadataBuilder.FromIdentity(identity).Build()).WithVersions(() => MergeVersionsAsync(identity, completed));
return master.WithVersions(
asyncValueFactory: () => MergeVersionsAsync(identity, completed));
var clonedResult = packageSearchMetadataBase as PackageSearchMetadataBuilder.ClonedPackageSearchMetadata;
if (clonedResult == null)
{
return packageSearchMetadataBase;
}
if (string.IsNullOrWhiteSpace(clonedResult.ReadmeFileUrl))
{
clonedResult.ReadmeFileUrl = completed.Select(m => m.ReadmeFileUrl).FirstOrDefault(u => !string.IsNullOrWhiteSpace(u));
}
if (string.IsNullOrWhiteSpace(clonedResult.PackagePath))
{
clonedResult.PackagePath = completed.Select(m => (m as PackageSearchMetadataBuilder.ClonedPackageSearchMetadata)?.PackagePath).FirstOrDefault(uri => !string.IsNullOrWhiteSpace(uri));
}
return clonedResult;
}
private static async Task<IEnumerable<VersionInfo>> MergeVersionsAsync(PackageIdentity identity, IEnumerable<IPackageSearchMetadata> packages)
@@ -164,6 +164,23 @@ namespace NuGet.PackageManagement.VisualStudio
return stream;
}
public async ValueTask<Stream?> GetReadmeAsync(Uri readmeUri, CancellationToken cancellationToken)
{
Assumes.NotNull(readmeUri);
Stream? stream;
if (IsEmbeddedUri(readmeUri))
{
stream = await GetEmbeddedFileAsync(readmeUri, cancellationToken);
}
else
{
stream = await GetStream(readmeUri);
}
return stream;
}
private async ValueTask<Stream?> GetEmbeddedFileAsync(Uri uri, CancellationToken cancellationToken)
{
string packagePath = uri.LocalPath;
@@ -5,6 +5,7 @@
Azure.Core.dll
Ben.Demystifier.dll
Markdig.Signed.dll
Microsoft.DataAI.NuGetRecommender.Contracts.dll
Microsoft.IO.Redist.dll
Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll
@@ -76,6 +77,9 @@ Microsoft.VisualStudio.Debugger.UI.Interfaces.dll
Microsoft.VisualStudio.Extensibility.Editor.Contracts.dll
Microsoft.VisualStudio.HotReload.Components.dll
Microsoft.VisualStudio.Linux.ConnectionManager.Store.dll
Microsoft.VisualStudio.Markdown.Core.dll
Microsoft.VisualStudio.Markdown.Platform.dll
Microsoft.VisualStudio.Markdown.Platform.resources.dll
Microsoft.VisualStudio.ProjectSystem.Managed.VS.dll
Microsoft.VisualStudio.ProjectSystem.Managed.VS.resources.dll
Microsoft.VisualStudio.ProjectSystem.Managed.dll
@@ -85,6 +89,7 @@ Microsoft.VisualStudio.ProjectSystem.Query.resources.dll
Microsoft.VisualStudio.Services.TestManagement.TestPlanning.WebApi.dll
Microsoft.VisualStudio.Services.TestResults.WebApi.dll
System.Configuration.ConfigurationManager.dll
System.Drawing.Common.dll
System.IdentityModel.Tokens.Jwt.dll
System.Management.Automation.dll
System.Memory.Data.dll
@@ -32,5 +32,6 @@ namespace NuGet.VisualStudio
public static readonly NuGetFeatureFlagConstants BulkRestoreCoordination = new("NuGet.BulkRestoreCoordination", "NUGET_BULK_RESTORE_COORDINATION", defaultState: true);
public static readonly NuGetFeatureFlagConstants NuGetSolutionCacheInitilization = new("NuGet.SolutionCacheInitialization", "NUGET_SOLUTION_CACHE_INITIALIZATION", defaultState: true);
public static readonly NuGetFeatureFlagConstants RenderReadmeInPMUI = new("NuGet.RenderReadmeInPMUI", "NUGET_RENDER_README_IN_PMUI", defaultState: true);
}
}
@@ -21,6 +21,7 @@ namespace NuGet.VisualStudio.Internal.Contracts
public Uri? IconUrl { get; internal set; }
public string? Tags { get; internal set; }
public Uri? LicenseUrl { get; internal set; }
public string? ReadmeFileUrl { get; internal set; }
public Uri? ReadmeUrl { get; internal set; }
public Uri? ProjectUrl { get; internal set; }
public DateTimeOffset? Published { get; internal set; }
@@ -59,6 +60,7 @@ namespace NuGet.VisualStudio.Internal.Contracts
Tags = packageSearchMetadata.Tags,
Identity = packageSearchMetadata.Identity,
LicenseUrl = packageSearchMetadata.LicenseUrl,
ReadmeFileUrl = packageSearchMetadata.ReadmeFileUrl,
ReadmeUrl = packageSearchMetadata.ReadmeUrl,
LicenseMetadata = packageSearchMetadata.LicenseMetadata,
IsRecommended = recommendedPackageSearchMetadata?.IsRecommended ?? false,
@@ -15,5 +15,6 @@ namespace NuGet.VisualStudio.Internal.Contracts
{
ValueTask<Stream?> GetPackageIconAsync(PackageIdentity packageIdentity, CancellationToken cancellationToken);
ValueTask<Stream?> GetEmbeddedLicenseAsync(PackageIdentity packageIdentity, CancellationToken cancellationToken);
ValueTask<Stream?> GetReadmeAsync(Uri readmeUri, CancellationToken cancellationToken);
}
}
@@ -33,6 +33,7 @@ namespace NuGet.VisualStudio.Internal.Contracts
public PackageIdentity Identity => _inner.Identity;
public Uri LicenseUrl => _inner.LicenseUrl;
public Uri ProjectUrl => _inner.ProjectUrl;
public string ReadmeFileUrl => _inner.ReadmeFileUrl;
public Uri ReadmeUrl => _inner.ReadmeUrl;
public Uri ReportAbuseUrl => _inner.ReportAbuseUrl;
public Uri PackageDetailsUrl => _inner.PackageDetailsUrl;
@@ -33,6 +33,8 @@ namespace NuGet.VisualStudio.Internal.Contracts
public Uri ReadmeUrl => _packageSearchMetadata.ReadmeUrl;
public string ReadmeFileUrl => _packageSearchMetadata.ReadmeFileUrl;
public Uri ReportAbuseUrl => _packageSearchMetadata.ReportAbuseUrl;
public Uri PackageDetailsUrl => _packageSearchMetadata.PackageDetailsUrl;
@@ -37,6 +37,7 @@ namespace NuGet.Protocol
public const string LicenseExpressionVersion = "licenseExpressionVersion";
public const string ProjectUrl = "projectUrl";
public const string ReadmeUrl = "readmeUrl";
public const string ReadmeFileUrl = "readmeFileUrl";
public const string Tags = "tags";
public const string DownloadCount = "totalDownloads";
public const string Created = "created";
@@ -24,6 +24,7 @@ namespace NuGet.Protocol.Core.Types
Uri LicenseUrl { get; }
Uri ProjectUrl { get; }
Uri ReadmeUrl { get; }
string ReadmeFileUrl { get; }
Uri ReportAbuseUrl { get; }
Uri PackageDetailsUrl { get; }
DateTimeOffset? Published { get; }
@@ -38,6 +38,8 @@ namespace NuGet.Protocol
public Uri IconUrl => GetIconUri();
public string ReadmeFileUrl => GetReadmeUri();
public PackageIdentity Identity => _nuspec.GetIdentity();
public Uri LicenseUrl => Convert(_nuspec.GetLicenseUrl());
@@ -206,5 +208,23 @@ namespace NuGet.Protocol
// get the special icon url
return builder.Uri;
}
private string GetReadmeUri()
{
string embeddedReadme = _nuspec.GetReadme();
if (embeddedReadme == null)
{
return null;
}
var baseUri = Convert(_package.Path);
UriBuilder builder = new UriBuilder(baseUri)
{
Fragment = embeddedReadme
};
// get the readme url
return builder.Uri.AbsoluteUri;
}
}
}
@@ -106,6 +106,9 @@ namespace NuGet.Protocol
[JsonConverter(typeof(SafeUriConverter))]
public Uri ReadmeUrl { get; private set; }
[JsonProperty(PropertyName = JsonProperties.ReadmeFileUrl)]
public string ReadmeFileUrl { get; private set; }
[JsonIgnore]
public Uri ReportAbuseUrl { get; set; }
@@ -40,6 +40,7 @@ namespace NuGet.Protocol.Core.Types
public Uri ProjectUrl { get; set; }
public DateTimeOffset? Published { get; set; }
public Uri ReadmeUrl { get; set; }
public string ReadmeFileUrl { get; set; }
public Uri ReportAbuseUrl { get; set; }
public Uri PackageDetailsUrl { get; set; }
public bool RequireLicenseAcceptance { get; set; }
@@ -98,6 +99,7 @@ namespace NuGet.Protocol.Core.Types
ProjectUrl = _metadata.ProjectUrl,
Published = _metadata.Published,
ReadmeUrl = _metadata.ReadmeUrl,
ReadmeFileUrl = _metadata.ReadmeFileUrl,
ReportAbuseUrl = _metadata.ReportAbuseUrl,
PackageDetailsUrl = _metadata.PackageDetailsUrl,
RequireLicenseAcceptance = _metadata.RequireLicenseAcceptance,
@@ -105,6 +105,8 @@ namespace NuGet.Protocol
public Uri ReadmeUrl { get; } = null; // The ReadmeUrl has not been added to the V2 feed.
public string ReadmeFileUrl { get; } = null; // The ReadmeFileUrl has not been added to the V2 feed.
public Uri ReportAbuseUrl { get; private set; }
public Uri PackageDetailsUrl { get; private set; }
@@ -1,2 +1,9 @@
#nullable enable
~NuGet.Protocol.Plugins.PluginDiscoverer.PluginDiscoverer(string rawPluginPaths) -> void
~const NuGet.Protocol.JsonProperties.ReadmeFileUrl = "readmeFileUrl" -> string
~NuGet.Protocol.Core.Types.IPackageSearchMetadata.ReadmeFileUrl.get -> string
~NuGet.Protocol.Core.Types.PackageSearchMetadataBuilder.ClonedPackageSearchMetadata.ReadmeFileUrl.get -> string
~NuGet.Protocol.Core.Types.PackageSearchMetadataBuilder.ClonedPackageSearchMetadata.ReadmeFileUrl.set -> void
~NuGet.Protocol.LocalPackageSearchMetadata.ReadmeFileUrl.get -> string
~NuGet.Protocol.PackageSearchMetadata.ReadmeFileUrl.get -> string
~NuGet.Protocol.PackageSearchMetadataV2Feed.ReadmeFileUrl.get -> string
@@ -1,2 +1,9 @@
#nullable enable
~NuGet.Protocol.Plugins.PluginDiscoverer.PluginDiscoverer(string rawPluginPaths) -> void
~const NuGet.Protocol.JsonProperties.ReadmeFileUrl = "readmeFileUrl" -> string
~NuGet.Protocol.Core.Types.IPackageSearchMetadata.ReadmeFileUrl.get -> string
~NuGet.Protocol.Core.Types.PackageSearchMetadataBuilder.ClonedPackageSearchMetadata.ReadmeFileUrl.get -> string
~NuGet.Protocol.Core.Types.PackageSearchMetadataBuilder.ClonedPackageSearchMetadata.ReadmeFileUrl.set -> void
~NuGet.Protocol.LocalPackageSearchMetadata.ReadmeFileUrl.get -> string
~NuGet.Protocol.PackageSearchMetadata.ReadmeFileUrl.get -> string
~NuGet.Protocol.PackageSearchMetadataV2Feed.ReadmeFileUrl.get -> string
@@ -1,2 +1,9 @@
#nullable enable
~NuGet.Protocol.Plugins.PluginDiscoverer.PluginDiscoverer(string rawPluginPaths) -> void
~const NuGet.Protocol.JsonProperties.ReadmeFileUrl = "readmeFileUrl" -> string
~NuGet.Protocol.Core.Types.IPackageSearchMetadata.ReadmeFileUrl.get -> string
~NuGet.Protocol.Core.Types.PackageSearchMetadataBuilder.ClonedPackageSearchMetadata.ReadmeFileUrl.get -> string
~NuGet.Protocol.Core.Types.PackageSearchMetadataBuilder.ClonedPackageSearchMetadata.ReadmeFileUrl.set -> void
~NuGet.Protocol.LocalPackageSearchMetadata.ReadmeFileUrl.get -> string
~NuGet.Protocol.PackageSearchMetadata.ReadmeFileUrl.get -> string
~NuGet.Protocol.PackageSearchMetadataV2Feed.ReadmeFileUrl.get -> string
@@ -41,7 +41,7 @@ namespace NuGet.PackageManagement.UI.Test
{
IReadOnlyList<PropertyInfo> properties = GetSerializableProperties<UserSettings>();
Assert.Equal(12, properties.Count);
Assert.Equal(13, properties.Count);
}
[Fact]
@@ -0,0 +1,166 @@
// 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.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.Sdk.TestFramework;
using Moq;
using NuGet.PackageManagement.UI.ViewModels;
using NuGet.VisualStudio.Internal.Contracts;
using Xunit;
namespace NuGet.PackageManagement.UI.Test.ViewModels
{
[Collection(MockedVS.Collection)]
public class ReadmePreviewViewModelTests
{
[Fact]
public void Constructor_WithNullPackageFileService_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
{
new ReadmePreviewViewModel(null, ItemFilter.All, true);
});
}
[Fact]
public void Constructor_Defaults()
{
//Arrange
var mockFileService = new Mock<INuGetPackageFileService>();
//Act
var target = new ReadmePreviewViewModel(mockFileService.Object, ItemFilter.All, true);
//Assert
Assert.False(target.ErrorLoadingReadme);
Assert.Equal(string.Empty, target.ReadmeMarkdown);
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public async Task SetPackageMetadataAsync_WithoutReadmeUrl_NoReadmeReturned(string readmeUrl)
{
//Arrange
var mockFileService = new Mock<INuGetPackageFileService>();
var target = new ReadmePreviewViewModel(mockFileService.Object, ItemFilter.Installed, true);
var package = new DetailedPackageMetadata();
package.ReadmeFileUrl = readmeUrl;
//Act
await target.SetPackageMetadataAsync(package, CancellationToken.None);
//Assert
Assert.False(target.ErrorLoadingReadme);
Assert.Equal(string.Empty, target.ReadmeMarkdown);
}
[Fact]
public async Task SetCurrentFilter_ItemFilterAll_NoLocalReadmeReturned()
{
//Arrange
var readmeContents = "readme contents";
using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(readmeContents));
var mockFileService = new Mock<INuGetPackageFileService>();
mockFileService.Setup(x => x.GetReadmeAsync(It.IsAny<Uri>(), It.IsAny<CancellationToken>())).ReturnsAsync(stream);
var target = new ReadmePreviewViewModel(mockFileService.Object, ItemFilter.Installed, true);
var package = new DetailedPackageMetadata();
package.ReadmeFileUrl = "C://path/to/readme.md";
await target.SetPackageMetadataAsync(package, CancellationToken.None);
//Act
await target.ItemFilterChangedAsync(ItemFilter.All);
//Assert
Assert.False(target.ErrorLoadingReadme);
Assert.Equal(string.Empty, target.ReadmeMarkdown);
}
[Theory]
[InlineData(ItemFilter.Installed)]
[InlineData(ItemFilter.Consolidate)]
[InlineData(ItemFilter.UpdatesAvailable)]
public async Task SetCurrentFilter_ItemFilterRenderingLocalReadme_LocalReadmeReturned(ItemFilter filter)
{
//Arrange
var readmeContents = "readme contents";
using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(readmeContents));
var mockFileService = new Mock<INuGetPackageFileService>();
mockFileService.Setup(x => x.GetReadmeAsync(It.IsAny<Uri>(), It.IsAny<CancellationToken>())).ReturnsAsync(stream);
var target = new ReadmePreviewViewModel(mockFileService.Object, ItemFilter.All, true);
var package = new DetailedPackageMetadata();
package.ReadmeFileUrl = "C://path/to/readme.md";
await target.SetPackageMetadataAsync(package, CancellationToken.None);
//Act
await target.ItemFilterChangedAsync(filter);
//Assert
Assert.False(target.ErrorLoadingReadme);
Assert.Equal(readmeContents, target.ReadmeMarkdown);
}
[Fact]
public async Task SetPackageMetadataAsync_WithLocalReadmeUrl_RenderLocalReadmeTrue_LocalReadmeReturned()
{
//Arrange
var readmeContents = "readme contents";
using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(readmeContents));
var mockFileService = new Mock<INuGetPackageFileService>();
mockFileService.Setup(x => x.GetReadmeAsync(It.IsAny<Uri>(), It.IsAny<CancellationToken>())).ReturnsAsync(stream);
var target = new ReadmePreviewViewModel(mockFileService.Object, ItemFilter.Installed, true);
var package = new DetailedPackageMetadata();
package.ReadmeFileUrl = "C://path/to/readme.md";
//Act
await target.SetPackageMetadataAsync(package, CancellationToken.None);
//Assert
Assert.False(target.ErrorLoadingReadme);
Assert.Equal(readmeContents, target.ReadmeMarkdown);
}
[Fact]
public async Task SetPackageMetadataAsync_WithLocalReadmeUrl_RenderLocalReadmeFalse_NoLocalReadmeReturned()
{
//Arrange
var readmeContents = "readme contents";
using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(readmeContents));
var mockFileService = new Mock<INuGetPackageFileService>();
mockFileService.Setup(x => x.GetReadmeAsync(It.IsAny<Uri>(), It.IsAny<CancellationToken>())).ReturnsAsync(stream);
var target = new ReadmePreviewViewModel(mockFileService.Object, ItemFilter.All, true);
var package = new DetailedPackageMetadata();
package.ReadmeFileUrl = "C://path/to/readme.md";
//Act
await target.SetPackageMetadataAsync(package, CancellationToken.None);
//Assert
Assert.False(target.ErrorLoadingReadme);
Assert.Equal(string.Empty, target.ReadmeMarkdown);
}
[Fact]
public async Task SetPackageMetadataAsync_WithLocalReadmeUrl_FileNotFound_NoReadmeFoundTextReturned()
{
//Arrange
var mockFileService = new Mock<INuGetPackageFileService>();
mockFileService.Setup(x => x.GetReadmeAsync(It.IsAny<Uri>(), It.IsAny<CancellationToken>())).Returns(null);
var target = new ReadmePreviewViewModel(mockFileService.Object, ItemFilter.Installed, true);
var package = new DetailedPackageMetadata();
package.ReadmeFileUrl = "C://path/to/readme.md";
//Act
await target.SetPackageMetadataAsync(package, CancellationToken.None);
//Assert
Assert.False(target.ErrorLoadingReadme);
Assert.Equal(Resources.Text_NoReadme, target.ReadmeMarkdown);
}
}
}
@@ -449,8 +449,41 @@ namespace NuGet.PackageManagement.VisualStudio.Test
Assert.Null(licenseStream);
}
[Fact]
public async Task Readme_WhenAdded_IsFound()
{
using (TestDirectory testDir = TestDirectory.Create())
{
// Create decoy nuget package
var readmeContent = "Hello this is README";
var readmeName = "readme.md";
var zipPath = Path.Combine(testDir.Path, "file.nupkg");
CreateDummyPackage(
zipPath: zipPath,
readmeName: readmeName,
readmeContent: readmeContent);
// prepare test
var builder = new UriBuilder(new Uri(zipPath, UriKind.Absolute))
{
Fragment = readmeName
};
var packageFileService = new NuGetPackageFileService(
default(ServiceActivationOptions),
Mock.Of<IServiceBroker>(),
new AuthorizationServiceClient(Mock.Of<IAuthorizationService>()),
_telemetryProvider.Object);
Stream readmeStream = await packageFileService.GetReadmeAsync(builder.Uri, CancellationToken.None);
Assert.NotNull(readmeStream);
Assert.Equal(readmeContent.Length, readmeStream.Length);
}
}
/// <summary>
/// Creates a NuGet package with .nupkg extension and with a PNG image named "icon.png"
/// Creates a NuGet package with .nupkg extension and with a PNG image named "icon.png" and a MD readme named "readme.md"
/// </summary>
/// <param name="path">Path to NuGet package</param>
/// <param name="iconName">Icon filename with .png extension</param>
@@ -459,7 +492,9 @@ namespace NuGet.PackageManagement.VisualStudio.Test
string iconName = "icon.png",
string iconFile = "icon.png",
string iconFileSourceElement = "icon.png",
string iconFileTargetElement = "")
string iconFileTargetElement = "",
string readmeName = "readme.md",
string readmeContent = "readme content")
{
var dir = Path.GetDirectoryName(zipPath);
var holdDir = "pkg";
@@ -471,7 +506,9 @@ namespace NuGet.PackageManagement.VisualStudio.Test
// create nuspec
var nuspec = NuspecBuilder.Create()
.WithIcon(iconName)
.WithFile(iconFileSourceElement, iconFileTargetElement);
.WithFile(iconFileSourceElement, iconFileTargetElement)
.WithReadme(readmeName)
.WithFile(readmeName);
// create png image
var iconPath = Path.Combine(folderPath, iconFile);
@@ -480,6 +517,13 @@ namespace NuGet.PackageManagement.VisualStudio.Test
File.WriteAllText(iconPath, StreamContents);
// create md readme
var readmePath = Path.Combine(folderPath, readmeName);
var readmeDir = Path.GetDirectoryName(readmePath);
Directory.CreateDirectory(readmeDir);
File.WriteAllText(readmePath, readmeContent);
// Create nuget package
using (var nuspecStream = new MemoryStream())
using (FileStream nupkgStream = File.Create(zipPath))
@@ -66,5 +66,16 @@ namespace NuGet.Protocol.Tests
Assert.True(_testData.TestData.IconUrl.IsAbsoluteUri);
Assert.NotNull(_testData.TestData.IconUrl.Fragment);
}
[Fact]
public void ReadmeFileUrl_ReturnsEmbeddedReadmeUri()
{
Assert.NotNull(_testData.TestData.ReadmeFileUrl);
var readmeFileUri = new Uri(_testData.TestData.ReadmeFileUrl);
Assert.True(readmeFileUri.IsFile);
Assert.True(readmeFileUri.IsAbsoluteUri);
Assert.NotNull(readmeFileUri.Fragment);
}
}
}
@@ -25,6 +25,7 @@ namespace NuGet.Test.Utility
.WithPackageId(pkgId.Id)
.WithPackageVersion(pkgId.Version.ToNormalizedString())
.WithIcon("icon.png")
.WithReadme("readme.md")
.Build();
pkg.Nuspec = XDocument.Parse(nuspec.ToString());
@@ -74,6 +74,8 @@ namespace NuGet.Test.Utility
public Uri ReadmeUrl => null;
public string ReadmeFileUrl => null;
public Uri LicenseUrl => null;
public Uri ProjectUrl => null;