Skip to content
This repository was archived by the owner on Jun 21, 2023. It is now read-only.

Initialize GitHubPane and menus asynchronously #1569

Merged
merged 25 commits into from
Apr 9, 2018
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c20e618
Create MEF commands on background thread
jcansdale Mar 20, 2018
0527497
Allow menuService.AddCommands to be called from a b/g thread
jcansdale Mar 21, 2018
266ec58
Construct PullRequestStatusBarManager using MEF on b/g thread
jcansdale Mar 21, 2018
5730008
Factor b/g loading menu code into AsyncMenuPackage
jcansdale Mar 21, 2018
ec46954
Restore missing metrics
jcansdale Mar 21, 2018
48cb0cc
Allow MEF to refresh its cache on a background thread
jcansdale Mar 22, 2018
ca0a273
Use ContentPresenter instead of ContentControl
jcansdale Mar 27, 2018
f66afaf
Use package without MEF dependency for GitHubPane
jcansdale Mar 27, 2018
47d3753
Revert "Factor b/g loading menu code into AsyncMenuPackage"
jcansdale Mar 28, 2018
e9c8db9
Revert "Allow menuService.AddCommands to be called from a b/g thread"
jcansdale Mar 28, 2018
2fc764b
Add placeholder initializing and error views
jcansdale Mar 28, 2018
e6ee44d
Add GetViewModelAsync for GitHubPane
jcansdale Mar 29, 2018
ed0cb39
Remove redundant initialization code
jcansdale Mar 29, 2018
5dcf605
Fixed typo
jcansdale Apr 3, 2018
29508bb
Make error text box read only
jcansdale Apr 4, 2018
89ffd87
Show `Initializing...` while MEF is loading
jcansdale Apr 4, 2018
e3158d2
Make GitHubPane await initialization of IGitHubPaneViewModel
jcansdale Apr 4, 2018
255b007
Remove empty IncrementNumberOfShowCurrentPullRequest method
jcansdale Apr 4, 2018
4746859
Cast IMenuCommandService on Main thread
jcansdale Apr 4, 2018
a9c5c16
Use JoinableTask rather than TaskCompletionSource
jcansdale Apr 4, 2018
0504119
This page is intentionally left blank
jcansdale Apr 4, 2018
cb26608
Use JoinableTaskFactory from parent AsyncPackage
jcansdale Apr 6, 2018
7511113
Retrieve IGitHubServiceProvider asynchronously
jcansdale Apr 6, 2018
0983ef0
Moved NumberOfShowCurrentPullRequest tracking into command
jcansdale Apr 9, 2018
39d77aa
Keep CA happy
jcansdale Apr 9, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/GitHub.Exports/Settings/Guids.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
23 changes: 3 additions & 20 deletions src/GitHub.InlineReviews/InlineReviewsPackage.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
using System;
using System.ComponentModel.Design;
using System.Runtime.InteropServices;
using System.Threading;
using GitHub.Commands;
using GitHub.InlineReviews.Views;
using GitHub.Services.Vssdk;
using GitHub.Services.Vssdk.Commands;
using GitHub.VisualStudio;
using Microsoft.VisualStudio.ComponentModelHost;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Threading;
using Task = System.Threading.Tasks.Task;

