65.9K
CodeProject 正在变化。 阅读更多。
Home

创建 SQL Server Management Studio 12 插件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.74/5 (14投票s)

2016年1月26日

CPOL

7分钟阅读

viewsIcon

46946

downloadIcon

1986

在 SQL Server 12 中开发您自己的 SSMS 对象资源管理器\命令栏菜单

下载插件项目

下载 SQL 脚本\数据

引言

和大多数 DBA 一样,我发现自己一遍又一遍地重复相同的 SQL 语句。肯定有更简单的方法可以提高 SQL 工作效率。因此,我决定使用 Visual Studio 13 开发自己的 SSMS (12) 自定义插件,而不是键入“SELECT COUNT(*) FROM XYZ_Table”。

背景

右键单击表即可获取行数,或者选择一列并查找该表中的重复值,或者从鼠标单击而不是键入、剪切\粘贴或从文件加载已保存的 SQL 查询并执行它们,就能针对 Master 数据库运行通用的常见 SQL 语句,这样难道不会更简单吗?

必备组件

我们要实现的目标

下面是插件将创建的自定义表上下文菜单(自定义 SQL & 脚本完整架构)的示例。您还将看到确定用户是右键单击表还是列的逻辑,并显示该对象的相应上下文菜单。下面是表的上下文菜单。对于列的上下文菜单,您可以找到该列中的重复值。

下面您可以看到,我为列添加了一个新的上下文菜单选项。在此示例中,我查找了特定列中的所有重复值。此示例将在稍后通过另一个屏幕截图以及 SQL\代码的解释来解释。

我们还将创建菜单命令来执行全局 SQL 语句。例如,下面我们正在使用“工具”菜单执行“终止事务”SQL 语句。我的 SQL 设计方式是,它不会完全执行 SQL,但有一些注释掉的行。您可以取消注释并在对 SQL 满意后运行它们。

创建您的插件项目

在获得插件项目的骨架之前,需要经过几个屏幕。下面您可以看到,大多数情况下您都可以默认完成。只需确保安装 Visual Studio 13 SDK,因为它包含了 Visual Studio 13 中使用的新插件项目模板。

项目设置和部署注意事项

SSMS 版本之间的重大更改

过去,当您必须为 SSMS 2005 开发插件,然后为 SSMS 20088\R2 开发相同的插件代码时,您的代码将无法编译,因为许多属性\方法已被弃用且不向后兼容。在 SSMS 12+ 中,Microsoft 已(尽管使用的是 Visual Studio 10)重新设计了插件背后的对象。从而使其更具前瞻性兼容性……我对此持保留态度,但冲突应该会大大减少。

部署文件夹

当您想在 SQL Server 中测试您的插件时,您需要让 SQL Server 知道它需要在启动时附加插件。为此,只需将您的 .Addin 文件复制到以下路径之一。下面的选项 2 将为所有 SQL Server 用户附加插件。如果以下文件夹不存在,则只需创建文件夹结构。

  1. C:\Users\UserName\AppData\Roaming\Microsoft\MSEnvShared\Addins
  2. C:\ProgramData\Microsoft\MicrosoftSQL Server Management Studio\11.0\Addins

程序集引用

我认为最麻烦的区域是程序集。因为存在名称相似且版本冲突的程序集。但下面的列表应该涵盖您的大部分插件任务。您可以在项目中看到程序集的路径(程序集的属性),并以此为指南在您的环境中查找程序集。

项目属性

右键单击您的项目,然后从上下文菜单中选择属性。将启动您的插件的外部应用程序设置为您的SQL Server Management Studio可执行文件的路径。大约是

C:\Program Files (x86)\Microsoft SQL Server\110\Tools\Binn\ManagementStudio\Ssms.exe

删除“命令行参数”和“工作目录”条目。

配置插件文件 XML

在部署或测试您的插件之前,您需要对其进行配置。基本上,您只需要更新三个元素。

  1. 名称 - 将此更改为Microsoft SQL Server Management Studio
  2. 版本 - 插入一个星号,这将涵盖各种 SQL Server 版本(因为会有 CTP 版本等)
  3. 程序集 - 输入您的插件程序集的路径(测试时为 debug,准备部署时为 release)

调试时关闭 PInvoke 异常

调试时,PInvoke 错误会弹出,这在运行时不是问题,而是因为 C++ 和 C# 之间的 LongInt 转换导致冲突(仅限调试)。因此,要停止弹出,只需在 Visual Studio 中关闭 PInvoke。

点击 Debug.Exceptions,然后向下滚动到 Pinvoke Stack Imbalance 并取消选中它。

项目结构

该插件具有非常简单的项目结构,一个主(connect.cs)类,一个控制器类,两个菜单项类以及我们将部署到 Addin 文件夹的 SSMSAdin.addin XML 文件。有一个部分类用于分离事件,而不是弄乱主 Connect.cs 类,因为我们将只使用几个主要的事件。

代码解释

