File: DesignerAttribute\VisualStudioDesignerAttributeService.cs
Web Access
Project: src\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_pxr0p0dn_wpftmp.csproj (Microsoft.VisualStudio.LanguageServices)
// 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 file in the project root for more information.
 
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.DesignerAttribute;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Remote;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.VisualStudio.Designer.Interfaces;
using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Shell.Services;
using Roslyn.Utilities;
 
namespace Microsoft.VisualStudio.LanguageServices.Implementation.DesignerAttribute;
 
[ExportEventListener(WellKnownEventListeners.Workspace, WorkspaceKind.Host), Shared]
internal sealed class VisualStudioDesignerAttributeService :
    IDesignerAttributeDiscoveryService.ICallback, IEventListener<object>, IDisposable
{
    private readonly VisualStudioWorkspaceImpl _workspace;
    private readonly IThreadingContext _threadingContext;
 
    /// <summary>
    /// Used to acquire the legacy project designer service.
    /// </summary>
    private readonly IServiceProvider _serviceProvider;
    private readonly IAsynchronousOperationListener _listener;
 
    /// <summary>
    /// Cache from project to the CPS designer service for it.  Computed on demand (which
    /// requires using the UI thread), but then cached for all subsequent notifications about
    /// that project.
    /// </summary>
    private readonly ConcurrentDictionary<ProjectId, IProjectItemDesignerTypeUpdateService?> _cpsProjects = [];
 
    /// <summary>
    /// Cached designer service for notifying legacy projects about designer attributes.
    /// </summary>
    private IVSMDDesignerService? _legacyDesignerService;
 
    private readonly AsyncBatchingWorkQueue _workQueue;
 
    // We'll get notifications from the OOP server about new attribute arguments. Collect those notifications and
    // deliver them to VS in batches to prevent flooding the UI thread.
    private readonly AsyncBatchingWorkQueue<DesignerAttributeData> _projectSystemNotificationQueue;
 
    [ImportingConstructor]
    [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
    public VisualStudioDesignerAttributeService(
        VisualStudioWorkspaceImpl workspace,
        IThreadingContext threadingContext,
        IAsynchronousOperationListenerProvider asynchronousOperationListenerProvider,
        Shell.SVsServiceProvider serviceProvider)
    {
        _workspace = workspace;
        _threadingContext = threadingContext;
        _serviceProvider = serviceProvider;
 
        _listener = asynchronousOperationListenerProvider.GetListener(FeatureAttribute.DesignerAttributes);
 
        _workQueue = new AsyncBatchingWorkQueue(
            DelayTimeSpan.Idle,
            this.ProcessWorkspaceChangeAsync,
            _listener,
            _threadingContext.DisposalToken);
 
        _projectSystemNotificationQueue = new AsyncBatchingWorkQueue<DesignerAttributeData>(
            DelayTimeSpan.Idle,
            this.NotifyProjectSystemAsync,
            _listener,
            _threadingContext.DisposalToken);
    }
 
    public void Dispose()
    {
        _workspace.WorkspaceChanged -= OnWorkspaceChanged;
    }
 
    void IEventListener<object>.StartListening(Workspace workspace, object _)
    {
        if (workspace != _workspace)
            return;
 
        _workspace.WorkspaceChanged += OnWorkspaceChanged;
        _workQueue.AddWork(cancelExistingWork: true);
    }
 
    private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs e)
    {
        _workQueue.AddWork(cancelExistingWork: true);
    }
 
    private async ValueTask ProcessWorkspaceChangeAsync(CancellationToken cancellationToken)
    {
        var statusService = _workspace.Services.GetRequiredService<IWorkspaceStatusService>();
        await statusService.WaitUntilFullyLoadedAsync(cancellationToken).ConfigureAwait(false);
 
        cancellationToken.ThrowIfCancellationRequested();
 
        var solution = _workspace.CurrentSolution;
        foreach (var (projectId, _) in _cpsProjects)
        {
            if (!solution.ContainsProject(projectId))
                _cpsProjects.TryRemove(projectId, out _);
        }
 
        var client = await RemoteHostClient.TryGetClientAsync(_workspace, cancellationToken).ConfigureAwait(false);
        if (client == null)
            return;
 
        var trackingService = solution.Services.GetRequiredService<IDocumentTrackingService>();
        await DesignerAttributeDiscoveryService.DiscoverDesignerAttributesAsync(
            solution, trackingService.GetActiveDocument(solution), client, _listener, this, cancellationToken).ConfigureAwait(false);
    }
 
    private async ValueTask NotifyProjectSystemAsync(
        ImmutableSegmentedList<DesignerAttributeData> data, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
 
        using var _ = ArrayBuilder<DesignerAttributeData>.GetInstance(out var filteredInfos);
        AddFilteredInfos(data, filteredInfos);
 
        // Now, group all the notifications by project and update that project at once.
        foreach (var group in filteredInfos.GroupBy(a => a.DocumentId.ProjectId))
        {
            cancellationToken.ThrowIfCancellationRequested();
            await NotifyProjectSystemAsync(group.Key, group, cancellationToken).ConfigureAwait(false);
        }
    }
 
    private static void AddFilteredInfos(ImmutableSegmentedList<DesignerAttributeData> data, ArrayBuilder<DesignerAttributeData> filteredData)
    {
        using var _ = PooledHashSet<DocumentId>.GetInstance(out var seenDocumentIds);
 
        // Walk the list of designer items in reverse, and skip any items for a project once
        // we've already seen it once.  That way, we're only reporting the most up to date
        // information for a project, and we're skipping the stale information.
        for (var i = data.Count - 1; i >= 0; i--)
        {
            var info = data[i];
            if (seenDocumentIds.Add(info.DocumentId))
                filteredData.Add(info);
        }
    }
 
    private async Task NotifyProjectSystemAsync(
        ProjectId projectId,
        IEnumerable<DesignerAttributeData> data,
        CancellationToken cancellationToken)
    {
        // Delegate to the CPS or legacy notification services as necessary.
        var cpsUpdateService = await GetUpdateServiceIfCpsProjectAsync(projectId, cancellationToken).ConfigureAwait(false);
        var task = cpsUpdateService == null
            ? NotifyLegacyProjectSystemAsync(projectId, data, cancellationToken)
            : NotifyCpsProjectSystemAsync(projectId, cpsUpdateService, data, cancellationToken);
 
        await task.ConfigureAwait(false);
    }
 
    private async Task NotifyLegacyProjectSystemAsync(
        ProjectId projectId,
        IEnumerable<DesignerAttributeData> data,
        CancellationToken cancellationToken)
    {
        // legacy project system can only be talked to on the UI thread.
        await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
 
        var designerService = _legacyDesignerService ??= (IVSMDDesignerService)_serviceProvider.GetService(typeof(SVSMDDesignerService));
        if (designerService == null)
            return;
 
        var hierarchy = _workspace.GetHierarchy(projectId);
        if (hierarchy == null)
            return;
 
        foreach (var info in data)
        {
            cancellationToken.ThrowIfCancellationRequested();
            NotifyLegacyProjectSystemOnUIThread(designerService, hierarchy, info);
        }
    }
 
    private void NotifyLegacyProjectSystemOnUIThread(
        IVSMDDesignerService designerService,
        IVsHierarchy hierarchy,
        DesignerAttributeData data)
    {
        _threadingContext.ThrowIfNotOnUIThread();
 
        var itemId = hierarchy.TryGetItemId(data.FilePath);
        if (itemId == VSConstants.VSITEMID_NIL)
            return;
 
        // PERF: Avoid sending the message if the project system already has the current value.
        if (ErrorHandler.Succeeded(hierarchy.GetProperty(itemId, (int)__VSHPROPID.VSHPROPID_ItemSubType, out var currentValue)))
        {
            var currentStringValue = string.IsNullOrEmpty(currentValue as string) ? null : (string)currentValue;
            if (string.Equals(currentStringValue, data.Category, StringComparison.OrdinalIgnoreCase))
                return;
        }
 
        try
        {
            designerService.RegisterDesignViewAttribute(
                hierarchy, (int)itemId, dwClass: 0,
                pwszAttributeValue: data.Category);
        }
        catch
        {
            // DevDiv # 933717
            // turns out RegisterDesignViewAttribute can throw in certain cases such as a file failed to be checked out by source control
            // or IVSHierarchy failed to set a property for this project
            //
            // just swallow it. don't crash VS.
        }
    }
 
    private async Task NotifyCpsProjectSystemAsync(
        ProjectId projectId,
        IProjectItemDesignerTypeUpdateService updateService,
        IEnumerable<DesignerAttributeData> data,
        CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
 
        // We may have updates for many different configurations of the same logical project system project.
        // However, the project system only associates designer attributes with one of those projects.  So just drop
        // the notifications for any sibling configurations.
        if (!_workspace.IsPrimaryProject(projectId))
            return;
 
        foreach (var info in data)
        {
            cancellationToken.ThrowIfCancellationRequested();
            await NotifyCpsProjectSystemAsync(updateService, info, cancellationToken).ConfigureAwait(false);
        }
    }
 
    private static async Task NotifyCpsProjectSystemAsync(
        IProjectItemDesignerTypeUpdateService updateService,
        DesignerAttributeData data,
        CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
 
        try
        {
            await updateService.SetProjectItemDesignerTypeAsync(data.FilePath, data.Category).ConfigureAwait(false);
        }
        catch (ObjectDisposedException)
        {
            // we might call update service after project is already removed and get object disposed exception.
            // we will catch the exception and ignore. 
            // see this PR for more detail - https://github.com/dotnet/roslyn/pull/35383
        }
    }
 
    private async Task<IProjectItemDesignerTypeUpdateService?> GetUpdateServiceIfCpsProjectAsync(
        ProjectId projectId, CancellationToken cancellationToken)
    {
        if (!_cpsProjects.TryGetValue(projectId, out var updateService))
        {
            await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
 
            updateService = ComputeUpdateService();
            _cpsProjects.TryAdd(projectId, updateService);
        }
 
        return updateService;
 
        IProjectItemDesignerTypeUpdateService? ComputeUpdateService()
        {
            if (!_workspace.IsCPSProject(projectId))
                return null;
 
            var vsProject = (IVsProject?)_workspace.GetHierarchy(projectId);
            if (vsProject == null)
                return null;
 
            if (ErrorHandler.Failed(vsProject.GetItemContext((uint)VSConstants.VSITEMID.Root, out var projectServiceProvider)))
                return null;
 
            var serviceProvider = new Shell.ServiceProvider(projectServiceProvider);
            return serviceProvider.GetService(typeof(IProjectItemDesignerTypeUpdateService)) as IProjectItemDesignerTypeUpdateService;
        }
    }
 
    /// <summary>
    /// Callback from the OOP service back into us.
    /// </summary>
    public ValueTask ReportDesignerAttributeDataAsync(ImmutableArray<DesignerAttributeData> data, CancellationToken cancellationToken)
    {
        Contract.ThrowIfNull(_projectSystemNotificationQueue);
        _projectSystemNotificationQueue.AddWork(data);
        return ValueTaskFactory.CompletedTask;
    }
}