Skip to content

Commit 2f48633

Browse files
committed
Handle changes to ExcludeFromCurrentConfigurationProperty
This change requires a good bit of background. The Solution Explorer builds its tree from the evaluation model of the "active" configuration and only the active configuration, and there can only be one active configuration. Items (source files, resource files, etc.) that aren't part of the active configuration won't be displayed. In MAUI projects, the items under the "Platforms" folder are meant to be platform-specific (by which we really mean target framework-specific), with each relevant platform getting its own subfolder. Items in the "Android" subfolder should not be included in the "iOS" build, for example. The most straightforward way to approach this would be to conditionally include the various `<Compile>` items under Platforms based on the target framework. That would work correctly for the purposes of the build, but means that not all of the items would display in the Solution Explorer--only those belonging to whatever platform is associated with that "active" configuration. Instead, the items are included for *all* target frameworks during MSBuild evaluation, and then the extraneous ones are removed by targets during the build. This ensures they always appear in Solution Explorer and the build still works as expected. However the language service integration in the .NET Project System utilizes both evaluation and design-time data and has to reconcile the two as best it can. One thing it can't handle on its own are items that are present in evaluation but removed by a target. To help with this, we added the concept of `ExcludeFromCurrentConfiguration` metadata for items; we can use this to tell the language service integration to _pretend_ the item doesn't exist in evaluation even though it does. The MAUI .props and .targets end up defining the items under the Platforms folder in all configurations, but the metadata on those items varies depending on whether or not the item should actually be passed to the Language Service. It's important to step back at this point and call out that there are a couple layers of workarounds going on here: MAUI includes the items in every configuration to work around the fact that CPS can't build the Solution Explorer tree from multiple target frameworks, and the language service needs `ExcludeFromCurrentConfiguration` metadata to work around the fact that the items are in every configuration. And now we can finally talk about bug AB#1895917. The above scheme works well enough when the `ExcludeFromCurrentConfiguration` metadata is properly applied to the items. The problem is that the metadata is added by a .targets file (Microsoft.Maui.Controls.SingleProject.targets) that comes from a NuGet package (Microsoft.Maui.Controls.Build.Tasks). If an evaluation occurs before NuGet restore runs and the Language Service integration processes that evaluation, we will see the items without the metadata. This means that we will pass Android-specific source files along as if they were also part of the Mac, iOS, and Windows builds--and do the same for every other platform-specific file. This will lead to spurious errors in the IDE (e.g. the Windows build complaining about the Andoid APIs as those aren't defined). More to the point of this bug, Hot Reload won't work because the IDE will notice the discrepancy between the types it knows about and what is actually in the built assembly and think that it missed some edits. The fix here is to also look out for changes in the `ExcludeFromCurrentConfiguration` metadata. If a file was previously included and we get a new evaluation saying it is excluded, we need to tell the Language Service to remove it. And if it was previously excluded and now is included, we need to tell the Language Service to add it. This way even if we provided the "wrong" data to the Language Service initially, once NuGet restore has run and we process the next evaluation we can fix everything up.
1 parent 9b28338 commit 2f48633

File tree

2 files changed

+81
-16
lines changed

2 files changed

+81
-16
lines changed

src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/LanguageServices/Handlers/AbstractEvaluationCommandLineHandler.cs

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -183,10 +183,7 @@ private void ApplyChangesToContext(IWorkspaceProjectContext context, IProjectCha
183183

184184
foreach (string includePath in difference.ChangedItems)
185185
{
186-
UpdateInContextIfPresent(context, includePath, previousMetadata, currentMetadata, isActiveContext, logger);
187-
188-
// TODO: Check for changes in the metadata indicating if we should ignore the file
189-
// in the current configuration.
186+
HandleEvaluationMetadataChange(context, includePath, previousMetadata, currentMetadata, isActiveContext, logger);
190187
}
191188
}
192189

@@ -223,16 +220,38 @@ private void AddToContextIfNotPresent(IWorkspaceProjectContext context, string i
223220
}
224221
}
225222

