Escolha o modelo de extensibilidade do Visual Studio certo para você
Você pode estender o Visual Studio usando três modelos principais de extensibilidade, VSSDK, Community Toolkit e VisualStudio.Extensibility. Este artigo aborda os prós e contras de cada um. Usamos um exemplo simples para destacar as diferenças de arquitetura e código entre os modelos.
VSSDK
O VSSDK (ou SDK do Visual Studio) é o modelo no qual a maioria das extensões no Visual Studio Marketplace se baseia. O próprio Visual Studio é criado com base nesse modelo. É o mais completo e o mais poderoso, mas também o mais complexo de aprender e usar corretamente. As extensões que usam o VSSDK são executadas no mesmo processo que o próprio Visual Studio. Carregar no mesmo processo que o Visual Studio significa que uma extensão que tem uma violação de acesso, loop infinito ou outros problemas pode falhar ou travar o Visual Studio e degradar a experiência do cliente. E como as extensões são executadas no mesmo processo do Visual Studio, elas só podem ser criadas usando o .NET Framework. Os extensores que desejam usar ou incorporar bibliotecas que usam o .NET 5 e posterior não podem fazer isso usando o VSSDK.
As APIs no VSSDK foram agregadas ao longo dos anos à medida que o próprio Visual Studio se transformou e evoluiu. Em uma única extensão, você pode se deparar com APIs baseadas em COM de uma marca herdada, passando pela simplicidade enganosa do DTE e mexendo nas importações e exportações do MEF . Vamos dar um exemplo de como escrever uma extensão que lê o texto do sistema de arquivos e o insere no início do documento ativo atual no editor. O trecho a seguir mostra o código que você escreveria para manipular quando um comando é invocado em uma extensão baseada em VSSDK:
private void Execute(object sender, EventArgs e)
{
var textManager = package.GetService<SVsTextManager, IVsTextManager>();
textManager.GetActiveView(1, null, out IVsTextView activeTextView);
if (activeTextView != null && activeTextView is IVsTextViewEx nativeView)
{
ErrorHandler.ThrowOnFailure(nativeView.GetWindowFrame(out object frameValue));
IComponentModel2 compService = package.GetService<SComponentModel, IComponentModel2>();
IVsEditorAdaptersFactoryService editorAdapter = compService.GetService<IVsEditorAdaptersFactoryService>();
var wpfTextView = editorAdapter?.GetWpfTextView(activeTextView);
if (frameValue is IVsWindowFrame frame && wpfTextView != null)
{
var fileText = File.ReadAllText(Path.Combine(Path.GetTempPath(), "test.txt"));
wpfTextView.TextBuffer?.Insert(0, fileText);
}
}
}
Além disso, você também precisaria fornecer um arquivo .vsct
, que define a configuração do comando, como onde colocá-lo na interface do usuário, o texto associado e assim por diante:
<Commands package="guidVSSDKPackage">
<Groups>
<Group guid="guidVSSDKPackageCmdSet" id="MyMenuGroup" priority="0x0600">
<Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_TOOLS" />
</Group>
</Groups>
<Buttons>
<Button guid="guidVSSDKPackageCmdSet" id="InsertTextCommandId" priority="0x0100" type="Button">
<Parent guid="guidVSSDKPackageCmdSet" id="MyMenuGroup" />
<Icon guid="guidImages" id="bmpPic1" />
<Strings>
<ButtonText>Invoke InsertTextCommand (Unwrapped Community Toolkit)</ButtonText>
</Strings>
</Button>
<Button guid="guidVSSDKPackageCmdSet" id="cmdidVssdkInsertTextCommand" priority="0x0100" type="Button">
<Parent guid="guidVSSDKPackageCmdSet" id="MyMenuGroup" />
<Icon guid="guidImages1" id="bmpPic1" />
<Strings>
<ButtonText>Invoke InsertTextCommand (VSSDK)</ButtonText>
</Strings>
</Button>
</Buttons>
<Bitmaps>
<Bitmap guid="guidImages" href="Resources\InsertTextCommand.png" usedList="bmpPic1, bmpPic2, bmpPicSearch, bmpPicX, bmpPicArrows, bmpPicStrikethrough" />
<Bitmap guid="guidImages1" href="Resources\VssdkInsertTextCommand.png" usedList="bmpPic1, bmpPic2, bmpPicSearch, bmpPicX, bmpPicArrows, bmpPicStrikethrough" />
</Bitmaps>
</Commands>
Como você pode ver no exemplo, o código pode parecer pouco intuitivo e é improvável que alguém familiarizado com o .NET aprenda facilmente. Há muitos conceitos a serem aprendidos e os padrões de API para acessar o texto do editor ativo são antiquados. Para a maioria dos extensores, as extensões VSSDK são construídas ao copiar e colar de fontes online, o que pode levar a sessões de depuração difíceis, tentativa e erro e frustração. Em muitos casos, as extensões do VSSDK podem não ser a maneira mais fácil de atingir as metas de extensão (embora, às vezes, sejam a única opção).
Kit de ferramentas do Community
O Community Toolkit é o modelo de extensibilidade baseado na comunidade de software livre para Visual Studio que encapsula o VSSDK para uma experiência de desenvolvimento mais fácil. Como ele se baseia no VSSDK, ele está sujeito às mesmas limitações que o VSSDK (ou seja, somente .NET Framework, sem isolamento do restante do Visual Studio e assim por diante). Continuando com o mesmo exemplo de escrita de uma extensão que insere o texto lido do sistema de arquivos, usando o Community Toolkit, a extensão seria escrita da seguinte forma para um manipulador de comandos:
protected override async Task ExecuteAsync(OleMenuCmdEventArgs e)
{
DocumentView docView = await VS.Documents.GetActiveDocumentViewAsync();
if (docView?.TextView == null) return;
var fileText = File.ReadAllText(Path.Combine(Path.GetTempPath(), "test.txt"));
docView.TextBuffer?.Insert(0, fileText);
}
O código resultante é em grande parte aprimorado do VSSDK em termos de simplicidade e intuitividade! Não apenas diminuímos significativamente o número de linhas, mas o código resultante também parece razoável. Não há necessidade de entender qual é a diferença entre SVsTextManager
e IVsTextManager
. As APIs têm mais aparência do . NET, adotando padrões comuns de nomenclatura e assíncronos, juntamente com a priorização de operações comuns. No entanto, o Community Toolkit ainda é construído sobre o modelo VSSDK existente e, portanto, os vestígios da estrutura subjacente são revelados. Por exemplo, um arquivo .vsct
ainda é necessário. Embora o Community Toolkit faça um ótimo trabalho ao simplificar as APIs, ele está vinculado às limitações do VSSDK e não tem uma maneira de simplificar a configuração da extensão.
VisualStudio.Extensibility
O VisualStudio.Extensibility é o novo modelo de extensibilidade em que as extensões são executadas fora do processo principal do Visual Studio. Devido a essa mudança arquitetônica fundamental, novos padrões e recursos agora estão disponíveis para extensões que não são possíveis com o VSSDK ou o Community Toolkit. O VisualStudio.Extensibility oferece um conjunto completamente novo de APIs que são consistentes e fáceis de usar, permite que as extensões sejam direcionadas ao .NET, isola bugs que surgem de extensões do restante do Visual Studio e permite que os usuários instalem extensões sem reiniciar o Visual Studio. No entanto, como o novo modelo é criado na nova arquitetura subjacente, ele ainda não tem a amplitude que o VSSDK e o Kit de Ferramentas da Comunidade têm. Para preencher essa lacuna, você pode executar suas extensões VisualStudio.Extensibility em processo, o que permite que você continue usando APIs do VSSDK. No entanto, isso significa que sua extensão só pode ser direcionada ao .NET Framework, pois ela compartilha o mesmo processo do Visual Studio, que se baseia no .NET Framework.
Continuando com o mesmo exemplo de escrever uma extensão que insere o texto de um arquivo, usando VisualStudio.Extensibility, a extensão seria escrita da seguinte maneira para manipulação de comandos:
public override async Task ExecuteCommandAsync(IClientContext context, CancellationToken cancellationToken)
{
var activeTextView = await context.GetActiveTextViewAsync(cancellationToken);
if (activeTextView is not null)
{
var editResult = await Extensibility.Editor().EditAsync(batch =>
{
var fileText = File.ReadAllText(Path.Combine(Path.GetTempPath(), "test.txt"));
ITextDocumentEditor editor = activeTextView.Document.AsEditable(batch);
editor.Insert(0, fileText);
}, cancellationToken);
}
}
Para configurar o comando para posicionamento, texto e assim por diante, você não precisa mais fornecer um arquivo .vsct
. Em vez disso, é feito por meio de código:
public override CommandConfiguration CommandConfiguration => new("%VisualStudio.Extensibility.Command1.DisplayName%")
{
Icon = new(ImageMoniker.KnownValues.Extension, IconSettings.IconAndText),
Placements = [CommandPlacement.KnownPlacements.ExtensionsMenu],
};
Este código é mais fácil de entender e de seguir. Na maioria das vezes, você pode escrever essa extensão puramente por meio do editor com a ajuda do IntelliSense, mesmo para configuração de comando.
Comparando os diferentes modelos de extensibilidade do Visual Studio
No exemplo, você pode observar que, usando VisualStudio.Extensibility, há mais linhas de código do que o Community Toolkit no manipulador de comandos. O Community Toolkit é um excelente wrapper fácil de usar além da criação de extensões com o VSSDK; no entanto, existem armadilhas que não são imediatamente óbvias, o que levou ao desenvolvimento do VisualStudio.Extensibility. Para entender a transição e a necessidade, especialmente quando parece que o Kit de Ferramentas da Comunidade também resulta em um código fácil de escrever e entender, vamos examinar o exemplo e comparar o que está acontecendo nas camadas mais profundas do código.
Podemos desencapsular rapidamente o código neste exemplo e ver o que realmente está sendo chamado no lado do VSSDK. Vamos nos concentrar apenas no snippet de execução de comando, pois há vários detalhes de que o VSSDK precisa, que o Community Toolkit esconde bem. Mas quando examinarmos o código subjacente, você entenderá por que a simplicidade aqui é uma compensação. A simplicidade oculta alguns dos detalhes subjacentes, o que pode levar a comportamentos inesperados, bugs e até problemas de desempenho e travamentos. O trecho de código a seguir mostra o código do Community Toolkit desencapsulado para mostrar as chamadas do VSSDK:
private void Execute(object sender, EventArgs e)
{
package.JoinableTaskFactory.RunAsync(async delegate
{
var textManager = await package.GetServiceAsync<SVsTextManager, IVsTextManager>();
textManager.GetActiveView(1, null, out IVsTextView activeTextView);
if (activeTextView != null && activeTextView is IVsTextViewEx nativeView)
{
await package.JoinableTaskFactory.SwitchToMainThreadAsync();
ErrorHandler.ThrowOnFailure(nativeView.GetWindowFrame(out object frameValue));
IComponentModel2 compService = package.GetService<SComponentModel, IComponentModel2>();
IVsEditorAdaptersFactoryService editorAdapter = compService.GetService<IVsEditorAdaptersFactoryService>();
var wpfTextView = editorAdapter?.GetWpfTextView(activeTextView);
if (frameValue is IVsWindowFrame frame && wpfTextView != null)
{
var fileText = File.ReadAllText(Path.Combine(Path.GetTempPath(), "test.txt"));
wpfTextView.TextBuffer?.Insert(0, fileText);
}
}
});
}
Há poucos problemas a serem abordados aqui, e todos eles giram em torno de threading e código assíncrono. Analisaremos cada um deles em detalhes.
API assíncrona versus execução de código assíncrona
A primeira coisa a observar é que o método ExecuteAsync
no Community Toolkit é uma chamada assíncrona de disparar e esquecer encapsulada no VSSDK:
package.JoinableTaskFactory.RunAsync(async delegate
{
…
});
O VSSDK em si não dá suporte à execução de comando assíncrono da perspectiva de uma API principal. Ou seja, quando um comando é executado, o VSSDK não tem uma maneira de executar o código do manipulador de comandos em um thread em segundo plano, aguardar a conclusão e retornar o usuário ao contexto de chamada original com os resultados da execução. Portanto, embora a API ExecuteAsync no Community Toolkit seja sintaticamente assíncrona, ela não é uma execução assíncrona verdadeira. E como é uma maneira de disparar e esquecer a execução assíncrona, você pode chamar ExecuteAsync repetidamente sem nunca esperar que a chamada anterior seja concluída primeiro. Embora o Community Toolkit forneça uma experiência melhor em termos de ajudar os extensores a descobrir como implementar cenários comuns, ele não pode resolver os problemas fundamentais com o VSSDK. Nesse caso, a API VSSDK subjacente não é assíncrona e os métodos auxiliares de disparar e esquecer fornecidos pelo Community Toolkit não podem abordar corretamente a produção assíncrona e o trabalho com o estado do cliente; ele pode ocultar alguns possíveis problemas difíceis de depurar.
Thread de interface do usuário versus thread em segundo plano
A outra consequência dessa chamada assíncrona encapsulada do Community Toolkit é que o código em si ainda é executado do thread da interface do usuário e cabe ao desenvolvedor da extensão descobrir como alternar corretamente para um thread em segundo plano se você não quiser se arriscar a congelar a interface do usuário. Por mais que o Community Toolkit possa ocultar o ruído e o código adicional do VSSDK, ele ainda exige que você entenda as complexidades do threading no Visual Studio. E uma das primeiras lições que você aprende no threading do VS é que nem tudo pode ser executado de um thread em segundo plano. Em outras palavras, nem tudo é seguro para threads, especialmente as chamadas que entram em componentes COM. Portanto, no exemplo acima, você vê que há uma chamada para alternar para o thread principal (interface do usuário):
await package.JoinableTaskFactory.SwitchToMainThreadAsync();
ErrorHandler.ThrowOnFailure(nativeView.GetWindowFrame(out object frameValue));
É claro que você pode voltar para um thread em segundo plano após essa chamada. No entanto, como um extensor usando o Community Toolkit, você precisará prestar muita atenção em qual thread seu código está e determinar se ele tem o risco de congelar a interface do usuário. O threading no Visual Studio é difícil de acertar e requer o uso adequado de JoinableTaskFactory
para evitar deadlocks. A luta para escrever código que lide com threading corretamente tem sido uma fonte constante de bugs, mesmo para nossos engenheiros internos do Visual Studio. O VisualStudio.Extensibility, por outro lado, evita esse problema executando extensões fora do processo e contando com APIs assíncronas de ponta a ponta.
API simples versus conceitos simples
Como o Community Toolkit oculta muitas das complexidades do VSSDK, ele pode dar aos extensores uma falsa sensação de simplicidade. Vamos continuar com o mesmo código de exemplo. Se um extensor não souber sobre os requisitos de threading do desenvolvimento do Visual Studio, ele poderá presumir que seu código é executado em um thread em segundo plano o tempo todo. Eles não terão problemas com o fato de que a chamada para ler um arquivo do texto é síncrona. Se estiver em um thread em segundo plano, ele não congelará a interface do usuário se o arquivo em questão for grande. No entanto, quando o código for desencapsulado no VSSDK, eles perceberão que esse não é o caso. Portanto, embora a API do Community Toolkit certamente pareça mais simples de entender e mais coesa de escrever, porque está vinculada ao VSSDK, ela está sujeita às limitações do VSSDK. As simplicidades podem encobrir conceitos importantes que, se os extensores não entenderem, podem causar mais danos. VisualStudio.Extensibility evita os muitos problemas causados por dependências de thread principal, concentrando-se no modelo fora do processo e nas APIs assíncronas como nossa base. Embora a falta do processo simplifique mais o threading, muitos desses benefícios também são transferidos para extensões que são executadas no processo. Por exemplo, os comandos VisualStudio.Extensibility são sempre executados em um thread em segundo plano. A interação com APIs VSSDK ainda requer conhecimento profundo de como o threading funciona, mas pelo menos você não pagará o custo de travamentos acidentais, como neste exemplo.
Gráfico de comparação
Para resumir o que abordamos em detalhes na seção anterior, a tabela a seguir mostra uma comparação rápida:
VSSDK | Kit de ferramentas do Community | VisualStudio.Extensibility | |
---|---|---|---|
Suporte de Runtime | .NET Framework | .NET Framework | .NET |
Isolamento do Visual Studio | ❌ | ❌ | ✅ |
API Simples. | ❌ | ✅ | ✅ |
Execução Assíncrona e API | ❌ | ❌ | ✅ |
Amplitude do Cenário do VS | ✅ | ✅ | ⏳ |
Instalável sem Reiniciar | ❌ | ❌ | ✅ |
Dá suporte ao VS 2019 e versões anteriores | ✅ | ✅ | ❌ |
Para ajudar você a aplicar a comparação às suas necessidades de extensibilidade do Visual Studio, aqui estão alguns cenários de exemplo e nossas recomendações sobre qual modelo usar:
- Sou novo no desenvolvimento de extensões do Visual Studio e quero a experiência de integração mais fácil para criar uma extensão de alta qualidade e eu só preciso dar suporte ao Visual Studio 2022 ou versões superiores.
- Nesse caso, recomendamos que você use VisualStudio.Extensibility.
- Gostaria de escrever uma extensão direcionada ao Visual Studio 2022 e versões superiores. No entanto, VisualStudio.Extensibility não dá suporte às funcionalidades de que eu preciso.
- Recomendamos que, nesse caso, você adote um método híbrido de combinar VisualStudio.Extensibility e VSSDK. Você pode criar uma extensão VisualStudio.Extensibility que seja executada em processo, o que permite acessar APIs do VSSDK ou do Community Toolkit.
- Tenho uma extensão existente e quero atualizá-la para oferecer suporte a versões mais recentes. Quero que minha extensão dê suporte ao maior número possível de versões do Visual Studio.
- Como o VisualStudio.Extensibility só dá suporte ao Visual Studio 2022 e versões superiores, o VSSDK ou o Community Toolkit é a melhor opção para esse caso.
- Tenho uma extensão existente que gostaria de migrar para VisualStudio.Extensibility para aproveitar o .NET e instalar sem reiniciar.
- Esse cenário é um pouco mais sutil, pois VisualStudio.Extensibility não dá suporte a versões de nível inferior do Visual Studio.
- Se sua extensão existente der suporte apenas ao Visual Studio 2022 e tiver todas as APIs necessárias, recomendamos que você reescreva sua extensão para usar VisualStudio.Extensibility. Mas se sua extensão precisar de APIs que VisualStudio.Extensibility ainda não tem, vá em frente e crie uma extensão VisualStudio.Extensibility que seja executada em processo para que você possa acessar APIs do VSSDK. Com o tempo, você pode eliminar o uso da API do VSSDK à medida que o VisualStudio.Extensibility adiciona suporte e move suas extensões para ficarem sem processo.
- Se sua extensão precisar dar suporte a versões de nível inferior do Visual Studio que não têm suporte para VisualStudio.Extensibility, recomendamos que você faça alguma refatoração em sua base de código. Efetue pull de todo o código comum que pode ser compartilhado entre versões do Visual Studio para sua própria biblioteca e crie projetos VSIX separados direcionados a diferentes modelos de extensibilidade. Por exemplo, se sua extensão precisar dar suporte ao Visual Studio 2019 e ao Visual Studio 2022, você poderá adotar a seguinte estrutura de projeto em sua solução:
- MyExtension-VS2019 (este é o projeto de contêiner VSIX baseado em VSSDK direcionado ao Visual Studio 2019)
- MyExtension-VS2022 (este é o projeto de contêiner VSIX+VisualStudio.Extensibility baseado em VSSDK direcionado ao Visual Studio 2022)
- VSSDK-CommonCode (essa é a biblioteca comum usada para chamar APIs do Visual Studio por meio do VSSDK. Ambos os projetos VSIX podem fazer referência a essa biblioteca para compartilhar código).
- MyExtension-BusinessLogic (essa é a biblioteca comum que contém todo o código pertinente à lógica de negócios da sua extensão. Ambos os projetos VSIX podem fazer referência a essa biblioteca para compartilhar código).
- Esse cenário é um pouco mais sutil, pois VisualStudio.Extensibility não dá suporte a versões de nível inferior do Visual Studio.
Próximas etapas
Nossa recomendação é que os extensores comecem com VisualStudio.Extensibility ao criar novas extensões ou aprimorar as existentes e usem o VSSDK ou o Community Toolkit se você se deparar com cenários sem suporte. Para começar a usar o VisualStudio.Extensibility, navegue pela documentação apresentada nesta seção. Você também pode fazer referência ao repositório VSExtensibility do GitHub para exemplos ou problemas do arquivo.