尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Visual Studio 扩展开发:VSIX 命令、工具窗口与 Roslyn

Visual Studio 扩展开发:VSIX 命令、工具窗口与 Roslyn 做 Visual Studio 扩展开发这几年被问得最多的一句话是我能不能给 Visual Studio 加一个自己的按钮答案是当然可以而且门槛比大多数人想象的低——只要你会写 C#会一点点 WPF剩下的东西 VS SDK 基本都替你铺好了路。扩展开发的本质是把自己的工具、规范、模板、检查规则塞进这个每天都在用的 IDE 里让它替你干那些重复了八百遍的机械活。这篇文章聊的是Visual Studio 扩展开发也就是常说的VS 插件开发从环境准备、第一个命令、工具窗口、编辑器装饰一直到 Roslyn 分析器、打包分发和版本兼容全部按我自己踩过的路径讲一遍。适合三类人看一是手上有重复性工作想自动化、又不想离开 VS 的老手二是想给团队做内部规范检查工具的技术负责人三是完全没碰过 VSIX、但写过 WPF 想试试水的开发者。VS Code 的插件市场很热闹但原生 Visual Studio 的扩展体系是另一套完全不同的东西搞混这两者前两周基本就白干了。1. 先搞清楚 VS 扩展和 VS Code 插件差在哪1.1 两套插件体系的底层差异很多人看到 VSIX 这个后缀就默认两边的插件是通用的这是个非常典型的误解。VSIX 只是一个打包容器格式里面的东西完全是两码事。VS Code 的扩展跑在 Electron 的扩展宿主进程里用 TypeScript/JavaScript 写通过vscode这个 npm 模块暴露的 API 跟编辑器对话能力边界相对收敛而 Visual Studio 的扩展是直接跑在devenv.exe进程内的 .NET 程序集通过 VS SDK 拿到的是编辑器内核级别的能力——你可以拿到ITextView、可以操作 Roslyn 语法树、可以往解决方案资源管理器里挂节点、可以接管调试器的评估逻辑。这个差异带来两个直接后果。第一能力上限完全不同。VS Code 插件能做的大多是外围增强想在编辑器里做像素级的文本装饰、想深度介入编译器服务通常很难或者压根做不到而 VS 扩展是可以直接插入编辑器管线Editor Pipeline的装饰层、标签层、智能提示层都能自己实现。第二稳定性和风险完全不同。你的代码跑在 IDE 进程里一个未捕获的异常、一次 UI 线程死锁可能直接把整个 Visual Studio 干掉用户会看到Microsoft Visual Studio 已停止工作。所以选型的时候先问自己一个问题这个需求是让编辑器更好用还是让 IDE 帮我干活如果只是语法高亮、片段补全、快捷键映射先看看 VS 自带的能不能满足或者用 Roslyn 分析器这种轻量方案如果是要加工具窗口、要跟解决方案/项目系统交互、要嵌自己的图形界面那才是 VS 扩展的主场。我见过太多人一上来就写 VSIX结果发现其实一个.editorconfig加两行规则就解决了。1.2 哪些需求值得做成原生 VS 扩展我的判断标准很简单每天都要做、每次都要手工做、做完还容易漏。这三条同时成立才值得投入几天时间写扩展。举几个真实的场景。第一个是代码规范巡检团队里定了一堆约定比如所有async方法必须以Async结尾、禁止直接new HttpClient()、日志必须带 TraceId。这些用 Roslyn 分析器做成波浪线 一键修复编译前就能拦住比 Code Review 时靠人肉靠谱得多。第二个是脚手架生成新建一个业务模块要建 Controller、Service、DTO、Mapper、单测五六个文件模板还带一堆占位符替换手工做一次二十分钟做成右键菜单里一个生成模块骨架三秒搞定。第三个是内部工具集成把公司的流水线状态、接口文档、数据库表结构拉进一个工具窗口随时查不用切浏览器。反过来这些事情我不建议做成扩展只给某一个人用的临时脚本写个控制台程序更省事、需要频繁改动的业务逻辑扩展的调试和发布链路比普通程序长、以及任何需要联网下载大量依赖才能工作的功能——扩展的加载时机很敏感启动时做重活会拖慢整个 IDE 的启动速度用户会骂你。1.3 扩展能做成什么形态刚开始接触的时候先把扩展能长成什么样这件事在脑子里过一遍后面写代码会顺畅很多。形态技术载体典型用途上手难度菜单命令MenuCommand .vsct触发一次性动作如格式化、生成文件低工具窗口ToolWindowPane WPF常驻面板如接口列表、日志查看器中编辑器装饰IWpfTextViewCreationListener Adornment行内提示、高亮、内联按钮中高代码分析Roslyn DiagnosticAnalyzer规范检查、错误提示、一键修复中项目/解决方案扩展IVsHierarchy 项目系统自定义项目类型、节点右键菜单高选项页DialogPage扩展自身的配置界面低编辑器语言服务基于 LSP 或旧版 Language Service自定义语言支持很高新手路径我建议是先做菜单命令跑通整条链路再做选项页理解配置持久化然后是工具窗口最后再去碰 Roslyn 和编辑器装饰。跳步的代价是遇到问题时你分不清是 SDK 的问题还是自己姿势不对——这在我身上发生过不止一次。2. 开发环境与工具链准备2.1 装 Visual Studio 时到底勾哪些工作负载这一步听着像废话但至少有三分之一的新手卡在这里。Visual Studio 的安装器默认勾选的工作负载是给普通应用开发准备的扩展开发需要额外的东西。打开 Visual Studio Installer找到当前安装的实例点修改然后工作负载里勾上Visual Studio 扩展开发Visual Studio extension development。这个负载会带来 VS SDK、VSIX 项目模板、实验实例管理工具是最关键的一项。如果你要写 Roslyn 分析器再勾上.NET 桌面开发它会带来完整的 .NET SDK 和 NuGet 工具链。单个组件标签页里搜一下Visual Studio SDK确认它被勾上。某些版本的安装器不会因为工作负载自动带上它。注意不要用 Build Tools 来做扩展开发。Build Tools 只包含编译器和 MSBuild没有 IDE、没有 SDK、没有实验实例你会连项目模板都找不到。网上很多人搜到 build tools for visual studio 2022 就装了然后在新建项目里死活搜不到 VSIX 模板问题就出在这。另外如果你机器上同时装着多个版本的 Visual Studio这本身没问题可以共存。但扩展调试时一定要看清楚 F5 启动的是哪一个实例——命令行参数里的/rootsuffix Exp只对当前工程的 SDK 版本生效版本错配会出现扩展装上了但没反应的情况。2.2 工程模板与目录结构长什么样环境装好之后新建项目里搜 VSIX能看到几个模板。我一般就选最朴素的VSIX Project空项目然后在项目上右键添加 → 新建项选Command命令模板。VS 会自动帮你生成一整套骨架包括包类、命令类、图标资源、.vsct文件和source.extension.vsixmanifest。这个骨架的结构值得看清楚因为后面所有工作都是在这上面长出来的MyFirstExtension/ ├── MyFirstExtension.csproj ├── source.extension.vsixmanifest // 扩展的身份证描述名称、版本、支持的 VS 版本 ├── Resources/ │ └── Package.ico ├── VSCommandTable.vsct // 菜单和命令的声明文件 ├── Command1.cs // 命令的注册与执行逻辑 ├── Command1Package.cs // 扩展的入口即 Package 类 └── Properties/ └── AssemblyInfo.cs.csproj里有一个GeneratePkgDefFiletrue/GeneratePkgDefFile和IncludeAssemblyInVSIXContainertrue/IncludeAssemblyInVSIXContainer这两个开关决定了你的程序集能不能被正确打进去、能不能被 IDE 认出来改动它们要格外小心。还有一个CopyBuildOutputToOutputDirectory调试时很有用。2.3 调试靠的是实验实例别搞坏自己的日常环境这是 VS 扩展开发和普通 .NET 开发最大的不同点F5 调试时你的扩展不会装进你日常用的 Visual Studio而是装进一个独立的实验实例Experimental Instance。实验实例是同一个devenv.exe但用不同的配置根目录启动简单说就是另一个平行宇宙里的 Visual Studio。它的配置文件、窗口布局、已装的扩展都跟你日常用的那个完全隔离。命令行参数就是/rootsuffix Exp这个参数由项目属性里的调试页自动加上你不用手写。第一次按 F5 的时候会有点慢因为实验实例要初始化自己的配置。之后每次调试就快了。但有个坑要提前说实验实例的配置会一直累积。你反复调试同一个扩展旧版本的注册信息可能残留表现出来就是代码明明改了运行起来还是老行为。解决办法是定期重置实验实例用 SDK 自带的CreateExpInstance.exeC:\Program Files\Microsoft Visual Studio\2022\Community\VSSDK\VisualStudioIntegration\Tools\Bin\CreateExpInstance.exe /Reset /VSInstance17.0 /RootSuffixExp/VSInstance后面的版本号要换成你自己装的版本。重置之后第一次 F5 又会慢一次属于正常现象。实操心得把上面这条命令存成一个.bat放在项目根目录命名成reset-exp.bat。遇到改了没生效的情况先跑一遍再说能省掉大量无意义的排查时间。3. 从零写第一个可用命令3.1 建项目、加 Command 模板先说结论第一步不要自己手写代码先把模板生成的代码读懂。新建 VSIX 项目后右键项目 → 添加 → 新建项 → 搜索 Command起个名字比如SayHelloCommand.csVS 会自动做四件事在VSCommandTable.vsct里加一段 Button 定义和 IDSymbol创建SayHelloCommand.cs命令类创建一个 Package 类如果还没有的话把图标资源复制进去。编译一次按 F5你会看到实验实例的工具菜单下多了一个 Invoke SayHelloCommand 的项点一下弹出一个消息框。这条链路跑通说明你的环境、SDK、调试机制全都是好的。3.2 生成代码逐行拆解先看 Package 类这是扩展的入口点[PackageRegistration(UseManagedResourcesOnly true, AllowsBackgroundLoading true)] [InstalledProductRegistration(#110, #112, 1.0, IconResourceID 400)] [ProvideMenuResource(Menus.ctmenu, 1)] [Guid(SayHelloCommandPackage.PackageGuidString)] public sealed class SayHelloCommandPackage : AsyncPackage { public const string PackageGuidString xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx; protected override async Task InitializeAsync( CancellationToken cancellationToken, IProgressServiceProgressData progress) { await this.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); await SayHelloCommand.InitializeAsync(this); } }几个特性逐个说清楚PackageRegistration告诉 VS 这是一个包。UseManagedResourcesOnly true表示资源都放在托管程序集里这是现代模板的默认值别改。AllowsBackgroundLoading true是允许包在后台线程加载这是性能关键项新写的包一定要加上。InstalledProductRegistration控制扩展管理器里显示的帮助信息和版本号三个字符串参数对应资源文件里的 ID#110是名称、#112是描述。ProvideMenuResource(Menus.ctmenu, 1)把编译后的.vsct资源挂上去菜单才能生效。第二个参数是资源版本号固定写 1。Guid包的唯一标识绝对不要跟别人重复。模板会生成一个你自己复制代码时千万别把 GUID 一起复制了这是新手最容易犯的致命错误。再看命令类private static async Task InitializeAsync(AsyncPackage package) { await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(package.DisposalToken); OleMenuCommandService commandService await package.GetServiceAsync(typeof(IMenuCommandService)) as OleMenuCommandService; var menuCommandID new CommandID(CommandSet, CommandId); var menuItem new MenuCommand(Execute, menuCommandID); commandService.AddCommand(menuItem); }CommandSet是命令组的 GUIDCommandId是数字 ID这两个值必须跟.vsct里声明的完全一致否则命令注册成功但菜单点了没反应。OleMenuCommandService是 VS 提供的命令分发服务拿到它之后用AddCommand把你的MenuCommand塞进去菜单点击就会回调到Execute方法。执行逻辑本身很简单private void Execute(object sender, EventArgs e) { ThreadHelper.ThrowIfNotOnUIThread(); string message string.Format(CultureInfo.CurrentCulture, 内部命令 {0} 被触发。, this.GetType().FullName); string title SayHelloCommand; VsShellUtilities.ShowMessageBox( this.package, message, title, OLEMSGICON.OLEMSGICON_INFO, OLEMSGBUTTON.OLEMSGBUTTON_OK, OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST); }ThreadHelper.ThrowIfNotOnUIThread()这行千万别删。它的作用是如果当前不在 UI 线程上直接抛异常告诉你。VS 的 UI 元素包括菜单命令、窗口、对话框都必须在 UI 线程上操作从后台线程碰它们会出各种奇怪的崩溃。这行代码相当于给自己买了个保险。3.3 .vsct 是菜单和命令的图纸.vsct文件Visual Studio Command Table是 VS 扩展设计里最复古的一块它是个 XML编译后生成.ctmenu二进制资源。刚开始看会有点晕但结构其实很清晰分三块Symbols定义 ID、Buttons定义按钮、GuidSymbol把 C# 里的 GUID 和 XML 里的名字对上。Commands packageguidSayHelloCommandPackage Buttons Button guidguidSayHelloCommandPackageCmdSet idSayHelloCommandId priority0x0100 typeButton Parent guidguidSHLMainMenu idIDG_VS_MM_TOOLSADDINS / Icon guidguidImages idbmpPic1 / Strings ButtonTextSay Hello/ButtonText /Strings /Button /Buttons /Commands Symbols GuidSymbol nameguidSayHelloCommandPackageCmdSet value{...你自己的GUID...} IDSymbol nameSayHelloCommandId value0x0100 / IDSymbol nameMyMenuGroup value0x1020 / /GuidSymbol /SymbolsParent决定了命令挂在哪个位置。模板默认挂在工具菜单下如果你想让命令出现在解决方案资源管理器的右键菜单里得把 Parent 换成对应的 GUID 和 ID比如IDM_VS_CTXT_ITEMNODE是文件节点右键菜单、IDM_VS_CTXT_PROJNODE是项目节点右键菜单。这些常量来自VSConstants和相关头文件网上有一份挺全的对照表建议收藏。priority控制同一个菜单里命令的排序值越小越靠前。同一组的按钮之间建议留出 0x0100 的间隔方便以后插新的。注意改完.vsct之后必须重新编译而且最好重置一次实验实例。.vsct的编译产物会被缓存改动没生效十有八九是缓存问题。3.4 编译、调试、验证整个流程走一遍改代码 → F5 → 实验实例打开 → 找到菜单 → 点击 → 看效果。打断点跟普通调试一模一样因为扩展进程就是devenv.exe附加调试器由 VS 自己处理。调试验证的时候我习惯做三件事第一在InitializeAsync里打一个断点确认包被加载了第二在Execute里打一个断点确认命令被正确路由第三故意把.vsct里的 ID 改错一位看看异常信息长什么样——先认识错误后面遇到了才能秒判。第三点听着有点浪费时间但真的很有用因为菜单相关的错误提示都特别含糊。4. 进阶能力工具窗口、编辑器装饰与代码分析4.1 工具窗口Tool Window菜单命令是一次性的工具窗口是常驻的。做一个工具窗口需要三样东西一个继承自ToolWindowPane的类、一个承载内容的 WPFUserControl、以及 Package 类上的[ProvideToolWindow]特性。[ProvideToolWindow(typeof(MyToolWindow))] public sealed class MyPackage : AsyncPackage { private async Task ShowToolWindowAsync(Type toolWindowType, int id, bool create) { var window await this.ShowToolWindowAsync(toolWindowType, id, create, this.DisposalToken); if (window?.Frame null) throw new NotSupportedException(无法创建工具窗口); } }内容部分就是标准 WPFToolWindowPane的Content属性直接塞一个UserControl进去就行。视图模型、数据绑定、命令绑定全都按 WPF 那套来所以如果你熟 WPF这部分基本没有学习成本。工具窗口真正的难点在生命周期。用户关掉窗口时你的控件可能被保留、也可能被销毁取决于 VS 的缓存策略。如果你在窗口里挂了定时器、事件订阅、后台线程一定要在Dispose里断干净不然重新打开窗口时会出现多个实例同时在跑界面数据乱跳。实操心得工具窗口里的耗时操作读文件、调 API、查数据库都放到后台线程去做做完再用JoinableTaskFactory.SwitchToMainThreadAsync()切回 UI 线程更新界面。直接在 UI 线程上做 IO用户会看到整个 IDE 卡住这个体验比功能没实现还糟糕。4.2 编辑器装饰与 WPF 内嵌编辑器装饰Adornment是我觉得最有意思的部分你可以在代码文本的上方或下方绘制自己的 WPF 图层做行内提示、颜色块、行尾按钮、内联图表。实现方式是导出一个IWpfTextViewCreationListener[Export(typeof(IWpfTextViewCreationListener))] [ContentType(code)] [TextViewRole(PredefinedTextViewRoles.Document)] internal sealed class MyAdornmentProvider : IWpfTextViewCreationListener { [Export(typeof(AdornmentLayerDefinition))] [Name(MyLayer)] [Order(After PredefinedAdornmentLayers.Selection, Before PredefinedAdornmentLayers.Text)] public AdornmentLayerDefinition editorAdornmentLayer; }几个属性的含义ContentType(code)表示只对代码文件生效想对纯文本生效就写textTextViewRole限定只对文档视图生效不包含预览窗口这种场景。装饰图层用[Order]决定自己的 Z 轴顺序画在文本下面还是上面全靠它。装饰的核心逻辑是监听LayoutChanged事件然后根据TextViewLines把 WPF 元素定位到对应的行上。这里有个性能陷阱LayoutChanged触发得非常频繁滚动、输入、改窗口大小都会触发。如果你在事件处理里做重活编辑器会卡成一帧一帧的。正确做法是做增量处理只处理可视区域内的行并且用Dispatcher合并高频事件。4.3 Roslyn 分析器与一键修复如果你的目标是代码规范检查那 Roslyn 分析器是更合适的方案它比编辑器装饰轻、比编译期检查灵活。分析器本质上是个独立的 DLLVS 在编译时按需加载不占用常驻内存。一个最简单的分析器长这样[DiagnosticAnalyzer(LanguageNames.CSharp)] public class MyAnalyzer : DiagnosticAnalyzer { private static readonly DiagnosticDescriptor Rule new DiagnosticDescriptor( id: MY0001, title: 异步方法必须以 Async 结尾, messageFormat: 方法 {0} 返回 Task但名称没有以 Async 结尾, category: Naming, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); public override ImmutableArrayDiagnosticDescriptor SupportedDiagnostics ImmutableArray.Create(Rule); public override void Initialize(AnalysisContext context) { context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.Method); } }ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None)这行很重要它会跳过自动生成的代码比如设计器文件、SourceGenerator 产物否则你会收到一堆没意义的警告。EnableConcurrentExecution()允许分析器并行跑大项目上性能差别很明显。配套的CodeFixProvider负责一键修复在RegisterCodeFixesAsync里注册好诊断 ID然后在FixAsync里用SyntaxNode重写语法树并生成新的Document返回。改语法树的时候千万别手动拼字符串一定要用SyntaxFactory和SyntaxNode.ReplaceNode否则注释、格式、换行全都会乱掉代码评审时会被骂死。注意把分析器打包进 VSIX 时source.extension.vsixmanifest里对应的 Asset 类型要写成Microsoft.VisualStudio.Analyzer路径指向编译输出的 DLL。只把 DLL 拷进去而不写 Asset 声明VS 是不会加载的这是新手卡壳的高频点。4.4 选项页与配置持久化任何稍微像样的扩展都需要配置。VS SDK 提供了DialogPage继承它然后用[ProvideOptionPage]挂到工具 → 选项里public class MyOptionsPage : DialogPage { [Category(常规)] [DisplayName(启用检查)] [Description(是否启用实时规范检查。)] public bool EnableCheck { get; set; } true; [Category(常规)] [DisplayName(忽略的目录)] public string IgnoreFolders { get; set; } obj;bin; } [ProvideOptionPage(typeof(MyOptionsPage), 我的扩展, 常规, 0, 0, true)] public sealed class MyPackage : AsyncPackage { /* ... */ }DialogPage最省心的一点是自动持久化用户点确定后属性值会被 VS 写进自己的配置文件里你不用管序列化不用管文件路径不用管权限。想加一个导入导出设置的能力再加一个[ProvideProfile]特性VS 会自动把它挂进设置导入导出向导。配置改动之后怎么通知已经打开的窗口刷新我的做法是在DialogPage里重写OnApply方法在里面触发一个事件工具窗口订阅这个事件然后重新加载数据。直接轮询配置对象是能工作但代码会越来越乱。5. 打包、依赖处理与版本兼容5.1 vsixmanifest 里几个容易写错的字段source.extension.vsixmanifest是扩展的身份证装不装得上、能不能在目标版本上跑全靠这个文件。Installation InstallationTarget IdMicrosoft.VisualStudio.Community Version[17.0,19.0) ProductArchitectureamd64/ProductArchitecture /InstallationTarget /InstallationInstallationTarget的Id决定了支持哪些 VS 版本社区版是Microsoft.VisualStudio.Community专业版是Microsoft.VisualStudio.Pro企业版是Microsoft.VisualStudio.Enterprise。想让扩展在所有版本上都能装最好把三个都写一遍或者直接用Microsoft.VisualStudio.Community微软做了兼容处理。Version是个区间表达式方括号表示包含、圆括号表示不包含。[17.0,19.0)的意思是17.0 及以上、19.0 以下。区间写太窄是导致扩展装不上的头号原因很多人只写了[17.0,18.0)结果 18.x 的用户一看就是灰的。ProductArchitecture在 VS 2022 之后是必填项因为 VS 已经全面转向 64 位写amd64。5.2 程序集加载ProvideBindingPath 的正确用法扩展经常要引用第三方库比如 Newtonsoft.Json、某个 WPF 控件库。这些 DLL 如果直接放在 VSIX 里运行时可能找不到报FileNotFoundException。解决方式是在 Package 类上加[ProvideBindingPath][ProvideBindingPath] public sealed class MyPackage : AsyncPackage { /* ... */ }加上之后VS 会把你的扩展安装目录加入程序集探测路径同目录下的 DLL 就能被正常加载了。这比写AppDomain.AssemblyResolve事件要干净得多不要用后者因为 VS 里同时有几十个扩展在跑全局挂 AssemblyResolve 会影响别人。另一个原则是能引框架程序集就别打包。像System.Text.Json这种如果目标框架已经有了就设置Privatefalse不要塞进 VSIX。扩展包越大加载越慢用户越不爽。我的经验是一个纯命令 工具窗口的扩展VSIX 控制在 500KB 以内是正常水平超过 5MB 就得回头看看是不是带了不该带的东西。5.3 命令行安装与 CI 出包调试跑通了接下来要分发。最简单的分发方式是直接给同事一个.vsix文件双击安装。安装器在 VS 的安装目录下C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\VSIXInstaller.exe D:\build\MyExtension.vsix /quiet/quiet是静默安装适合写进部署脚本。卸载的话加上/uninstall /quiet和扩展的 GUID。打包本身在命令行里做用 MSBuild 就够了msbuild MyExtension.csproj /p:ConfigurationRelease /p:DeployExtensionfalse /p:OutputPath..\artifacts\DeployExtensionfalse这个参数必须加不加的话 MSBuild 会把扩展直接装进你本机的实验实例CI 机器上可能连 VS 都没装直接报错。生成的.vsix就在输出目录里丢进流水线的产物里就行。版本号管理上我建议在 CI 里用参数覆盖清单里的版本避免每次发版都手动改文件msbuild MyExtension.csproj /p:ConfigurationRelease /p:VsixVersion1.2.3不过要注意模板生成的 vsixmanifest 里版本号是硬编码的得先把它改成$Version$之类的占位符或者用BuildVersion属性。这块细节各个 SDK 版本略有差异第一次配置的时候多试两次就明白了。6. 踩坑实录与排查速查表6.1 常见报错与对应原因下面这张表基本覆盖了我这几年遇到过的八成问题照着查能省不少时间。现象大概率原因处理方式F5 后实验实例里找不到菜单.vsct里 GUID 或 ID 与 C# 不一致逐字符比对两边定义注意大小写命令点下去没反应命令没注册成功或注册在错误的 CommandSet在InitializeAsync打断点确认AddCommand执行了改代码后行为没变实验实例缓存了旧版本跑CreateExpInstance.exe /Reset重置扩展装上但标灰InstallationTarget版本区间不匹配放宽区间补上ProductArchitecture启动时报FileNotFoundException第三方 DLL 没被加载Package 类加[ProvideBindingPath]IDE 启动明显变慢包同步加载、初始化做了重活加AllowsBackgroundLoading true重活挪到后台随机崩溃后台线程碰了 UI 对象加ThreadHelper.ThrowIfNotOnUIThread()定位工具窗口数据重复事件订阅没随窗口释放实现IDisposableDispose里断订阅常见问题速查如果遇到的是扩展列表里能看到但功能全都不生效九成是Package类没被正确加载。检查ProvideMenuResource的资源名是不是跟生成的.ctmenu对得上——模板生成的名字是Menus.ctmenu你自己改过.vsct文件名的话这里也要同步改。6.2 线程与 UI 卡顿这一块我想单独拎出来讲因为它是最容易埋雷的地方。VS 的线程模型有两条铁律UI 元素的访问必须在 UI 线程上UI 线程上不能做耗时操作。跨线程访问 UI用JoinableTaskFactoryawait this.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); // 这里已经在 UI 线程上可以安全操作界面JoinableTaskFactory是 VS 对SynchronizationContext的封装它最妙的地方在于能自动处理同步等待异步这种容易死锁的情况。传统的InvokeWait组合在 VS 这种复杂环境里极易死锁所以永远不要用.Result或.Wait()去等待一个需要切回 UI 线程的任务。后台线程做重活的时候记住切回来await TaskScheduler.Default; // 切到线程池 var data await FetchDataAsync(); // 这里跑 IO await this.JoinableTaskFactory.SwitchToMainThreadAsync(DisposalToken); UpdateUi(data); // 回 UI 线程更新启动性能上还有个原则ProvideAutoLoad能不写就不写。这个特性会让你的包在 IDE 启动时就被加载哪怕用户当天根本用不到你的功能。如果确实需要自动加载用规则化的方式比如只在打开解决方案时加载[ProvideAutoLoad(UIContextGuids80.SolutionExists, PackageAutoLoadFlags.BackgroundLoad)]配合BackgroundLoad标志加载动作会被挪到后台不会阻塞启动界面。6.3 调试期几个提效小技巧最后分享几个我自己攒下来的小技巧都是些文档里不会写、但用起来很爽的东西。第一日志写到 Output 窗口不要弹消息框。用ActivityLog或者OutputWindow服务把关键路径的日志打出来。弹窗调试会打断操作流而且改一次代码弹一次很快就烦了。Output 窗口里可以连续看历史。第二给自己留一个重置 启动的一键脚本。调试 VS 扩展的循环是改 → 重置实验实例 → F5 → 验证手动点这四步特别耗耐心写成脚本能明显提升节奏。第三用.pkgdef检查注册结果。VSIX 安装后会在%LocalAppData%\Microsoft\VisualStudio\版本Exp\Extensions\下面生成一堆文件看看你的包是不是真的注册进去了比猜要快得多。第四一个 GUID 只用在一个人身上。团队协作时如果两个人从同一份模板复制项目很容易出现 GUID 冲突表现是装了 A 的扩展B 的就失效了。项目一开始就把 GUID 全部重新生成一遍这件事花两分钟能省掉后面半天的扯皮。第五先做减法再做加法。第一个版本别想着把菜单、工具窗口、分析器全做齐先做一个命令跑通全链路发布给同事试用拿到反馈再扩展。我最早做过一个什么都有的扩展结果调试复杂度爆炸两周没跑通最后砍到只剩一个功能一天就上线了。最后再说一句关于版本兼容的体会。VS 的 SDK 在大版本之间是有变化的尤其是从旧版本升到 17.x 之后线程模型、包加载机制都改过。我的做法是把 SDK 的引用版本定在一个相对新的稳定版上然后靠InstallationTarget的版本区间向下兼容同时在自己的真机上留着两三个不同版本的 VS 做回归。听起来有点麻烦但比起用户发来装了不好使的消息这点准备工作实在算不上什么。
返回列表