226-
private void UpdateInContextIfPresent(IWorkspaceProjectContext context, string includePath, IImmutableDictionary<string, IImmutableDictionary<string, string>> previousMetadata, IImmutableDictionary<string, IImmutableDictionary<string, string>> currentMetadata, bool isActiveContext, IManagedProjectDiagnosticOutputService logger)
223+
/// <remarks>
224+
/// This should only be called for evaluation changes. The items we get from design-time builds represent
225+
/// command line arguments and won't have metadata.
226+
/// </remarks>
227+
private void HandleEvaluationMetadataChange(IWorkspaceProjectContext context, string includePath, IImmutableDictionary<string, IImmutableDictionary<string, string>> previousMetadata, IImmutableDictionary<string, IImmutableDictionary<string, string>> currentMetadata, bool isActiveContext, IManagedProjectDiagnosticOutputService logger)
227228
{
228-
string fullPath = _project.MakeRooted(includePath);
229-
230-
if (_paths.Contains(fullPath))
229+
// A change in ExcludeFromCurrentConfiguration metadata needs to be processed as an add or remove rather
230+
// than an update, so check for that first.
231+
bool previouslyIncluded = IsItemInCurrentConfiguration(includePath, previousMetadata);
232+
bool currentlyIncluded = IsItemInCurrentConfiguration(includePath, currentMetadata);
233+
234+
if (previouslyIncluded && !currentlyIncluded)
235+
{
236+
RemoveFromContextIfPresent(context, includePath, logger);
237+
}
238+
else if (!previouslyIncluded && currentlyIncluded)
231239
{
232-
IImmutableDictionary<string, string> previousItemMetadata = previousMetadata.GetValueOrDefault(includePath, ImmutableStringDictionary<string>.EmptyOrdinal);
233-
IImmutableDictionary<string, string> currentItemMetadata = currentMetadata.GetValueOrDefault(includePath, ImmutableStringDictionary<string>.EmptyOrdinal);
240+
AddToContextIfNotPresent(context, includePath, currentMetadata, isActiveContext, logger);
241+
}
242+
else
243+
{
244+
// No change to ExcludeFromCurrentConfiguration; handle as an update.
245+
246+
string fullPath = _project.MakeRooted(includePath);
234247

235-
UpdateInContext(context, fullPath, previousItemMetadata, currentItemMetadata, isActiveContext, logger);
248+
if (_paths.Contains(fullPath))
249+
{
250+
IImmutableDictionary<string, string> previousItemMetadata = previousMetadata.GetValueOrDefault(includePath, ImmutableStringDictionary<string>.EmptyOrdinal);
251+
IImmutableDictionary<string, string> currentItemMetadata = currentMetadata.GetValueOrDefault(includePath, ImmutableStringDictionary<string>.EmptyOrdinal);
252+
253+
UpdateInContext(context, fullPath, previousItemMetadata, currentItemMetadata, isActiveContext, logger);
254+
}
236255
}
237256
}
238257

tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/ProjectSystem/VS/LanguageServices/Handlers/AbstractEvaluationCommandLineHandlerTests.cs

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ public void AddEvaluationChanges_CanAddItemWithMetadata()
9999
var difference = IProjectChangeDiffFactory.WithAddedItems("A.cs");
100100
var metadata = MetadataFactory.Create("A.cs", ("Name", "Value"));
101101

102-
ApplyProjectEvaluation(context, handler, 1, difference, metadata);
102+
ApplyProjectEvaluation(context, handler, 1, difference, metadata: metadata);
103103

104104
var result = handler.Files[@"C:\Project\A.cs"];
105105

@@ -117,7 +117,7 @@ public void AddEvaluationChanges_ItemsWithExclusionMetadataAreIgnored()
117117
.Add("B.cs", ("ExcludeFromCurrentConfiguration", "false"));
118118

119119

120-
ApplyProjectEvaluation(context, handler, 1, difference, metadata);
120+
ApplyProjectEvaluation(context, handler, 1, difference, metadata: metadata);
121121

122122
string[] expectedFiles = new[] { @"C:\Project\B.cs", @"C:\Project\C.cs" };
123123
Assert.Equal(expectedFiles.OrderBy(f => f), handler.FileNames.OrderBy(f => f));
@@ -311,7 +311,7 @@ public void ApplyProjectEvaluationChanges_WithExistingEvaluationChanges_CanAddCh
311311
var difference = IProjectChangeDiffFactory.WithChangedItems(file);
312312
var metadata = MetadataFactory.Create(file, ("Name", "Value"));
313313

