【小试插件开发】给Visual Studio装上自己定制的功能来提高代码调试效率

时间:2020-12-01 18:02:16

背后的故事

随着项目需求的逐步增加,后端开发框架在我手上也慢慢重构为组件开发模式,整体结构类似于NopCommence。在这种结构中,每个组件所在的类库项目其实是生成到网站项目里指定的一个目录的,然后随之而来的就有一个不痛不痒的问题一直挥之不去。那就是每次在组件内修改代码后都要清理解决方案,然后重新生成一下才能开始调试。如果不重新生成的话,修改后的代码根本看不到效果,但是重新生成会替换上一次生成的程序集,这时候程序集有可能正在被iis express的进程占用就会生成失败,这时候就要先清理解决方案。

对于那种只在视图里改了一个文字的情况还要重新生成简直是不能忍,所以特别怀念之前web开发中保存文件后刷新浏览器就能看到效果的日子。虽然说操作上也不是很复杂,可是由于项目众多,每次先清理再编译一次特别浪费时间,最重要的是修改前端代码完全不需要去编译啊,于是就有了下面的想法。

因为生成项目的时候本质上对静态文件是一个复制过程,就想着有没有办法通过一个操作把组件内的视图文件复制到指定目录下去?

既然有了这个想法,那也不能塞回去吧,就只有一个字了:干!

把想法付诸实践

既然想给VS添加自己想要的功能,那就得给VS开发一个插件了。记得以前看过VS插件开发的帖子,估计用的上,照猫画狗加上百度一番,终于把想要的东西实现了。

先创建一个插件项目:

【小试插件开发】给Visual Studio装上自己定制的功能来提高代码调试效率

然后在项目中添加一个自定义命令MyCommand:

【小试插件开发】给Visual Studio装上自己定制的功能来提高代码调试效率

可以看到项目中出现了很多以“MyCommand”开头的文件,不用猜也知道都是和这个命令有关的一些文件。其中“MyCommand.cs”需要特别关注,因为你的命令创建、回调事件都是在这个类中定义的,这里面必须要了解的就是MenuItemCallback方法,看名称大致可以猜到它是你命令执行的回调函数。说白了,你的命令想干些什么事就是在这个方法里面code出来的,看一下自动生成的代码:

        /// <summary>
