diff --git a/src/GitHub.Exports/Settings/Guids.cs b/src/GitHub.Exports/Settings/Guids.cs index f84372a58b..dd2805aaea 100644 --- a/src/GitHub.Exports/Settings/Guids.cs +++ b/src/GitHub.Exports/Settings/Guids.cs @@ -13,6 +13,7 @@ public static class Guids public const string CodeContainerProviderId = "6CE146CB-EF57-4F2C-A93F-5BA685317660"; public const string InlineReviewsPackageId = "248325BE-4A2D-4111-B122-E7D59BF73A35"; public const string PullRequestStatusPackageId = "5121BEC6-1088-4553-8453-0DDC7C8E2238"; + public const string GitHubPanePackageId = "0A40459D-6B6D-4110-B6CE-EC83C0BC6A09"; public const string TeamExplorerWelcomeMessage = "C529627F-8AA6-4FDB-82EB-4BFB7DB753C3"; public const string LoginManagerId = "7BA2071A-790A-4F95-BE4A-0EEAA5928AAF"; diff --git a/src/GitHub.InlineReviews/InlineReviewsPackage.cs b/src/GitHub.InlineReviews/InlineReviewsPackage.cs index 24f9c105a4..1bb4310743 100644 --- a/src/GitHub.InlineReviews/InlineReviewsPackage.cs +++ b/src/GitHub.InlineReviews/InlineReviewsPackage.cs @@ -35,14 +35,17 @@ protected override async Task InitializeAsync( async Task InitializeMenus() { - var menuService = (IMenuCommandService)(await GetServiceAsync(typeof(IMenuCommandService))); var componentModel = (IComponentModel)(await GetServiceAsync(typeof(SComponentModel))); var exports = componentModel.DefaultExportProvider; + var commands = new IVsCommandBase[] + { + exports.GetExportedValue(), + exports.GetExportedValue() + }; await JoinableTaskFactory.SwitchToMainThreadAsync(); - menuService.AddCommands( - exports.GetExportedValue(), - exports.GetExportedValue()); + var menuService = (IMenuCommandService)(await GetServiceAsync(typeof(IMenuCommandService))); + menuService.AddCommands(commands); } } } diff --git a/src/GitHub.InlineReviews/PullRequestStatusBarPackage.cs b/src/GitHub.InlineReviews/PullRequestStatusBarPackage.cs index 30cdf34150..55eeb756e6 100644 --- a/src/GitHub.InlineReviews/PullRequestStatusBarPackage.cs +++ b/src/GitHub.InlineReviews/PullRequestStatusBarPackage.cs @@ -1,12 +1,12 @@ using System; using System.Threading; using System.Runtime.InteropServices; -using GitHub.Services; using GitHub.VisualStudio; using GitHub.InlineReviews.Services; using Microsoft.VisualStudio.Shell; using Task = System.Threading.Tasks.Task; using Microsoft.VisualStudio.Threading; +using Microsoft.VisualStudio.ComponentModelHost; namespace GitHub.InlineReviews { @@ -27,9 +27,9 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke async Task InitializeStatusBar() { - var usageTracker = (IUsageTracker)await GetServiceAsync(typeof(IUsageTracker)); - var serviceProvider = (IGitHubServiceProvider)await GetServiceAsync(typeof(IGitHubServiceProvider)); - var barManager = new PullRequestStatusBarManager(usageTracker, serviceProvider); + var componentModel = (IComponentModel)(await GetServiceAsync(typeof(SComponentModel))); + var exports = componentModel.DefaultExportProvider; + var barManager = exports.GetExportedValue(); await JoinableTaskFactory.SwitchToMainThreadAsync(); barManager.StartShowingStatus(); diff --git a/src/GitHub.InlineReviews/Services/PullRequestStatusBarManager.cs b/src/GitHub.InlineReviews/Services/PullRequestStatusBarManager.cs index c013f1ce5b..cd2c204538 100644 --- a/src/GitHub.InlineReviews/Services/PullRequestStatusBarManager.cs +++ b/src/GitHub.InlineReviews/Services/PullRequestStatusBarManager.cs @@ -1,16 +1,14 @@ using System; using System.Windows; -using System.Windows.Input; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.ComponentModel.Composition; +using GitHub.Commands; using GitHub.InlineReviews.Views; using GitHub.InlineReviews.ViewModels; using GitHub.Services; -using GitHub.VisualStudio; using GitHub.Models; using GitHub.Logging; -using GitHub.Extensions; using Serilog; using ReactiveUI; @@ -19,21 +17,25 @@ namespace GitHub.InlineReviews.Services /// /// Manage the UI that shows the PR for the current branch. /// + [Export(typeof(PullRequestStatusBarManager))] public class PullRequestStatusBarManager { static readonly ILogger log = LogManager.ForContext(); const string StatusBarPartName = "PART_SccStatusBarHost"; - readonly IUsageTracker usageTracker; - readonly IGitHubServiceProvider serviceProvider; + readonly IShowCurrentPullRequestCommand showCurrentPullRequestCommand; - IPullRequestSessionManager pullRequestSessionManager; + // At the moment this must be constructed on the main thread. + // TeamExplorerContext needs to retrieve DTE using GetService. + readonly Lazy pullRequestSessionManager; [ImportingConstructor] - public PullRequestStatusBarManager(IUsageTracker usageTracker, IGitHubServiceProvider serviceProvider) + public PullRequestStatusBarManager( + IShowCurrentPullRequestCommand showCurrentPullRequestCommand, + Lazy pullRequestSessionManager) { - this.usageTracker = usageTracker; - this.serviceProvider = serviceProvider; + this.showCurrentPullRequestCommand = showCurrentPullRequestCommand; + this.pullRequestSessionManager = pullRequestSessionManager; } /// @@ -46,8 +48,7 @@ public void StartShowingStatus() { try { - pullRequestSessionManager = serviceProvider.GetService(); - pullRequestSessionManager.WhenAnyValue(x => x.CurrentSession) + pullRequestSessionManager.Value.WhenAnyValue(x => x.CurrentSession) .Subscribe(x => RefreshCurrentSession()); } catch (Exception e) @@ -58,16 +59,14 @@ public void StartShowingStatus() void RefreshCurrentSession() { - var pullRequest = pullRequestSessionManager.CurrentSession?.PullRequest; + var pullRequest = pullRequestSessionManager.Value.CurrentSession?.PullRequest; var viewModel = pullRequest != null ? CreatePullRequestStatusViewModel(pullRequest) : null; ShowStatus(viewModel); } PullRequestStatusViewModel CreatePullRequestStatusViewModel(IPullRequestModel pullRequest) { - var dte = serviceProvider.TryGetService(); - var command = new RaisePullRequestCommand(dte, usageTracker); - var pullRequestStatusViewModel = new PullRequestStatusViewModel(command); + var pullRequestStatusViewModel = new PullRequestStatusViewModel(showCurrentPullRequestCommand); pullRequestStatusViewModel.Number = pullRequest.Number; pullRequestStatusViewModel.Title = pullRequest.Title; return pullRequestStatusViewModel; @@ -111,44 +110,5 @@ StatusBar FindSccStatusBar(Window mainWindow) var contentControl = mainWindow?.Template?.FindName(StatusBarPartName, mainWindow) as ContentControl; return contentControl?.Content as StatusBar; } - - class RaisePullRequestCommand : ICommand - { - readonly string guid = Guids.guidGitHubCmdSetString; - readonly int id = PkgCmdIDList.showCurrentPullRequestCommand; - - readonly EnvDTE.DTE dte; - readonly IUsageTracker usageTracker; - - internal RaisePullRequestCommand(EnvDTE.DTE dte, IUsageTracker usageTracker) - { - this.dte = dte; - this.usageTracker = usageTracker; - } - - public bool CanExecute(object parameter) => true; - - public void Execute(object parameter) - { - try - { - object customIn = null; - object customOut = null; - dte?.Commands.Raise(guid, id, ref customIn, ref customOut); - } - catch (Exception e) - { - log.Error(e, "Couldn't raise {Guid}:{ID}", guid, id); - } - - usageTracker.IncrementCounter(x => x.NumberOfShowCurrentPullRequest).Forget(); - } - - public event EventHandler CanExecuteChanged - { - add { } - remove { } - } - } } } diff --git a/src/GitHub.VisualStudio/Commands/ShowCurrentPullRequestCommand.cs b/src/GitHub.VisualStudio/Commands/ShowCurrentPullRequestCommand.cs index 749db44dfc..48c9f9ae19 100644 --- a/src/GitHub.VisualStudio/Commands/ShowCurrentPullRequestCommand.cs +++ b/src/GitHub.VisualStudio/Commands/ShowCurrentPullRequestCommand.cs @@ -4,6 +4,7 @@ using GitHub.Commands; using GitHub.Logging; using GitHub.Services; +using GitHub.Extensions; using GitHub.Services.Vssdk.Commands; using Serilog; @@ -20,12 +21,14 @@ public class ShowCurrentPullRequestCommand : VsCommand, IShowCurrentPullRequestC { static readonly ILogger log = LogManager.ForContext(); readonly IGitHubServiceProvider serviceProvider; + readonly Lazy usageTracker; [ImportingConstructor] - protected ShowCurrentPullRequestCommand(IGitHubServiceProvider serviceProvider) + protected ShowCurrentPullRequestCommand(IGitHubServiceProvider serviceProvider, Lazy usageTracker) : base(CommandSet, CommandId) { this.serviceProvider = serviceProvider; + this.usageTracker = usageTracker; } /// @@ -58,6 +61,8 @@ public override async Task Execute() var manager = serviceProvider.TryGetService(); var host = await manager.ShowGitHubPane(); await host.ShowPullRequest(session.RepositoryOwner, host.LocalRepository.Name, pullRequest.Number); + + usageTracker.Value.IncrementCounter(x => x.NumberOfShowCurrentPullRequest).Forget(); } catch (Exception ex) { diff --git a/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj b/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj index 8f0a917e97..ce9380d2ce 100644 --- a/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj +++ b/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj @@ -315,6 +315,7 @@ + diff --git a/src/GitHub.VisualStudio/GitHubPackage.cs b/src/GitHub.VisualStudio/GitHubPackage.cs index 233ce50840..d008f5ac1c 100644 --- a/src/GitHub.VisualStudio/GitHubPackage.cs +++ b/src/GitHub.VisualStudio/GitHubPackage.cs @@ -7,7 +7,6 @@ using System.Runtime.InteropServices; using GitHub.Api; using GitHub.Commands; -using GitHub.Helpers; using GitHub.Info; using GitHub.Exports; using GitHub.Logging; @@ -29,7 +28,6 @@ namespace GitHub.VisualStudio [Guid(Guids.guidGitHubPkgString)] [ProvideMenuResource("Menus.ctmenu", 1)] [ProvideAutoLoad(Guids.UIContext_Git, PackageAutoLoadFlags.BackgroundLoad)] - [ProvideToolWindow(typeof(GitHubPane), Orientation = ToolWindowOrientation.Right, Style = VsDockStyle.Tabbed, Window = EnvDTE.Constants.vsWindowKindSolutionExplorer)] [ProvideOptionPage(typeof(OptionsPage), "GitHub for Visual Studio", "General", 0, 0, supportsAutomation: true)] public class GitHubPackage : AsyncPackage { @@ -56,12 +54,10 @@ void LogVersionInformation() async Task InitializeMenus() { - var menuService = (IMenuCommandService)(await GetServiceAsync(typeof(IMenuCommandService))); var componentModel = (IComponentModel)(await GetServiceAsync(typeof(SComponentModel))); var exports = componentModel.DefaultExportProvider; - - await JoinableTaskFactory.SwitchToMainThreadAsync(); - menuService.AddCommands( + var commands = new IVsCommandBase[] + { exports.GetExportedValue(), exports.GetExportedValue(), exports.GetExportedValue(), @@ -69,7 +65,12 @@ async Task InitializeMenus() exports.GetExportedValue(), exports.GetExportedValue(), exports.GetExportedValue(), - exports.GetExportedValue()); + exports.GetExportedValue() + }; + + await JoinableTaskFactory.SwitchToMainThreadAsync(); + var menuService = (IMenuCommandService)(await GetServiceAsync(typeof(IMenuCommandService))); + menuService.AddCommands(commands); } async Task EnsurePackageLoaded(Guid packageGuid) @@ -147,9 +148,8 @@ public async Task ShowGitHubPane() ErrorHandler.Failed(frame.Show()); } - var viewModel = (IGitHubPaneViewModel)((FrameworkElement)pane.Content).DataContext; - await viewModel.InitializeAsync(pane); - return viewModel; + var gitHubPane = (GitHubPane)pane; + return await gitHubPane.GetViewModelAsync(); } static ToolWindowPane ShowToolWindow(Guid windowGuid) diff --git a/src/GitHub.VisualStudio/GitHubPanePackage.cs b/src/GitHub.VisualStudio/GitHubPanePackage.cs new file mode 100644 index 0000000000..a92afcef08 --- /dev/null +++ b/src/GitHub.VisualStudio/GitHubPanePackage.cs @@ -0,0 +1,22 @@ +using System; +using System.Runtime.InteropServices; +using GitHub.VisualStudio.UI; +using Microsoft.VisualStudio.Shell; + +namespace GitHub.VisualStudio +{ + /// + /// This is the host package for the tool window. + /// + /// + /// This package mustn't use MEF. + /// See: https://github.com/github/VisualStudio/issues/1550 + /// + [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] + [Guid(Guids.GitHubPanePackageId)] + [ProvideToolWindow(typeof(GitHubPane), Orientation = ToolWindowOrientation.Right, + Style = VsDockStyle.Tabbed, Window = EnvDTE.Constants.vsWindowKindSolutionExplorer)] + public sealed class GitHubPanePackage : AsyncPackage + { + } +} \ No newline at end of file diff --git a/src/GitHub.VisualStudio/UI/GitHubPane.cs b/src/GitHub.VisualStudio/UI/GitHubPane.cs index df89bc39b4..7b44755d79 100644 --- a/src/GitHub.VisualStudio/UI/GitHubPane.cs +++ b/src/GitHub.VisualStudio/UI/GitHubPane.cs @@ -1,18 +1,20 @@ using System; +using System.Threading.Tasks; using System.ComponentModel.Design; using System.Diagnostics.CodeAnalysis; using System.Reactive.Linq; using System.Runtime.InteropServices; using System.Windows; -using GitHub.Extensions; +using System.Windows.Controls; using GitHub.Factories; -using GitHub.Models; using GitHub.Services; -using GitHub.ViewModels; using GitHub.ViewModels.GitHubPane; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; +using Microsoft.VisualStudio.ComponentModelHost; +using Microsoft.VisualStudio.Threading; using ReactiveUI; +using IAsyncServiceProvider = Microsoft.VisualStudio.Shell.IAsyncServiceProvider; namespace GitHub.VisualStudio.UI { @@ -28,22 +30,24 @@ namespace GitHub.VisualStudio.UI /// /// [Guid(GitHubPaneGuid)] - public class GitHubPane : ToolWindowPane, IServiceProviderAware + public class GitHubPane : ToolWindowPane { public const string GitHubPaneGuid = "6b0fdc0a-f28e-47a0-8eed-cc296beff6d2"; - bool initialized = false; + + JoinableTask viewModelTask; + IDisposable viewSubscription; - IGitHubPaneViewModel viewModel; + ContentPresenter contentPresenter; - FrameworkElement View + public FrameworkElement View { - get { return Content as FrameworkElement; } + get { return contentPresenter.Content as FrameworkElement; } set { viewSubscription?.Dispose(); viewSubscription = null; - Content = value; + contentPresenter.Content = value; viewSubscription = value.WhenAnyValue(x => x.DataContext) .SelectMany(x => @@ -61,6 +65,7 @@ FrameworkElement View public GitHubPane() : base(null) { Caption = "GitHub"; + Content = contentPresenter = new ContentPresenter(); BitmapImageMoniker = new Microsoft.VisualStudio.Imaging.Interop.ImageMoniker() { @@ -75,24 +80,39 @@ public GitHubPane() : base(null) protected override void Initialize() { - base.Initialize(); - Initialize(this); + // Using JoinableTaskFactory from parent AsyncPackage. That way if VS shuts down before this + // work is done, we won't risk crashing due to arbitrary work going on in background threads. + var asyncPackage = (AsyncPackage)Package; + viewModelTask = asyncPackage.JoinableTaskFactory.RunAsync(() => InitializeAsync(asyncPackage)); } - public void Initialize(IServiceProvider serviceProvider) + public Task GetViewModelAsync() => viewModelTask.JoinAsync(); + + async Task InitializeAsync(AsyncPackage asyncPackage) { - if (!initialized) + try { - var provider = VisualStudio.Services.GitHubServiceProvider; + ShowInitializing(); + + // Allow MEF to initialize its cache asynchronously + var provider = (IGitHubServiceProvider)await asyncPackage.GetServiceAsync(typeof(IGitHubServiceProvider)); + var teServiceHolder = provider.GetService(); - teServiceHolder.ServiceProvider = serviceProvider; + teServiceHolder.ServiceProvider = this; var factory = provider.GetService(); - viewModel = provider.ExportProvider.GetExportedValue(); - viewModel.InitializeAsync(this).Forget(); + var viewModel = provider.ExportProvider.GetExportedValue(); + await viewModel.InitializeAsync(this); View = factory.CreateView(); View.DataContext = viewModel; + + return viewModel; + } + catch (Exception e) + { + ShowError(e); + throw; } } @@ -131,6 +151,20 @@ public override void OnToolWindowCreated() UpdateSearchHost(pane?.IsSearchEnabled ?? false, pane?.SearchQuery); } + void ShowInitializing() + { + // This page is intentionally left blank. + } + + void ShowError(Exception e) + { + View = new TextBox + { + Text = e.ToString(), + IsReadOnly = true, + }; + } + void UpdateSearchHost(bool enabled, string query) { if (SearchHost != null)