创建对象资源管理器上下文 & 命令栏菜单项

您将实现的 IDTExtensibility2 成员方法之一是 OnConnection。这是插件的起点。下面的代码片段创建了新的 Tools 菜单项,以及一个事件处理程序,用于在对象资源管理器操作更改时(即单击表或列 - 此事件将捕获该操作),从而允许您控制特定对象资源管理器树项应显示的上下文菜单。

public void OnConnection(object application, ext_ConnectMode connectMode, object addInInst, ref Array custom)
        {
            applicationObject = (DTE2)application;
            addInInstance = (AddIn)addInInst;

            try
            {
                ContextService contextService = (ContextService)ServiceCache.ServiceProvider.GetService(typeof(IContextService));
                contextService.ActionContext.CurrentContextChanged += ActionContextOnCurrentContextChanged;
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }

            if (connectMode == ext_ConnectMode.ext_cm_UISetup)
            {
                object[] contextGUIDS = new object[] { };
                Commands2 commands = (Commands2)applicationObject.Commands;
                string toolsMenuName = "Tools";
                
                //Find the MenuBar command bar, which is the top-level command bar holding all the main menu items:
                Microsoft.VisualStudio.CommandBars.CommandBar menuBarCommandBar = ((Microsoft.VisualStudio.CommandBars.CommandBars)applicationObject.CommandBars)["MenuBar"];

                //Find the Tools command bar on the MenuBar command bar:
                CommandBarControl toolsControl = menuBarCommandBar.Controls[toolsMenuName];
                CommandBarPopup toolsPopup = (CommandBarPopup)toolsControl;

                //This try/catch block can be duplicated if you wish to add multiple commands to be handled by your Add-in,
                //  just make sure you also update the QueryStatus/Exec method to include the new command names.
                try
                {
                    //Add a command to the Commands collection:
                    cmdKillTrans = commands.AddNamedCommand2(addInInstance, "KillTrans", "Kill Transactions", "List all transactions that can be stopped", true, 9, ref contextGUIDS, (int)vsCommandStatus.vsCommandStatusSupported + (int)vsCommandStatus.vsCommandStatusEnabled, (int)vsCommandStyle.vsCommandStylePictAndText, vsCommandControlType.vsCommandControlTypeButton);

                    //Add a control for the command to the tools menu:
                    if ((cmdKillTrans != null) && (toolsPopup != null))
                    {
                        cmdKillTrans.AddControl(toolsPopup.CommandBar, 1);
                    }
                }
                catch (System.ArgumentException ex)
                {
                    MessageBox.Show(ex.Message);
                    DebugMessage(ex.Message);
                }

                //This try/catch block can be duplicated if you wish to add multiple commands to be handled by your Add-in,
                //  just make sure you also update the QueryStatus/Exec method to include the new command names.
                try
                {
                    //Add a command to the Commands collection:
                    cmdServerInfo = commands.AddNamedCommand2(addInInstance, "ServerInfo", "Server Information", "List server\\product information", true, 5, ref contextGUIDS, (int)vsCommandStatus.vsCommandStatusSupported + (int)vsCommandStatus.vsCommandStatusEnabled, (int)vsCommandStyle.vsCommandStylePictAndText, vsCommandControlType.vsCommandControlTypeButton);

                    //Add a control for the command to the tools menu:
                    if ((cmdServerInfo != null) && (toolsPopup != null))
                    {
                        cmdServerInfo.AddControl(toolsPopup.CommandBar, 2);
                    }
                }
                catch (System.ArgumentException ex)
                {
                    MessageBox.Show(ex.Message);
                    DebugMessage(ex.Message);
                }
                 //This try/catch block can be duplicated if you wish to add multiple commands to be handled by your Add-in,
                //  just make sure you also update the QueryStatus/Exec method to include the new command names.
                try
                {
                    //Add a command to the Commands collection:
                    commandBackUp = commands.AddNamedCommand2(addInInstance, "BackupInfo", "Backup Information", "List backup information", true, 8, ref contextGUIDS, (int)vsCommandStatus.vsCommandStatusSupported + (int)vsCommandStatus.vsCommandStatusEnabled, (int)vsCommandStyle.vsCommandStylePictAndText, vsCommandControlType.vsCommandControlTypeButton);

                    //Add a control for the command to the tools menu:
                    if ((commandBackUp != null) && (toolsPopup != null))
                    {
                        commandBackUp.AddControl(toolsPopup.CommandBar, 3);
                    }
                }
                catch (System.ArgumentException ex)
                {
                    MessageBox.Show(ex.Message);
                    DebugMessage(ex.Message);
                }

            }
        }

对象资源管理器事件处理程序

下面的事件在上面的 OnConnection 方法中设置,因此我们可以捕获每个对象资源管理器的更改。我们利用 static 变量来防止上下文菜单项被创建多次。

注意:UserTables 是表的 InvariantName