/// This function is the callback used to execute the command when the menu item is clicked.
/// See the constructor to see how the menu item is associated with this function using
/// OleMenuCommandService service and MenuCommand class.
/// </summary>
/// <param name="sender">Event sender.</param>
/// <param name="e">Event args.</param>
private void MenuItemCallback(object sender, EventArgs e)
{
string message = string.Format(CultureInfo.CurrentCulture, "Inside {0}.MenuItemCallback()", this.GetType().FullName);
string title = "MyCommand"; // Show a message box to prove we were here
VsShellUtilities.ShowMessageBox(
this.ServiceProvider,
message,
title,
OLEMSGICON.OLEMSGICON_INFO,
OLEMSGBUTTON.OLEMSGBUTTON_OK,
OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
}

通过VS的方法提示和代码注释可以看到方法体内主要做了一个弹框操作,类似Winform的MessageBox.Show()的玩意儿,那我们就在这里根据实际需求来写代码。

我的需求是:通过执行这个命令把当前编辑的文件保存到本地指定的一个目录中,如果有同名文件则直接替换。非常简单的需求,那就开始像平常开发那样啪啪啪地coding了。中间只有一个需要注意的点,就是要根据当前文件所在的组件名称去拼接目标目录,好在我的项目命名都是有规律的,所以也就比较轻松了。主要代码为:

        private void MenuItemCallback(object sender, EventArgs e)
{
       var dte = this.ServiceProvider.GetService(typeof(DTE)) as DTE;
var doc = dte?.ActiveDocument;//当前文档
if (doc == null)
{
ShowErrorMessage();
return;
}
if (!doc.Name.EndsWith(".cshtml")&&!doc.Name.EndsWith(".js")&&!doc.Name.EndsWith(".css"))
{
ShowErrorMessage();
return;
}
doc.Save();
dte.StatusBar.Text = "suibao:当前修改已保存";
DirectoryInfo directory = new DirectoryInfo(doc.Path);
var projectPath = directory.Parent.Parent;
var moduleName = projectPath.Name.Split('.');
if (moduleName.Length > )
{
string path = doc.Path.Replace(projectPath.Name, "SuiBao.WebAdmin\\Plugins\\" + moduleName[]);
//doc.Save(path);
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
File.Copy(doc.FullName, path + doc.Name, true);//复制且替换
dte.StatusBar.Text = "suibao:保存到组件目录完成";
}
else
{
ShowErrorMessage();
}
} private void ShowErrorMessage()
{
VsShellUtilities.ShowMessageBox(ServiceProvider, "无效操作!", "系统提示", OLEMSGICON.OLEMSGICON_WARNING, OLEMSGBUTTON.OLEMSGBUTTON_OK, OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
}

目前只做了Razor视图、js、css的处理,同时也做了异常操作处理,并且在VS状态栏中给出操作结果提示。然后编译、运行,这时会在VS的主菜单“工具”下面第一行多了自定义的命令:

【小试插件开发】给Visual Studio装上自己定制的功能来提高代码调试效率

觉得“Invoke MyCommand”这个名字不喜欢想自己定义?没问题~打开项目中的“MyCommandPackage.vsct”文件,找到Buttons这个节点,里面定义了我们命令的各种属性,改名称改图标自己看着办:

      <Button guid="guidMyCommandPackageCmdSet" id="MyCommandId" priority="0x0100" type="Button">
<Parent guid="guidMyCommandPackageCmdSet" id="MyMenuGroup" />
<Icon guid="guidImages" id="bmpPic1" />
<Strings>
<ButtonText>MyCommandDemo</ButtonText>
</Strings>
</Button>

什么?觉得每次都要点2次菜单太麻烦想搞个快捷键?小意思~有两种方式,如下。

方式一,在配置文件中设置快捷键,参考这里

<KeyBindings>
<KeyBinding guid="guidMyCommandPackageCmdSet" id="MyCommandId"
editor="guidVSStd97" key1="Q" mod1="CONTROL"/>
</KeyBindings>

方式二,在VS中给命令设置快捷键:

依次打开菜单“工具”-“选项”-“环境”-“键盘”,按名称搜索到命令,然后输入快捷键,点击“分配”,再保存一下,搞定。

【小试插件开发】给Visual Studio装上自己定制的功能来提高代码调试效率

持续地探索

折腾到现在总算是解决了其中一个问题,内心多少有点小兴奋。回到项目中,依然有个痛点亟需解决,那就是关于编译的问题。稍微分析一下不难发现,这个问题的核心其实就是DLL文件生成与存放路径。于是就打算继续按上面的套路,在本项目生成程序集然后copy到web项目中,然后就开干了。在写代码过程中,发现EnvDTE.DTE这个接口提供了很多操作VS资源的方法,然后顺着一路找下来看到了SolutionBuild这个接口对解决方案有各种Build相关的方法(参考这里这里),于是果然放弃之前的套路,打算把“清理”和“重新编译”两个命令结合到一起。因为按原来的思路,也是要先编译完才能复制DLL,中间还要解决DLL被进程占用的问题,还不如直接Clean+Build一条龙来的快。代码非常简单:

        private void MenuItemCallback(object sender, EventArgs e)
{
var dte = this.ServiceProvider.GetService(typeof(DTE)) as DTE;
dte.Solution.SolutionBuild.Clean(true);
dte.Solution.SolutionBuild.Build();
}

有了上面提到的那些接口,发现能够干的事太多了,几乎可以随心所欲来扩展自己想要的功能。

总结

本文的目的并不是展示Visual Studio插件开发的流程,只是借这个例子来阐述遇到问题时要积极寻找合适的工具或方法去解决问题,对于过程中碰到未知领域,要乐于探索,对于工作中那种重复性特别高的事,尽可能想办法来提高效率。我是第一次接触VS插件开发,本文的例子也是最最基础的尝试。网上有很多强大和酷炫的插件开发示例,VS的插件库也有很多实用的扩展包可以下载使用。总之,能解决你实际问题的任何过程和产出都是有价值、有意义的~