diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Properties/InterceptedProjectProperties/ApplicationPropertyPage/CompoundTargetFrameworkValueProvider.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Properties/InterceptedProjectProperties/ApplicationPropertyPage/CompoundTargetFrameworkValueProvider.cs index 28f646d268c..6367386612f 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Properties/InterceptedProjectProperties/ApplicationPropertyPage/CompoundTargetFrameworkValueProvider.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Properties/InterceptedProjectProperties/ApplicationPropertyPage/CompoundTargetFrameworkValueProvider.cs @@ -18,7 +18,7 @@ namespace Microsoft.VisualStudio.ProjectSystem.Properties TargetPlatformVersionProperty }, ExportInterceptingPropertyValueProviderFile.ProjectFile)] - internal sealed class CompoundTargetFrameworkValueProvider : InterceptingPropertyValueProviderBase + internal class CompoundTargetFrameworkValueProvider : InterceptingPropertyValueProviderBase { private const string InterceptedTargetFrameworkProperty = "InterceptedTargetFramework"; private const string TargetPlatformProperty = ConfigurationGeneral.TargetPlatformIdentifierProperty; @@ -44,10 +44,10 @@ private struct ComplexTargetFramework } [ImportingConstructor] - public CompoundTargetFrameworkValueProvider(ProjectProperties properties, ConfiguredProject configuredProject) + public CompoundTargetFrameworkValueProvider(ProjectProperties properties) { _properties = properties; - _configuredProject = configuredProject; + _configuredProject = properties.ConfiguredProject; } private async Task GetStoredPropertiesAsync() @@ -70,12 +70,22 @@ public override Task IsValueDefinedInContextAsync(string propertyName, IPr // Changing the Target Framework Moniker if (StringComparers.PropertyLiteralValues.Equals(propertyName, InterceptedTargetFrameworkProperty)) { - // Delete stored platform properties - storedProperties.TargetPlatformIdentifier = null; - storedProperties.TargetPlatformVersion = null; - await ResetPlatformPropertiesAsync(defaultProperties); - + if (Strings.IsNullOrEmpty(unevaluatedPropertyValue)) + { + return null; + } + storedProperties.TargetFrameworkMoniker = unevaluatedPropertyValue; + + // Only projects targeting .NET 5 or higher use platform properties. + string targetFrameworkAlias = await GetTargetFrameworkAliasAsync(unevaluatedPropertyValue); + if (!IsNetCore5OrHigher(targetFrameworkAlias)) + { + // Delete platform properties + storedProperties.TargetPlatformIdentifier = null; + storedProperties.TargetPlatformVersion = null; + await ResetPlatformPropertiesAsync(defaultProperties); + } } // Changing the Target Platform Identifier @@ -83,8 +93,7 @@ public override Task IsValueDefinedInContextAsync(string propertyName, IPr { if (unevaluatedPropertyValue != storedProperties.TargetPlatformIdentifier) { - // Delete stored platform properties - storedProperties.TargetPlatformIdentifier = null; + // Delete platform properties. storedProperties.TargetPlatformVersion = null; await ResetPlatformPropertiesAsync(defaultProperties); @@ -198,7 +207,8 @@ private async Task ComputeValueAsync(ComplexTargetFramework complexTarge return targetFrameworkAlias + "-windows" + complexTargetFramework.TargetPlatformVersion; } - if (!Strings.IsNullOrEmpty(complexTargetFramework.TargetPlatformIdentifier)) + // We only keep the platform properties for projects targeting .NET 5 or higher. + if (!Strings.IsNullOrEmpty(complexTargetFramework.TargetPlatformIdentifier) && IsNetCore5OrHigher(targetFrameworkAlias)) { string targetPlatformAlias = await GetTargetPlatformAliasAsync(complexTargetFramework.TargetPlatformIdentifier); @@ -269,7 +279,7 @@ private async Task IsWindowsPlatformNeededAsync() /// Retrieves the target framework alias (i.e. net5.0) from the project's subscription service. /// /// - private async Task GetTargetFrameworkAliasAsync(string targetFrameworkMoniker) + internal virtual async Task GetTargetFrameworkAliasAsync(string targetFrameworkMoniker) { IProjectSubscriptionService? subscriptionService = _configuredProject.Services.ProjectSubscription; Assumes.Present(subscriptionService); @@ -318,6 +328,7 @@ private async Task GetTargetPlatformAliasAsync(string targetPlatformIden /// private static async Task ResetPlatformPropertiesAsync(IProjectProperties projectProperties) { + await projectProperties.DeletePropertyAsync(TargetPlatformProperty); await projectProperties.DeletePropertyAsync(TargetPlatformVersionProperty); await projectProperties.DeletePropertyAsync(SupportedOSPlatformVersionProperty); } diff --git a/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/ProjectSystem/Properties/InterceptingProjectProperties/ApplicationPropertyPage/CompoundTargetFrameworkValueProviderTests.cs b/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/ProjectSystem/Properties/InterceptingProjectProperties/ApplicationPropertyPage/CompoundTargetFrameworkValueProviderTests.cs new file mode 100644 index 00000000000..982e23a1ae7 --- /dev/null +++ b/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/ProjectSystem/Properties/InterceptingProjectProperties/ApplicationPropertyPage/CompoundTargetFrameworkValueProviderTests.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information. + +namespace Microsoft.VisualStudio.ProjectSystem.Properties; + +public class CompoundTargetFrameworkValueProviderTests +{ + [Theory] + [InlineData("net6.0-windows7.0", ".NETCoreApp,Version=v6.0", ".NETCoreApp", "Windows", "7.0", true, null, null)] // Kept: changing the useWPF, useWindowsForms and useWinUI keeps the platform properties + [InlineData("net6.0-windows7.0", ".NETCoreApp,Version=v6.0", ".NETCoreApp", "Windows", "7.0", null, true, null)] // Kept + [InlineData("net6.0-windows7.0", ".NETCoreApp,Version=v6.0", ".NETCoreApp", "Windows", "7.0", null, null, true)] // Kept + [InlineData("net7.0-windows-7.0", ".NETCoreApp,Version=v7.0", ".NETCoreApp", "Windows", "7.0", true, null, null)] // Kept: upgrading tf + [InlineData("net8.0-windows7.0", ".NETCoreApp,Version=v8.0", ".NETCoreApp", "Windows", "7.0", true, null, null)] // Kept + [InlineData("net5.0-windows10.0", ".NETCoreApp,Version=v5.0", ".NETCoreApp", "Windows", "10.0", true, null, null)] // Kept: changing platform version + [InlineData("netcoreapp3.1", ".NETCoreApp,Version=v3.1", ".NETCoreApp", "", "", true, null, null)] // Removed: downgrading tf + [InlineData("net5.0-android", ".NETCoreApp,Version=v5.0", ".NETCoreApp", "Android", "", null, null, null)] // Removed: changing platform + public async Task WhenChangingTargetFrameworkProperties_PlatformPropertiesAreKeptOrRemoved(string tf, string tfm, string tfi, string tpi, string tpv, bool? useWF, bool? useWPF, bool? useWUI) + { + // Previous target framework properties + var propertiesAndValues = new Dictionary + { + { "InterceptedTargetFramework", ".net5.0-windows" }, + { "TargetPlatformIdentifier", "windows" }, + { "TargetPlatformVersion", "7.0" } + }; + var iProjectProperties = IProjectPropertiesFactory.CreateWithPropertiesAndValues(propertiesAndValues); + + // New target framework properties + var projectProperties = ProjectPropertiesFactory.Create( + new PropertyPageData(ConfigurationGeneral.SchemaName, ConfigurationGeneral.TargetFrameworkProperty, tf, null), + new PropertyPageData(ConfigurationGeneral.SchemaName, ConfigurationGeneral.TargetFrameworkMonikerProperty, tfm, null), + new PropertyPageData(ConfigurationGeneral.SchemaName, ConfigurationGeneral.TargetFrameworkIdentifierProperty, tfi, null), + new PropertyPageData(ConfigurationGeneral.SchemaName, ConfigurationGeneral.TargetPlatformIdentifierProperty, tpi, null), + new PropertyPageData(ConfigurationGeneral.SchemaName, ConfigurationGeneral.TargetPlatformVersionProperty, tpv, null), + new PropertyPageData(ConfigurationGeneral.SchemaName, ConfigurationGeneral.UseWPFProperty, useWPF, null), + new PropertyPageData(ConfigurationGeneral.SchemaName, ConfigurationGeneral.UseWindowsFormsProperty, useWF, null), + new PropertyPageData(ConfigurationGeneral.SchemaName, ConfigurationGeneral.UseWinUIProperty, useWUI, null)); + + var mockProvider = new Mock(MockBehavior.Strict, projectProperties); + mockProvider.Setup(m => m.GetTargetFrameworkAliasAsync(tfm)).Returns(Task.FromResult(tf)); + mockProvider.Setup(m => m.OnSetPropertyValueAsync("InterceptedTargetFramework", tfm, iProjectProperties, null)).CallBase(); + + var provider = mockProvider.Object; + await provider.OnSetPropertyValueAsync("InterceptedTargetFramework", tfm, iProjectProperties); + + mockProvider.VerifyAll(); + mockProvider.Setup(m => m.OnGetEvaluatedPropertyValueAsync(It.IsAny(), It.IsAny(), It.IsAny())).CallBase(); + + // The value of the property we surface in the UI is stored as a moniker. + var actualTargetFramework = await provider.OnGetEvaluatedPropertyValueAsync("InterceptedTargetFramework", "", iProjectProperties); + Assert.Equal(tfm, actualTargetFramework); + + var actualPlatformIdentifier = await provider.OnGetEvaluatedPropertyValueAsync(ConfigurationGeneral.TargetPlatformIdentifierProperty, "", iProjectProperties); + Assert.Equal(tpi, actualPlatformIdentifier); + + var actualPlatformVersion = await provider.OnGetEvaluatedPropertyValueAsync(ConfigurationGeneral.TargetPlatformVersionProperty, "", iProjectProperties); + Assert.Equal(tpv, actualPlatformVersion); + + var actualComputedTargetFramework = await provider.OnGetEvaluatedPropertyValueAsync(ConfigurationGeneral.TargetFrameworkProperty, "", iProjectProperties); + Assert.Equal(tf, actualComputedTargetFramework); + } +}