Поделиться через


Выберите подходящую модель расширяемости Visual Studio.

Вы можете расширить Visual Studio с помощью трех основных моделей расширяемости, VSSDK, Community Toolkit и VisualStudio.Extensibility. В этой статье рассматриваются плюсы и минусы каждого. Мы используем простой пример для выделения отличий архитектуры и кода между моделями.

VSSDK

VSSDK (или SDK для Visual Studio) — это модель, на основе которой построены большинство расширений Visual Studio Marketplace. Эта модель — это то, на что основана сама Visual Studio. Это самый полный и самый мощный, но и самый сложный в освоении и правильном использовании. Расширения, использующие VSSDK, выполняются в том же процессе, что и сама Visual Studio. Загрузка в том же процессе, что и Visual Studio, означает, что расширение с нарушением доступа, бесконечным циклом или другими проблемами может завершиться сбоем или зависнуть в Visual Studio и снизить взаимодействие с клиентом. И поскольку расширения выполняются в том же процессе, что и Visual Studio, их можно создавать только с помощью .NET Framework. Расширения, которые хотят использовать или включать библиотеки, использующие .NET 5 и более поздних версий, не могут сделать это с помощью VSSDK.

API в VSSDK были агрегированы на протяжении многих лет, так как Visual Studio сама преобразовывалась и развивалась. В одном расширении вы можете столкнуться с COMAPI на основе устаревшего авторитета, пролегать через обманчивую простоту DTE, а также экспериментировать с импортом и экспортом MEF. Давайте рассмотрим пример написания расширения, которое считывает текст из файловой системы и вставляет его в начало текущего активного документа в редакторе. В следующем фрагменте кода показан код, который будет выполняться при вызове команды в расширении на основе 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);
        }
    }
}

Кроме того, вам также потребуется предоставить файл .vsct, который определяет конфигурацию команды, например расположение в пользовательском интерфейсе, текст, связанный с ним и т. д.:

<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>

Как видно в примере, код может показаться неинтуитивным и вряд ли кто-то, знакомый с .NET, легко его поймёт. Существует множество концепций для изучения и шаблонов API для доступа к активному тексту редактора являются устаревшими. Для большинства расширителей расширения VSSDK создаются путем копирования и вставки из онлайн-источников, что может привести к сложным сеансам отладки, методу проб и ошибок и разочарованию. Во многих случаях расширения VSSDK могут быть не самым простым способом достижения целей расширения (хотя иногда они единственный выбор).

Набор средств сообщества

Community Toolkit — это модель расширения на основе сообщества с открытым кодом для Visual Studio, которая упаковывает VSSDK для упрощения разработки. Так как он основан на VSSDK, он подвергается тем же ограничениям, что и VSSDK (то есть только .NET Framework, без изоляции от остальной части Visual Studio и т. д.). Продолжая с тем же примером написания расширения, которое вставляет текст из файловой системы с помощью Community Toolkit, расширение будет записано следующим образом для обработчика команд:

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);
}

Полученный код значительно улучшен из VSSDK с точки зрения простоты и интуитивности! Кроме того, мы значительно сократили количество строк, но результирующий код также выглядит разумно. Нет необходимости понимать, в чем разница между SVsTextManager и IVsTextManager. Интерфейсы API стали более удобны для .NET, включая общие шаблоны именования и асинхронности, и придавая приоритеты общим операциям. Однако Набор средств сообщества по-прежнему основан на существующей модели VSSDK, и поэтому следы базовой структуры просачиваются. Например, файл .vsct по-прежнему необходим. Хотя Набор средств сообщества делает большую работу по упрощению API, он привязан к ограничениям VSSDK и не имеет способа упростить конфигурацию расширения.

VisualStudio.Extensibility