namespace GitHub.InlineReviews
Expand All @@ -18,28 +16,13 @@ namespace GitHub.InlineReviews
[ProvideAutoLoad(Guids.UIContext_Git, PackageAutoLoadFlags.BackgroundLoad)]
[ProvideMenuResource("Menus.ctmenu", 1)]
[ProvideToolWindow(typeof(PullRequestCommentsPane), DocumentLikeTool = true)]
public class InlineReviewsPackage : AsyncPackage
public class InlineReviewsPackage : AsyncMenuPackage
{
protected override async Task InitializeAsync(
CancellationToken cancellationToken,
IProgress<ServiceProgressData> progress)
protected override async Task InitializeMenusAsync(OleMenuCommandService menuService)
{
var menuService = (IMenuCommandService)(await GetServiceAsync(typeof(IMenuCommandService)));
var componentModel = (IComponentModel)(await GetServiceAsync(typeof(SComponentModel)));
var exports = componentModel.DefaultExportProvider;

// Avoid delays when there is ongoing UI activity.
// See: https://github.com/github/VisualStudio/issues/1537
await JoinableTaskFactory.RunAsync(VsTaskRunContext.UIThreadNormalPriority, InitializeMenus);
}

async Task InitializeMenus()
{
var menuService = (IMenuCommandService)(await GetServiceAsync(typeof(IMenuCommandService)));
var componentModel = (IComponentModel)(await GetServiceAsync(typeof(SComponentModel)));
var exports = componentModel.DefaultExportProvider;

await JoinableTaskFactory.SwitchToMainThreadAsync();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should bring back the SwitchToMainThreadAsync call that you removed. I expect the menuService is an STA COM object that requires the UI thread.

menuService.AddCommands(
exports.GetExportedValue<INextInlineCommentCommand>(),
exports.GetExportedValue<IPreviousInlineCommentCommand>());
Expand Down
8 changes: 4 additions & 4 deletions src/GitHub.InlineReviews/PullRequestStatusBarPackage.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand All @@ -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<PullRequestStatusBarManager>();

await JoinableTaskFactory.SwitchToMainThreadAsync();
barManager.StartShowingStatus();
Expand Down
68 changes: 37 additions & 31 deletions src/GitHub.InlineReviews/Services/PullRequestStatusBarManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
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;
Expand All @@ -19,21 +19,28 @@ namespace GitHub.InlineReviews.Services
/// <summary>
/// Manage the UI that shows the PR for the current branch.
/// </summary>
[Export(typeof(PullRequestStatusBarManager))]
public class PullRequestStatusBarManager
{
static readonly ILogger log = LogManager.ForContext<PullRequestStatusBarManager>();
const string StatusBarPartName = "PART_SccStatusBarHost";

readonly IUsageTracker usageTracker;
readonly IGitHubServiceProvider serviceProvider;
readonly IShowCurrentPullRequestCommand showCurrentPullRequestCommand;

IPullRequestSessionManager pullRequestSessionManager;
// More the moment this must be constructed on the main thread.
// TeamExplorerContext needs to retrieve DTE using GetService.
readonly Lazy<IPullRequestSessionManager> pullRequestSessionManager;

[ImportingConstructor]
public PullRequestStatusBarManager(IUsageTracker usageTracker, IGitHubServiceProvider serviceProvider)
public PullRequestStatusBarManager(
IUsageTracker usageTracker,
IShowCurrentPullRequestCommand showCurrentPullRequestCommand,
Lazy<IPullRequestSessionManager> pullRequestSessionManager)
{
this.usageTracker = usageTracker;
this.serviceProvider = serviceProvider;
this.showCurrentPullRequestCommand = showCurrentPullRequestCommand;
this.pullRequestSessionManager = pullRequestSessionManager;
}

/// <summary>
Expand All @@ -46,8 +53,7 @@ public void StartShowingStatus()
{
try
{
pullRequestSessionManager = serviceProvider.GetService<IPullRequestSessionManager>();
pullRequestSessionManager.WhenAnyValue(x => x.CurrentSession)
pullRequestSessionManager.Value.WhenAnyValue(x => x.CurrentSession)
.Subscribe(x => RefreshCurrentSession());
}
catch (Exception e)
Expand All @@ -58,21 +64,24 @@ 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<EnvDTE.DTE>();
var command = new RaisePullRequestCommand(dte, usageTracker);
var pullRequestStatusViewModel = new PullRequestStatusViewModel(command);
var trackingCommand = new UsageTrackingCommand(showCurrentPullRequestCommand, usageTracker);
var pullRequestStatusViewModel = new PullRequestStatusViewModel(trackingCommand);
pullRequestStatusViewModel.Number = pullRequest.Number;
pullRequestStatusViewModel.Title = pullRequest.Title;
return pullRequestStatusViewModel;
}

void IncrementNumberOfShowCurrentPullRequest()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty method?

{
}

void ShowStatus(PullRequestStatusViewModel pullRequestStatusViewModel = null)
{
var statusBar = FindSccStatusBar(Application.Current.MainWindow);
Expand Down Expand Up @@ -112,42 +121,39 @@ StatusBar FindSccStatusBar(Window mainWindow)
return contentControl?.Content as StatusBar;
}

class RaisePullRequestCommand : ICommand
class UsageTrackingCommand : ICommand
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this intended to be a more general class now? (the rename suggests that it is). If so, should it be moved to a place where it can be reused more easily and made to accept a lambda which identifies the usage model field? This doesn't need to be in this PR though.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed it to use the IShowCurrentPullRequestCommand directly rather than using dte.Commands.Raise. The problme was I lost the metrics.

I'm thinking it would be a better idea to record the metrics in the command itself? Only issue is, you wouldn't be able to differentiate where the user found the command (not that it matters yet in this case).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll fix this in another PR. It's working for the moment.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've moved the tracking of NumberOfShowCurrentPullRequest into the command itself (where I think it belongs). RaisePullRequestCommand is now obsolete.

{
readonly string guid = Guids.guidGitHubCmdSetString;
readonly int id = PkgCmdIDList.showCurrentPullRequestCommand;

readonly EnvDTE.DTE dte;
readonly ICommand command;
readonly IUsageTracker usageTracker;

internal RaisePullRequestCommand(EnvDTE.DTE dte, IUsageTracker usageTracker)
internal UsageTrackingCommand(ICommand command, IUsageTracker usageTracker)
{
this.dte = dte;
this.command = command;
this.usageTracker = usageTracker;
}

public bool CanExecute(object parameter) => true;

public void Execute(object parameter)
public event EventHandler CanExecuteChanged
{
try
add
{
object customIn = null;
object customOut = null;
dte?.Commands.Raise(guid, id, ref customIn, ref customOut);
command.CanExecuteChanged += value;
}
catch (Exception e)

remove
{
log.Error(e, "Couldn't raise {Guid}:{ID}", guid, id);
command.CanExecuteChanged -= value;
}
}

usageTracker.IncrementCounter(x => x.NumberOfShowCurrentPullRequest).Forget();
public bool CanExecute(object parameter)
{
return command.CanExecute(parameter);
}

public event EventHandler CanExecuteChanged
public void Execute(object parameter)
{
add { }
remove { }
command.Execute(parameter);
usageTracker.IncrementCounter(x => x.NumberOfShowCurrentPullRequest).Forget();
}
}
}
Expand Down
41 changes: 41 additions & 0 deletions src/GitHub.Services.Vssdk/AsyncMenuPackage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System;
using System.Threading;
using System.ComponentModel.Design;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Task = System.Threading.Tasks.Task;

namespace GitHub.Services.Vssdk
{
public abstract class AsyncMenuPackage : AsyncPackage
{
IVsUIShell vsUIShell;

sealed protected async override Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress)
{
vsUIShell = await GetServiceAsync(typeof(SVsUIShell)) as IVsUIShell;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should use await this.JoinableTaskFactory.SwitchToMainThreadAsync(CancellationToken); as the first line in your method. Casting the SVsUIShell object to an interface itself generally requires the UI thread, and otherwise you're vulnerable to a deadlock by relying on RPC to get you to the UI thread.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an helper method btw that you can use as ServiceProvider.GetGlobalServiceAsync<SVsUIShell, IVsUIShell> that handles this automatically.


var menuCommandService = (OleMenuCommandService)(await GetServiceAsync(typeof(IMenuCommandService)));
await InitializeMenusAsync(menuCommandService);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since menus have to be initialized on main thread, would be a good idea to switch to main thread before calling InitializeMenusAsync.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could... but then as an async method of its own, it should be sure to switch within the method itself.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you pass CancellationToken into this method so that it can cancel async init operations if VS starts to shut down?

}

protected abstract Task InitializeMenusAsync(OleMenuCommandService menuService);

// The IDesignerHost, ISelectionService and IVsUIShell are requested by the MenuCommandService.
// This override allows IMenuCommandService.AddCommands to be called form a background thread.
protected override object GetService(Type serviceType)
{
if (serviceType == typeof(SVsUIShell))
{
return vsUIShell;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your comment here is interesting. Returning this STA COM object object from a free-threaded method doesn't solve the problem that the caller will very likely cast this to an interface while still on the background thread that your comment suggests it will be on. You mustn't cast COM objects to any interface, or invoke those interfaces, while on a background thread unless you're sure they are free-threaded objects (and I don't think this one is one of those few).

}

if (serviceType == typeof(ISelectionService) || serviceType == typeof(IDesignerHost))
{
return null;
}

return base.GetService(serviceType);
}
}
}
1 change: 1 addition & 0 deletions src/GitHub.Services.Vssdk/GitHub.Services.Vssdk.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@
<Compile Include="..\common\SolutionInfo.cs">
<Link>Properties\SolutionInfo.cs</Link>
</Compile>
<Compile Include="AsyncMenuPackage.cs" />
<Compile Include="Commands\MenuCommandServiceExtensions.cs" />
<Compile Include="Commands\VsCommand.cs" />
<Compile Include="Commands\VsCommandBase.cs" />
Expand Down
1 change: 1 addition & 0 deletions src/GitHub.VisualStudio/GitHub.VisualStudio.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@
<Compile Include="Commands\ShowGitHubPaneCommand.cs" />
<Compile Include="Commands\OpenPullRequestsCommand.cs" />
<Compile Include="GitContextPackage.cs" />
<Compile Include="GitHubPanePackage.cs" />
<Compile Include="IServiceProviderPackage.cs" />
<Compile Include="Helpers\ActiveDocumentSnapshot.cs" />
<Compile Include="Commands\AddConnectionCommand.cs" />
Expand Down
37 changes: 13 additions & 24 deletions src/GitHub.VisualStudio/GitHubPackage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,18 @@
using System.Windows;
using System.Threading;
using System.Threading.Tasks;
using System.ComponentModel.Design;
using System.ComponentModel.Composition;
using System.Runtime.InteropServices;
using GitHub.Api;
using GitHub.Commands;
using GitHub.Helpers;
using GitHub.Info;
using GitHub.Exports;
using GitHub.Logging;
using GitHub.Services;
using GitHub.Services.Vssdk.Commands;
using GitHub.ViewModels.GitHubPane;
using GitHub.VisualStudio.UI;
using GitHub.Services.Vssdk;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.ComponentModelHost;
using Microsoft.VisualStudio.Shell;
Expand All @@ -29,38 +28,19 @@ 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
public class GitHubPackage : AsyncMenuPackage
{
static readonly ILogger log = LogManager.ForContext<GitHubPackage>();

protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress)
protected async override Task InitializeMenusAsync(OleMenuCommandService menuService)
{
LogVersionInformation();
await base.InitializeAsync(cancellationToken, progress);
await GetServiceAsync(typeof(IUsageTracker));

// Avoid delays when there is ongoing UI activity.
// See: https://github.com/github/VisualStudio/issues/1537
await JoinableTaskFactory.RunAsync(VsTaskRunContext.UIThreadNormalPriority, InitializeMenus);
}