private void ActionContextOnCurrentContextChanged(object sender, EventArgs e)
        {
            try
            {
                INodeInformation[] nodes;
                INodeInformation node;
                int nodeCount;
                IObjectExplorerService objectExplorer = (ObjectExplorerService)ServiceCache.ServiceProvider.GetService(typeof(IObjectExplorerService));

                objectExplorer.GetSelectedNodes(out nodeCount, out nodes);
                node = nodeCount > 0 ? nodes[0] : null;

                if (node != null)
                {
                    if (node.Parent.InvariantName == "UserTables")
                    {
                        if (!IsTableMenuAdded)
                        {
                            _tableMenu = (HierarchyObject)node.GetService(typeof(IMenuHandler));
                            SqlTableMenuItem item = new SqlTableMenuItem(applicationObject);
                            _tableMenu.AddChild(string.Empty, item);
                            IsTableMenuAdded = true;
                        }
                    }
                    else if (node.Parent.InvariantName == "Columns")
                    {
                        if (!IsColumnMenuAdded)
                        {
                            _tableMenu = (HierarchyObject)node.GetService(typeof(IMenuHandler));
                            SqlColumnMenuItem item = new SqlColumnMenuItem(applicationObject);
                            _tableMenu.AddChild(string.Empty, item);
                            IsColumnMenuAdded = true;
                        }
                    }
                }
            }
            catch (Exception ObjectExplorerContextException)
            {
                MessageBox.Show(ObjectExplorerContextException.Message);
            }
        }

断开连接

断开与 SQL Server 的连接时,我们需要进行清理并删除新添加的菜单项。当 SQL Server 启动时,它们将被重新加载,因为 SQL Server 会在 Addin 文件夹中查找。

我们删除它们是因为,您可以手动进入并删除 Addin 文件,而它们仍会留在 SQL Server 中(永远!!!)。

public void OnDisconnection(ext_DisconnectMode disconnectMode, ref Array custom)
        {
            switch (disconnectMode)
            {
                case ext_DisconnectMode.ext_dm_HostShutdown:
                case ext_DisconnectMode.ext_dm_UserClosed:
                    if ((cmdServerInfo != null))
                    {
                        cmdServerInfo.Delete();
                    }
                    if ((commandBackUp != null))
                    {
                        commandBackUp.Delete();
                    }
                    if ((cmdKillTrans != null))
                    {
                        cmdKillTrans.Delete();
                    }                 
                    break;
            }
        }

上下文菜单点击事件

重复上下文菜单项的点击事件将检索数据库、表和列名,并构建 SQL 语句。在新的查询窗口中显示它们并执行。

 private void Duplicate_Click(object sender, EventArgs e)
        {
            ToolStripMenuItem item = (ToolStripMenuItem)sender;
            bool generateColumnNames = (bool)item.Tag;

            Match match = columnRegex.Match(this.Parent.Context);
            if (match != null)
            {
                string columnName = match.Groups["Column"].Value;
                string tableName = match.Groups["Table"].Value;
                string schema = match.Groups["Schema"].Value;
                string database = match.Groups["Database"].Value;
                string connectionString = this.Parent.Connection.ConnectionString + ";Database=" + database;
                string sqlStatement = string.Format(SSMSAddin.Properties.Resources.SQLDuplicateColumnData, columnName, tableName);

                this.dteController.CreateNewScriptWindow(new StringBuilder(sqlStatement)); // create new document

                this.applicationObject.ExecuteCommand("Query.Execute"); // get query analyzer window to execute query
            }
        }

我们可以看到确实存在重复的艺术家,如果我们从表中进行 select - 当我们执行上述重复查询时,它会留下一个唯一的艺术家。

在 SQL Server 12 中测试\运行插件

由于我们将 SSMS 设置为要测试的外部应用程序(项目属性),SSMS 将自动启动,我们可以单步执行(F10)调试器。

注意: 记住将您的 .Addin 文件复制到部署文件夹之一,并且 XML 元素 Assembly 引用我们的 Debug 程序集。

下面是我们创建的每个菜单项的示例。

表上下文菜单

表计数

生成插入脚本

列上下文菜单

正如我们在上一节中看到的,有一个列上下文菜单项。请注意注释掉的行,当您对执行 SQL 语句有信心时,可以取消注释这些行。

全局 - 命令栏菜单

从“工具”菜单中,我们可以看到新创建的(全局作用)SQL 语句。我在其中包含了一些常见的 SQL 语句,例如终止事务服务器信息和显示备份信息

下面,我执行了服务器信息菜单项,这将提供有关您的 SQL Server 的一般信息。

SQL Server 12 版本详情

下面是我用于开发插件的 SQL Server 12 的版本详情(足够标准)。

关注点

既然您已经看到了如何引用表或列,那么扩展您的自定义插件以执行更复杂的 SQL 语句就相对容易了。例如,当选择两个列时,可以选择基于连接(因为您可以执行 SQL 语句来查找 FK 并动态构建 SQL)的所有内容。

有用链接

© . All rights reserved.