VisualStudio.Extensibility — это новая модель расширяемости, в которой расширения выполняются за пределами основного процесса Visual Studio. Из-за этого фундаментального изменения архитектуры новые шаблоны и возможности теперь доступны для расширений, которые недоступны в VSSDK или Community Toolkit. VisualStudio.Extensibility предлагает совершенно новый набор API, которые согласованы и просты в использовании, позволяют расширениям нацеливаться на .NET, изолируют ошибки, вызванные расширениями, от остальной части Visual Studio и дают возможность пользователям устанавливать расширения без перезапуска Visual Studio. Тем не менее, поскольку новая модель построена на новой базовой архитектуре, она еще не имеет ширины VSSDK и набора средств сообщества. Чтобы устранить этот разрыв, можно запустить расширения VisualStudio.Extensibility в процессе, что позволяет продолжать использовать API VSSDK. Однако это означает, что расширение может использовать только платформу .NET Framework, так как она использует тот же процесс, что и Visual Studio, основанный на .NET Framework.

Продолжая с тем же примером написания расширения, которое вставляет текст из файла с помощью VisualStudio.Extensibility, расширение будет записано следующим образом для обработки команд:

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);
                
    }
}

Чтобы настроить команду для размещения, текста и т. д., вам больше не нужно предоставить файл .vsct. Вместо этого он выполняется с помощью кода:

public override CommandConfiguration CommandConfiguration => new("%VisualStudio.Extensibility.Command1.DisplayName%")
{
    Icon = new(ImageMoniker.KnownValues.Extension, IconSettings.IconAndText),
    Placements = [CommandPlacement.KnownPlacements.ExtensionsMenu],
};

Этот код проще понять и следовать. В большинстве случаев вы можете написать это расширение исключительно через редактор с помощью IntelliSense, даже для настройки команд.

Сравнение различных моделей расширяемости Visual Studio

В примере можно заметить, что при использовании VisualStudio.Extensibility в обработчике команд больше строк кода, чем в Community Toolkit. Community Toolkit — это отличная оболочка, упрощающая использование для создания расширений с помощью VSSDK. Однако существуют ловушки, которые не сразу заметны, и это привело к появлению VisualStudio.Extensibility. Чтобы понять переход и необходимость, особенно если кажется, что набор средств сообщества также приводит к коду, который легко писать и понимать, давайте рассмотрим пример и сравните то, что происходит в более глубоких слоях кода.

Мы можем быстро распаковывать код в этом примере и видеть, что на самом деле вызывается на стороне VSSDK. Мы собираемся сосредоточиться исключительно на фрагменте выполнения команд, поскольку существует множество сведений, необходимых для VSSDK, которые Community Toolkit удобно скрывает. Но как только мы рассмотрим базовый код, вы поймете, почему простота здесь компромисс. Простота скрывает некоторые базовые сведения, что может привести к неожиданному поведению, ошибкам и даже проблемам с производительностью и сбоям. В следующем фрагменте кода показан развернутый код Community Toolkit, чтобы продемонстрировать вызовы 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);    
            }
        }
    });
}

Здесь есть несколько проблем, и все они связаны с потоками и асинхронным кодом. Мы подробно рассмотрим каждый из них.

Асинхронный API и асинхронное выполнение кода

Первое, что следует отметить, это то, что метод ExecuteAsync в Community Toolkit — это асинхронный вызов "fire-and-forget" в VSSDK:

package.JoinableTaskFactory.RunAsync(async delegate
{
  …
});

САМ VSSDK не поддерживает асинхронное выполнение команд с точки зрения основного API. То есть при выполнении команды VSSDK не имеет способа выполнить код обработчика команд в фоновом потоке, дождитесь завершения и верните пользователя в исходный контекст вызова с результатами выполнения. Таким образом, хотя API ExecuteAsync в Community Toolkit синтаксически асинхронно, это не настоящее асинхронное выполнение. И поскольку это подход типа "огонь и забыть" для асинхронного выполнения, вы можете вызывать ExecuteAsync снова и снова, не ожидая завершения предыдущего вызова. Хотя Community Toolkit обеспечивает лучший пользовательский опыт в плане того, как помочь разработчикам реализовать распространенные сценарии, в конечном счете он не может решить основные проблемы с VSSDK. В этом случае базовый API VSSDK не является асинхронным, а вспомогательные методы fire-and-forget, предоставляемые Community Toolkit, не могут правильно обрабатывать асинхронные операции и работу с состоянием клиента; это может скрыть некоторые потенциальные проблемы, которые трудно отлаживать.

