Check out the other posts in this series!
- Extending Visual Studio CodeLens Functionality
- YOU ARE HERE!
- Coming Soon!
If you remember Part I of this blog post series (which was only 8 months ago - sorry about that đŹ), youâll know that in THIS post, weâre going to talk about how you can make calls back into services in your extension host FROM the CodeLens provider.
Since the CodeLens provider / service runs out of process from your main extension, its hard to get back to some of the goodness youâve already built out there - like services, commands, etc. But, fear not, with a couple interfaces and some MEF injection, we can make it all work!
Letâs start with a base request of getting the Visual Studio process ID FROM the CodeLens provider. Now, since the CodeLens provider runs out of process from your extension, that means it also runs out of process from Visual Studio, too, which means we canât access it directly from our provider.
The first thing youâll want to do is add a constructor to your CodeLevelMetricsProvider
class that we created in Part I of this series, and have it set to be the ImportingConstructor
for MEF -
[ImportingConstructor]
public CodeLevelMetricsProvider() {
}
Now, we need to add a parameter to the constructor and store it in a private field -
private readonly Lazy<ICodeLensCallbackService> _callbackService;
[ImportingConstructor]
public CodeLevelMetricsProvider(Lazy<ICodeLensCallbackService> callbackService) {
_callbackService = callbackService;
}
Lazy
is just a way to provide lazy initialization, and the ICodeLensCallbackService
is a special interface from the Microsoft.VisualStudio.Language.CodeLens.Remoting
namespace (so youâll need to add that if youâre getting errors).
Now, in your main extension, weâre going to create a new âserviceâ. I called mine CodeLevelMetricsCallbackService
. Letâs start by having it -
- Bring in the
Microsoft.VisualStudio.Language.CodeLens
namespace - Implement the
ICodeLensCallbackListener
- Export as Shared with MEF -
using Microsoft.VisualStudio.Language.CodeLens;
namespace CodeLensServices { // Whatever your ACTUAL namespace is can go here :)
[Export(typeof(ICodeLensCallbackListener))]
[PartCreationPolicy(CreationPolicy.Shared)]
[ContentType("CSharp")]
public class CodeLevelMetricsCallbackService : ICodeLensCallbackListener {
}
}
This next part isnât REQUIRED, but I think it helps to clean up some âmagic stringsâ that may appear otherwise. Letâs create a new interface for our specific purposes -
public interface ICodeLevelMetricsCallbackService {
}
And since we know weâre trying to get the Visual Studio process ID, lets declare that desire in this interface -
public interface ICodeLevelMetricsCallbackService{
int GetVisualStudioPid();
}
Armed with this new interface, letâs go back to our CodeLensService
and implement this interface as well, which would look something like this -
using Microsoft.VisualStudio.Language.CodeLens;
namespace CodeLensServices {// Whatever your ACTUAL namespace is can go here :)
[Export(typeof(ICodeLensCallbackListener))]
[PartCreationPolicy(CreationPolicy.Shared)]
[ContentType("CSharp")]
public class CodeLevelMetricsCallbackService : ICodeLensCallbackListener, ICodeLevelMetricsCallbackService {
public int GetVisualStudioPid() {
return Process.GetCurrentProcess().Id;
}
}
}
It should be noted that if you want to reuse a service youâve already defined -
- Inject it into the constructor
- Call the appropriate method in THAT service from a method in THIS class
Alright, weâre ready to add the magic to the CodeLens provider! Letâs pass our service down to the data point from the CreateDataPointAsync
method -
public async Task<IAsyncCodeLensDataPoint> CreateDataPointAsync(CodeLensDescriptor descriptor, CodeLensDescriptorContext context, CancellationToken token) {
return new CodeLensDataPoint(descriptor, _callbackService.Value);
}
That _callbackService.Value
does the lazy loading of the service now that weâre actually trying to use it.
Now we can modify our datapoint to take the new constructor parameter, and wire it all up!
public class CodeLensDataPoint : IAsyncCodeLensDataPoint {
private readonly ICodeLensCallbackService _callbackService;
public CodeLensDataPoint(CodeLensDescriptor descriptor, ICodeLensCallbackService callbackService) {
_callbackService = callbackService;
Descriptor = descriptor;
}
public Task<CodeLensDataPointDescriptor> GetDataAsync(CodeLensDescriptorContext descriptorContext, CancellationToken token) {
var vsPid = await _callbackService
.InvokeAsync<int>(this,
nameof(ICodeLevelMetricsCallbackService.GetVisualStudioPid),
cancellationToken: token)
.ConfigureAwait(false);
return new CodeLensDataPointDescriptor {
Description = $"The Visual Studio PID is {vsPid}!",
//ImageId = Shows an image next to the Code Lens entry
//IntValue = I haven't figured this one out yet!
TooltipText = $"Shows Up On Hover, show it here, too! - VS PID = {vsPid}"
};
}
public Task<CodeLensDetailsDescriptor> GetDetailsAsync(CodeLensDescriptorContext descriptorContext, CancellationToken token) {
// this is what gets triggered when you click a Code Lens entry, and we don't really care about this part for now
return Task.FromResult<CodeLensDetailsDescriptor>(null);
}
public CodeLensDescriptor Descriptor { get; }
public event AsyncEventHandler InvalidatedAsync;
}
Notice that we only made a few changes to this class (started with the final result from Part I of the series) -
- Modified the constructor to accept the ICodeLensCallbackService, and stored it off for use later
- When we actually want to RENDER the CodeLens, we make a call through that service to the method we created to retrieve the PID
- Pass the PID we received into the Description of the CodeLens so itâll show up!
Letâs talk about that method call we added, because thats the fun part of all this. As long as you set up the interfaces correctly, Visual Studio will actually handle all the actual communication for you through calls using InvokeAsync
, which just uses RPC âunder the hoodâ.
var vsPid = await _callbackService
.InvokeAsync<int>(this,
nameof(ICodeLevelMetricsCallbackService.GetVisualStudioPid),
cancellationToken: token)
.ConfigureAwait(false);
- The initial method call here is really just like any other async method you may call - await it, invoke it, and (optionally) configure it.
InvokeAsync
is generic and requires you to specify a few things - a.int
- The return value type (What isGetVisualStudioPid
returning from our extension service?) a.this
- We need to specify the âownerâ, which in this case is the datapoint itself. a.nameof(ICodeLevelMetricsCallbackService.GetVisualStudioPid)
- I like usingnameof()
in this case for those scenarios where you may have magic strings show up. This is basically why we created that secondary interface. Technically optional, but then you would need to pass"GetVisualStudioPid"
in place ofnameof()
here instead. Blah.- Since its async, need a
cancellationToken
If you run your application, you should see a fancy new CodeLens entry that reports the process ID of Visual Studio!
First, what I call âinlineâ CodeLens -
Secondly, the âhoverâ state -
Now, before we go, I do want to note a couple other variations of InvokeAsync
that you can use if need be.
First - you can absolutely pass arguments back up through these InvokeAsync
calls. That would look something like this -
var returnVal = _callbackService.Value
.InvokeAsync<int>(this,
nameof(ICodeLevelMetricsCallbackService.MeaningOfLife),
new[] { 42 }, // arguments here - just an array of objects
cancellationToken: token)
.ConfigureAwait(false);
That would correspond to a method signature of -
public int MeaningOfLife(int meaningOfLife) {
return meaningOfLife;
}
Iâll also note that the array of arguments needs to be passed in the same order as the parameters to the method and types must match. I donât, however, see any requirement that the NAMES match.
Secondly, is that you can call methods that are just void return types, which would look something like this -
_ = _callbackService.Value
.InvokeAsync(this,
nameof(ICodeLevelMetricsCallbackService.BurnItDown),
cancellationToken: token)
.ConfigureAwait(false);
And that would coorespond to a method signature of -
public void BurnItDown() {
Process.GetCurrentProcess().Kill();
}
Of course, you can combine any combination of return types, parameters, etc as you need.
There you go dear readers! One-way communication from your out-of-process CodeLens provider BACK into your extension! Up next will be the final post in this series where we discuss how to setup TWO-WAY communication - useful for forcing your CodeLens to refresh for some reason, like an event that happened in your main extension.
Stay tuned!
This post, "Extending Visual Studio CodeLens Functionality - Part II", first appeared on https://www.codingwithcalvin.net/extending-visual-studio-codelens-functionality-part-ii/