File: ErrorReporting\VisualStudioInfoBar.cs
Web Access
Project: ..\..\..\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_ckcrqypr_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.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Imaging;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.ErrorReporting
{
    internal sealed class VisualStudioInfoBar
    {
        private readonly IThreadingContext _threadingContext;
        private readonly SVsServiceProvider _serviceProvider;
        private readonly IAsynchronousOperationListener _listener;
 
        /// <summary>
        /// Keep track of the messages that are currently being shown to the user.  If we would 
        /// show the same message again, block that from happening so we don't spam the user with
        /// the same message.  When the info bar item is dismissed though, we then may show the
        /// same message in the future.  This is important for user clarity as it's possible for 
        /// a feature to fail for some reason, then work fine for a while, then fail again.  We want
        /// the second failure message to be reported to ensure the user is not confused.
        /// 
        /// Accessed on UI thread.
        /// </summary>
        private readonly HashSet<string> _currentlyShowingMessages = new();
 
        public VisualStudioInfoBar(
            IThreadingContext threadingContext,
            SVsServiceProvider serviceProvider,
            IAsynchronousOperationListenerProvider listenerProvider)
        {
            _threadingContext = threadingContext;
            _serviceProvider = serviceProvider;
            _listener = listenerProvider.GetListener(FeatureAttribute.InfoBar);
        }
 
        public void ShowInfoBar(string message, params InfoBarUI[] items)
        {
            // We can be called from any thread since errors can occur anywhere, however we can only construct and InfoBar from the UI thread.
            _threadingContext.JoinableTaskFactory.RunAsync(async () =>
            {
                using var _ = _listener.BeginAsyncOperation(nameof(ShowInfoBar));
 
                await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(_threadingContext.DisposalToken);
 
                // If we're already shown this same message to the user, then do not bother showing it
                // to them again.  It will just be noisy.
                if (!_currentlyShowingMessages.Contains(message) &&
                    _serviceProvider.GetService(typeof(SVsShell)) is IVsShell shell &&
                    ErrorHandler.Succeeded(shell.GetProperty((int)__VSSPROPID7.VSSPROPID_MainWindowInfoBarHost, out var globalInfoBar)) &&
                    globalInfoBar is IVsInfoBarHost infoBarHost &&
                    _serviceProvider.GetService(typeof(SVsInfoBarUIFactory)) is IVsInfoBarUIFactory factory)
                {
                    // create action item list
                    var actionItems = new List<IVsInfoBarActionItem>();
 
                    foreach (var item in items)
                    {
                        switch (item.Kind)
                        {
                            case InfoBarUI.UIKind.Button:
                                actionItems.Add(new InfoBarButton(item.Title));
                                break;
                            case InfoBarUI.UIKind.HyperLink:
                                actionItems.Add(new InfoBarHyperlink(item.Title));
                                break;
                            case InfoBarUI.UIKind.Close:
                                break;
                            default:
                                throw ExceptionUtilities.UnexpectedValue(item.Kind);
                        }
                    }
 
                    var infoBarModel = new InfoBarModel(
                        new[] { new InfoBarTextSpan(message) },
                        actionItems,
                        KnownMonikers.StatusInformation,
                        isCloseButtonVisible: true);
 
                    var infoBarUI = factory.CreateInfoBar(infoBarModel);
                    if (infoBarUI == null)
                        return;
 
                    uint? infoBarCookie = null;
                    var eventSink = new InfoBarEvents(items, onClose: () =>
                    {
                        Contract.ThrowIfFalse(_threadingContext.JoinableTaskContext.IsOnMainThread);
 
                        // Remove the message from the list that we're keeping track of.  Future identical
                        // messages can now be shown.
                        _currentlyShowingMessages.Remove(message);
 
                        // Run given onClose action if there is one.
                        items.FirstOrDefault(i => i.Kind == InfoBarUI.UIKind.Close).Action?.Invoke();
 
                        if (infoBarCookie.HasValue)
                        {
                            infoBarUI.Unadvise(infoBarCookie.Value);
                        }
                    });
 
                    infoBarUI.Advise(eventSink, out var cookie);
                    infoBarCookie = cookie;
 
                    infoBarHost.AddInfoBar(infoBarUI);
 
                    _currentlyShowingMessages.Add(message);
                }
            });
        }
 
        private sealed class InfoBarEvents : IVsInfoBarUIEvents
        {
            private readonly InfoBarUI[] _items;
            private readonly Action _onClose;
 
            public InfoBarEvents(InfoBarUI[] items, Action onClose)
            {
                Contract.ThrowIfNull(onClose);
 
                _items = items;
                _onClose = onClose;
            }
 
            public void OnActionItemClicked(IVsInfoBarUIElement infoBarUIElement, IVsInfoBarActionItem actionItem)
            {
                var item = _items.FirstOrDefault(i => i.Title == actionItem.Text);
                if (item.IsDefault)
                {
                    return;
                }
 
                item.Action?.Invoke();
 
                if (!item.CloseAfterAction)
                {
                    return;
                }
 
                infoBarUIElement.Close();
            }
 
            public void OnClosed(IVsInfoBarUIElement infoBarUIElement)
                => _onClose();
        }
    }
}