314-
ApplyProjectEvaluation(context, handler, 2, difference, metadata);
314+
ApplyProjectEvaluation(context, handler, 2, difference, metadata: metadata);
315315

316316
var result = handler.Files[@"C:\Project\A.cs"];
317317

@@ -372,10 +372,56 @@ public void ApplyProjectBuild_WhenOlderEvaluationChangesWithRemovedConflict_Desi
372372
Assert.Single(handler.FileNames, @"C:\Project\Source.cs");
373373
}
374374

375-
private static void ApplyProjectEvaluation(IWorkspaceProjectContext context, AbstractEvaluationCommandLineHandler handler, IComparable version, IProjectChangeDiff difference, IImmutableDictionary<string, IImmutableDictionary<string, string>>? metadata = null)
375+
[Fact]
376+
public void ApplyProjectEvaluation_ChangingExclusionMetadata_IncludesFile()
377+
{
378+
var handler = CreateInstance(@"C:\Project\Project.csproj");
379+
var context = IWorkspaceProjectContextMockFactory.Create();
380+
381+
var metadata = MetadataFactory.Create("Source.cs", ("ExcludeFromCurrentConfiguration", "true"));
382+
383+
ApplyProjectEvaluation(context, handler, version: 0, IProjectChangeDiffFactory.WithAddedItems("Source.cs"), metadata: metadata);
384+
385+
Assert.Empty(handler.FileNames);
386+
387+
var previousMetadata = metadata;
388+
metadata = MetadataFactory.Create("Source.cs", ("ExcludeFromCurrentConfiguration", "false"));
389+
390+
ApplyProjectEvaluation(context, handler, version: 1, IProjectChangeDiffFactory.WithChangedItems("Source.cs"), previousMetadata, metadata);
391+
392+
Assert.Single(handler.FileNames, @"C:\Project\Source.cs");
393+
}
394+
395+
[Fact]
396+
public void ApplyProjectEvaluation_ChangingExclusionMetadata_ExcludesFile()
397+
{
398+
var handler = CreateInstance(@"C:\Project\Project.csproj");
399+
var context = IWorkspaceProjectContextMockFactory.Create();
400+
401+
var metadata = MetadataFactory.Create("Source.cs", ("ExcludeFromCurrentConfiguration", "false"));
402+
403+
ApplyProjectEvaluation(context, handler, version: 0, IProjectChangeDiffFactory.WithAddedItems("Source.cs"), metadata: metadata);
404+
405+
Assert.Single(handler.FileNames, @"C:\Project\Source.cs");
406+
407+
var previousMetadata = metadata;
408+
metadata = MetadataFactory.Create("Source.cs", ("ExcludeFromCurrentConfiguration", "true"));
409+
410+
ApplyProjectEvaluation(context, handler, version: 1, IProjectChangeDiffFactory.WithChangedItems("Source.cs"), previousMetadata, metadata);
411+
412+
Assert.Empty(handler.FileNames);
413+
}
414+
415+
private static void ApplyProjectEvaluation(
416+
IWorkspaceProjectContext context,
417+
AbstractEvaluationCommandLineHandler handler,
418+
IComparable version,
419+
IProjectChangeDiff difference,
420+
IImmutableDictionary<string, IImmutableDictionary<string, string>>? previousMetadata = null,
421+
IImmutableDictionary<string, IImmutableDictionary<string, string>>? metadata = null)
376422
{
377423
metadata ??= ImmutableDictionary<string, IImmutableDictionary<string, string>>.Empty;
378-
var previousMetadata = ImmutableDictionary<string, IImmutableDictionary<string, string>>.Empty;
424+
previousMetadata ??= ImmutableDictionary<string, IImmutableDictionary<string, string>>.Empty;
379425
bool isActiveContext = true;
380426
var logger = IManagedProjectDiagnosticOutputServiceFactory.Create();
381427

0 commit comments

Comments
 (0)