void LogVersionInformation()
{
var packageVersion = ApplicationInfo.GetPackageVersion(this);
var hostVersionInfo = ApplicationInfo.GetHostVersionInfo();
log.Information("Initializing GitHub Extension v{PackageVersion} in {$FileDescription} ({$ProductVersion})",
packageVersion, hostVersionInfo.FileDescription, hostVersionInfo.ProductVersion);
}

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(
exports.GetExportedValue<IAddConnectionCommand>(),
exports.GetExportedValue<IBlameLinkCommand>(),
Expand All @@ -72,6 +52,14 @@ async Task InitializeMenus()
exports.GetExportedValue<IShowGitHubPaneCommand>());
}

void LogVersionInformation()
{
var packageVersion = ApplicationInfo.GetPackageVersion(this);
var hostVersionInfo = ApplicationInfo.GetHostVersionInfo();
log.Information("Initializing GitHub Extension v{PackageVersion} in {$FileDescription} ({$ProductVersion})",
packageVersion, hostVersionInfo.FileDescription, hostVersionInfo.ProductVersion);
}

async Task EnsurePackageLoaded(Guid packageGuid)
{
var shell = await GetServiceAsync(typeof(SVsShell)) as IVsShell;
Expand Down Expand Up @@ -147,7 +135,8 @@ public async Task<IGitHubPaneViewModel> ShowGitHubPane()
ErrorHandler.Failed(frame.Show());
}

var viewModel = (IGitHubPaneViewModel)((FrameworkElement)pane.Content).DataContext;
var gitHubPane = (GitHubPane)pane;
var viewModel = (IGitHubPaneViewModel)(gitHubPane.View).DataContext;
await viewModel.InitializeAsync(pane);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't viewModel.InitializeAsync have already been called by the time GetViewModelAsync returns?

return viewModel;
}
Expand Down
22 changes: 22 additions & 0 deletions src/GitHub.VisualStudio/GitHubPanePackage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System;
using System.Runtime.InteropServices;
using GitHub.VisualStudio.UI;
using Microsoft.VisualStudio.Shell;

namespace GitHub.VisualStudio
{
/// <summary>
/// This is the host package for the <see cref="GitHubPane"/> tool window.
/// </summary>
/// <remarks>
/// This package mustn't use MEF.
/// See: https://github.com/github/VisualStudio/issues/1550
/// </remarks>
[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
{
}
}
Loading