Поток пользовательского интерфейса против фонового потока

Другие последствия, связанные с этим обернутым асинхронным вызовом из Community Toolkit, заключаются в том, что сам код по-прежнему выполняется в потоке пользовательского интерфейса, и разработчик расширения должен выяснить, как правильно переключиться на фоновый поток, если он не хочет рисковать зависанием пользовательского интерфейса. Хотя Community Toolkit может скрыть шум и дополнительный код VSSDK, он всё же требует от вас понимания сложностей работы с потоками в Visual Studio. И один из первых уроков, которые вы усваиваете при работе с потоками в Visual Studio, заключается в том, что не все может быть запущено из фонового потока. Другими словами, не всё является потокобезопасно, особенно вызовы, которые попадают в COM-компоненты. Таким образом, в приведенном выше примере вы видите, что есть вызов переключения на основной (пользовательский интерфейс) поток:

await package.JoinableTaskFactory.SwitchToMainThreadAsync();
ErrorHandler.ThrowOnFailure(nativeView.GetWindowFrame(out object frameValue));

Конечно, после этого вызова вы можете снова переключиться на фоновый поток. Тем не менее, как разработчик, использующий набор инструментов сообщества, вам необходимо тщательно следить за тем, на каком потоке выполняется ваш код, и определить, существует ли риск блокировки пользовательского интерфейса. Работа с потоками в Visual Studio сложна и требует правильного использования JoinableTaskFactory, чтобы избежать дедлоков. Старания написать код, который правильно обрабатывает многопоточность, являются постоянным источником ошибок, даже для наших инженеров Visual Studio. С другой стороны, VisualStudio.Extensibility полностью избегает этой проблемы, выполняя расширения вне основного процесса и полагаясь на асинхронные API на всех этапах.

Простой API и простые понятия

Поскольку Набор средств сообщества скрывает многие из сложностей VSSDK, он может дать разработчикам расширений ложное впечатление простоты. Давайте продолжим использовать тот же пример кода. Если расширитель не знал о требованиях к потоковой работе в Visual Studio, он может предположить, что их код выполняется из фонового потока все время. Они не будут против того, что считывание файла из текста синхронно. Если он находится в фоновом потоке, это не задержит интерфейс пользователя, если рассматриваемый файл большой. Тем не менее, когда код распаковывается в VSSDK, они понимают, что это не так. Таким образом, хотя API из Community Toolkit, безусловно, выглядит более простым для понимания и более удобным для написания, из-за его привязки к VSSDK, он подвержен ограничениям VSSDK. Упрощения могут скрыть важные понятия, которые, если их не понимают ответственные лица, могут причинить серьезный вред. VisualStudio.Extensibility избегает многих проблем, вызванных зависимостями от основного потока, фокусируясь на модели внепроцессного выполнения и асинхронных API в качестве основы. Если выйти за пределы процесса, это упростит работу с потоками больше всего, однако многие из этих преимуществ переносятся также на расширения, которые выполняются внутри процесса. Например, команды VisualStudio.Extensibility всегда выполняются в фоновом потоке. Взаимодействие с API VSSDK по-прежнему требует глубоких знаний работы с потоками, но, по крайней мере, вы избежите случайных зависаний, как это показано в данном примере.

Диаграмма сравнения

Подводя итог тому, что мы подробно рассмотрели в предыдущем разделе, следующая таблица показывает краткое сравнение:

VSSDK Набор средств сообщества VisualStudio.Extensibility
поддержка среды выполнения Платформа .NET Framework Платформа .NET Framework .СЕТЬ
Изоляция от Visual Studio
простой API
Асинхронное выполнение и API
Широта сценария VS
Устанавливаемое без перезагрузки
поддерживаетVS 2019 иниже

Чтобы помочь вам применить сравнение с потребностями расширения Visual Studio, ниже приведены некоторые примеры сценариев и наши рекомендации по использованию модели:

  • я не знаком с разработкой расширений Visual Studio, и я хочу, чтобы самый простой вводный опыт для создания высококачественного расширения, и мне тольконужнaподдержка Visual Studio 2022 или более поздних версий.
    • В этом случае рекомендуется использовать VisualStudio.Extensibility.
  • я хотел бы написать расширение, которое предназначено для Visual Studio 2022 и выше. ОднакоVisualStudio.Extensibility не поддерживает все необходимые функции.
    • В этом случае рекомендуется использовать гибридный метод объединения VisualStudio.Extensibility и VSSDK. Вы можете создать расширение VisualStudio.Extensibility, которое выполняется в процессе, что позволяет получить доступ к API VSSDK или Community Toolkit.
  • у меня есть существующее расширение и хотите обновить его для поддержки новых версий. Я хочу, чтобы расширение поддерживало как можно больше версий Visual Studio.
    • Так как VisualStudio.Extensibility поддерживает только Visual Studio 2022 и выше, VSSDK или Community Toolkit является лучшим вариантом для этого случая.
  • у меня есть существующее расширение, которое я хотел бы перенести наVisualStudio.Extensibility, чтобы воспользоваться преимуществами .NET и установить без перезапуска.
    • Этот сценарий немного более нюансен, так как VisualStudio.Extensibility не поддерживает версии Visual Studio нижнего уровня.
      • Если существующее расширение поддерживает только Visual Studio 2022 и имеет все необходимые API, рекомендуется переписать расширение для использования VisualStudio.Extensibility. Но если вашему расширению требуются API, которых VisualStudio.Extensibility еще не поддерживает, то создайте расширение VisualStudio.Extensibility, которое выполняется в процессе, чтобы получить доступ к API VSSDK. Со временем вы можете отказаться от использования API VSSDK, так как VisualStudio.Extensibility добавляет поддержку, и переместить ваши расширения для работы вне процесса.
      • Если расширение должно поддерживать версии Visual Studio нижнего уровня, у которых нет поддержки VisualStudio.Extensibility, рекомендуется выполнить рефакторинг в базе кода. Извлеките весь общий код, который можно совместно использовать в версиях Visual Studio в собственную библиотеку, и создайте отдельные проекты VSIX, предназначенные для различных моделей расширяемости. Например, если расширение должно поддерживать Visual Studio 2019 и Visual Studio 2022, в решении можно применить следующую структуру проекта:
        • MyExtension-VS2019 (это проект контейнера VSSDK на основе VSIX, предназначенный для Visual Studio 2019)
        • MyExtension-VS2022 (это проект контейнера VSSDK+VisualStudio.Extensibility на основе VSIX, предназначенный для Visual Studio 2022)
        • VSSDK-CommonCode (это общая библиотека, которая используется для вызова API Visual Studio через VSSDK. Оба проекта VSIX могут ссылаться на эту библиотеку для совместного использования кода.)
        • MyExtension-BusinessLogic (это общая библиотека, содержащая весь код, соответствующий бизнес-логике расширения. Оба проекта VSIX могут ссылаться на эту библиотеку для совместного использования кода.)

Дальнейшие действия

Наша рекомендация заключается в том, что расширения начинаются с VisualStudio.Extensibility при создании новых расширений или улучшении существующих, а также используйте VSSDK или Набор средств сообщества, если вы работаете в неподдерживаемых сценариях. Чтобы приступить к работе с VisualStudio.Extensibility, ознакомьтесь с документацией, представленной в этом разделе. Вы также можете ссылаться на репозиторий VSExtensibility GitHub за примерами или чтобы сообщить о проблемах .