Escolha o modelo de extensibilidade correto do Visual Studio para você
Você pode estender o Visual Studio usando três modelos de extensibilidade principais, VSSDK, Community Toolkit e VisualStudio.Extensibility. Este artigo aborda os prós e contras de cada um. Usamos um exemplo simples para realçar as diferenças de arquitetura e código entre os modelos.
VSSDK
do VSSDK (ou SDK do Visual Studio) é o modelo no qual a maioria das extensões no do Visual Studio Marketplace se baseia. Esse modelo é no qual o Visual Studio em si é criado. É 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 desligar o Visual Studio e prejudicar a experiência do cliente. E como as extensões são executadas no mesmo processo que o Visual Studio, elas só podem ser criadas usando .NET Framework. Os extensores que desejam usar ou incorporar bibliotecas que usam .NET 5 e posteriores 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 COMde uma marca herdada, passando pela simplicidade enganosa do DTE e mexendo com importações e exportações de MEF. Vamos usar um exemplo de criação de uma extensão que lê o texto do sistema de arquivos e o insere no início do documento ativo atual no editor. O snippet 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 de 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 pegue 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 controles do extensor, as extensões do VSSDK são criadas ao copiar e colar em 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 alcançar as metas de extensão (embora, às vezes, elas sejam a única opção).
Kit de ferramentas da comunidade
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 se baseia no VSSDK, ele está sujeito às mesmas limitações que o VSSDK (ou seja, somente .NET Framework, nenhum isolamento do restante do Visual Studio e assim por diante). Continuando com o mesmo exemplo de gravação de uma extensão que insere o texto lido do sistema de arquivos, usando o Community Toolkit, a extensão seria gravada da seguinte maneira 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 é muito aprimorado do VSSDK em termos de simplicidade e intuitividade! Não só reduzimos significativamente o número de linhas, mas o código resultante também parece razoável. Não é necessário entender qual é a diferença entre SVsTextManager
e IVsTextManager
. As APIs estão mais alinhadas com o .NET, adotando nomenclaturas comuns e padrões assíncronos, além de priorizar operações comuns. No entanto, o Kit de Ferramentas da Comunidade ainda é baseado no modelo existente do VSSDK e, portanto, os vestígios da estrutura subjacente ainda são perceptíveis. Por exemplo, um arquivo .vsct
ainda é necessário. Embora o Kit de Ferramentas da Comunidade faça um ótimo trabalho simplificando as APIs, ele está associado às limitações do VSSDK e não tem uma maneira de tornar a configuração de extensão mais simples.
VisualStudio.Extensibility
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 de arquitetura fundamental, novos padrões e funcionalidades 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, isole 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 é baseado em uma nova arquitetura subjacente, ele ainda não tem a amplitude que o VSSDK e o Kit de Ferramentas da Comunidade têm. Para superar essa lacuna, você pode executar suas extensões VisualStudio.Extensibility no processo, o que permite que você continue usando as APIs VSSDK. No entanto, isso significa que sua extensão só pode ser direcionada ao .NET Framework, pois ela compartilha o mesmo processo que o Visual Studio, que se baseia no .NET Framework.
Continuando com o mesmo exemplo de desenvolvimento de uma extensão que insere o texto de um arquivo, usando VisualStudio.Extensibility, a extensão seria escrita da seguinte maneira para tratamento de comando:
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, isso é feito por meio do código:
public override CommandConfiguration CommandConfiguration => new("%VisualStudio.Extensibility.Command1.DisplayName%")
{
Icon = new(ImageMoniker.KnownValues.Extension, IconSettings.IconAndText),
Placements = [CommandPlacement.KnownPlacements.ExtensionsMenu],
};
Esse código é mais fácil de entender e seguir. Na maioria das vezes, você pode escrever essa extensão puramente por meio do editor com a ajuda do IntelliSense, mesmo para a 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 Kit de Ferramentas da Comunidade é uma excelente camada de facilidade de uso sobre a criação de extensões com o VSSDK. No entanto, há 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 rapidamente cancelar quebra de linha do código neste exemplo e ver o que realmente está sendo chamado no contexto do VSSDK. Vamos nos concentrar apenas no trecho de código de execução de comando, pois há vários detalhes que o VSSDK precisa, que o Community Toolkit oculta bem. Mas quando olharmos para 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 falhas. O snippet de código a seguir mostra o código do Kit de Ferramentas da Comunidade desenvolvido 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á poucas questões para abordar aqui, e todas elas giram em torno de tarefas paralelas (threading) e código assíncrono. Vamos passar por cada um em detalhes.
API assíncrona versus execução de código assíncrono
A primeira coisa a observar é que o método ExecuteAsync
no Kit de Ferramentas da Comunidade é uma chamada assíncrona de fire-and-forget encapsulada no VSSDK:
package.JoinableTaskFactory.RunAsync(async delegate
{
…
});
O VSSDK em si não dá suporte à execução de comando assíncrono de uma perspectiva de 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 até que ele seja concluído e retornar o usuário ao contexto de chamada original com resultados de execução. Portanto, mesmo que a API ExecuteAsync no Community Toolkit seja sintaticamente assíncrona, não é uma execução assíncrona verdadeira. E como é uma forma de fire-and-forget para execução assíncrona, você pode chamar ExecuteAsync várias vezes sem precisar esperar que a chamada anterior seja concluída. Embora o Kit de Ferramentas da Comunidade 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 subjacente do VSSDK não é assíncrona, e os métodos auxiliares de fire-and-forget fornecidos pelo Kit de Ferramentas da Comunidade não conseguem lidar adequadamente com o controle de rendimentos assíncronos e o gerenciamento do estado do cliente. Isso pode ocultar alguns possíveis problemas que são difíceis de depurar.
Thread de interface do usuário versus thread em segundo plano
Outra consequência dessa chamada assíncrona do Kit de Ferramentas da Comunidade é que o próprio código ainda é executado a partir do thread da interface do usuário, e cabe ao desenvolvedor da extensão descobrir como fazer a transição corretamente para um thread em segundo plano, se você não quiser arriscar o congelamento da interface do usuário. Por mais que o Kit de Ferramentas da Comunidade possa ocultar o ruído e o código extra 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 em um thread em segundo plano. Em outras palavras, nem tudo é thread-safe, especialmente as chamadas que entram em componentes COM. Portanto, no exemplo acima, você vê que há uma chamada para alternar para a linha de execução principal (interface do usuário).
await package.JoinableTaskFactory.SwitchToMainThreadAsync();
ErrorHandler.ThrowOnFailure(nativeView.GetWindowFrame(out object frameValue));
É claro que você pode alternar novamente para um thread em segundo plano após essa chamada. No entanto, como um controle do extensor que usa o Community Toolkit, você precisará prestar muita atenção ao thread em que está o código e determinar se existe o risco de congelar a interface do usuário. A utilização de threads no Visual Studio é difícil de acertar e requer o uso adequado de JoinableTaskFactory
para evitar deadlocks. A luta para escrever código que lida 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 completamente executando extensões fora do processo e contando com APIs assíncronas de ponta a ponta.
API simples versus conceitos simples
Como o Kit de Ferramentas da Comunidade oculta muitos dos meandros 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á assumir 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 de 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 analisado 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 para gravar, pois está vinculada ao VSSDK, ela está sujeita a limitações do VSSDK. As simplificações podem mascarar conceitos importantes que, se aqueles encarregados de expandir conceitos não entenderem, podem causar mais danos. O VisualStudio.Extensibility evita muitos problemas causados por dependências do thread principal, concentrando-se no modelo fora de processo e nas APIs assíncronas como base. Embora executar fora do processo simplifique mais o threading, muitos desses benefícios também se aplicam a extensões que são executadas durante o processo. Por exemplo, os comandos VisualStudio.Extensibility são sempre executados em um thread em segundo plano. Interagir com as APIs do VSSDK ainda requer um conhecimento aprofundado de como funciona o threading, mas pelo menos você não arcará com o custo de travamentos acidentais, como neste exemplo.
Gráfico de comparação
Para resumir o que abordamos detalhadamente na seção anterior, a tabela a seguir mostra uma comparação rápida:
VSSDK | Kit de Ferramentas da Comunidade | 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 | ❌ | ❌ | ✅ |
Compatível comVS 2019 eanterior | ✅ | ✅ | ❌ |
Para ajudá-lo 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 que a experiência de integração inicial seja a mais fácil possível para criar uma extensão de alta qualidade, e sóprecisodar suporte ao Visual Studio 2022 ou superior.
- Nesse caso, recomendamos que você use VisualStudio.Extensibility.
- gostaria de escrever uma extensão direcionada ao Visual Studio 2022 e superior. No entanto,VisualStudio.Extensibility não dá suporte a todas as funcionalidades denecessárias.
- Recomendamos que, nesse caso, você adote um método híbrido de combinação de VisualStudio.Extensibility e VSSDK. Você pode criar uma extensão VisualStudio.Extensibility que é executada durante o processo, o que permite acessar as APIs do VSSDK ou do Kit de Ferramentas da Comunidade.
- tenho uma extensão existente e quero atualizá-la para dar 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 dá suporte apenas ao Visual Studio 2022 e superior, o VSSDK ou o Community Toolkit é a melhor opção para esse caso.
- tenho uma extensão existente que gostaria de migrar paraVisualStudio.Extensibility para aproveitar o .NET e instalar sem reiniciar.
- Esse cenário é um pouco mais nuançado, pois o VisualStudio.Extensibility não dá suporte a versões de nível inferior do Visual Studio.
- Se sua extensão existente dá suporte apenas ao Visual Studio 2022 e tem 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 o VisualStudio.Extensibility ainda não tem, vá em frente e crie uma extensão VisualStudio.Extensibility que é 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 mover suas extensões para serem executadas fora do 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 algumas refatorações 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 baseado em VSSDK+VisualStudio.Extensibility que tem como destino o 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 referenciar essa biblioteca para compartilhar código.)
- MyExtension-BusinessLogic (esta é 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 referenciar essa biblioteca para compartilhar código.)
- Esse cenário é um pouco mais nuançado, pois o 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 Kit de Ferramentas da Comunidade se você encontrar cenários sem suporte. Para começar, com VisualStudio.Extensibility, navegue pela documentação apresentada nesta seção. Você também pode referenciar o repositório do GitHub VSExtensibility para exemplos ou registrar problemas .