如何创建从执行计划窗口打开的 SSMS 扩展?

How can I create an SSMS extension that opens from the execution plan window?

提问人:Joe Obbish 提问时间:8/31/2022 更新时间:1/22/2023 访问量:713

问:

我想创建一个从执行计划窗口打开的 SQL Server Management Studio v18 扩展。我相信这在技术上是可行的,因为有一个第三方工具已经做到了这一点:

enter image description here

到目前为止,我已经能够使用此处的指南在 SSMS v18 中创建基本扩展。我还能够通过引用文档中的 ID 来移动按钮的位置。但是,我无法弄清楚如何修改 .vsct 文件以将我的按钮移动到执行计划窗口中。

如何创建从执行计划窗口打开的 SSMS 扩展?

sql-server visual-studio-extensions ssms-18

评论

2赞 Hannah Vernon 1/10/2023
这是一个非常有用的问题和答案,这显然是 Stack Overflow 的主题。
0赞 Nick.Mc 1/22/2023
我建议记住,Azure Data Studio 是未来的工具,它使用更现代的工具来生成扩展
1赞 Hannah Vernon 1/22/2023
此外,Azure Data Tools 也很糟糕。
0赞 Martin Smith 1/22/2023
上次我看它时,它使用与“粘贴计划”相同的 Web 项目来显示执行计划,这大大不如 SSMS。看起来从那时起事情可能已经发生了变化,尽管 learn.microsoft.com/en-us/sql/azure-data-studio/......
0赞 Nick.Mc 1/25/2023
@HannahVernon Azure Data Studio 肯定是一种不同的体验,不太适合在本地使用。是的,它仍然很不成熟。它确实具有SSMS所没有的一系列功能,并且仍然有点臃肿。如果你指的是SQL Server Data Tools,是的,那绝对是可怕的!

答:

5赞 Martin Smith 1/9/2023 #1

我想通了。

enter image description here

在 *.vsct 文件的元素中,添加<Symbols>

<GuidSymbol name="foo1" value="{33F13AC3-80BB-4ECB-85BC-225435603A5E}">
  <IDSymbol name="foo2" value="0x0080"/>
</GuidSymbol>

然后改变

<Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_TOOLS"/>

<Parent guid="foo1" id="foo2"/>

如此处所述。

(您可能还希望将 on 父元素设置为 if eager to see your extension in the menu higher)priority<Group0x0001

我确定魔术 Guid 的机制最初非常费力,并且在此答案的编辑历史记录中,但一种不那么费力的方法是设置注册表项

[HKEY_CURRENT_USER\SOFTWARE\Microsoft\SQL Server Management Studio\18.0_IsoShell\General]
"EnableVSIPLogging"=dword:00000001

然后打开 SSMS,获取执行计划,然后单击 + + 右键以显示一个消息框,如下所示(小数点后 128 位)。CtrlShift0x80

enter image description here

现在怎么办?

对我来说,如何在菜单单击事件中做任何有用的事情并不明显,所以我认为添加一个示例是有益的。

作为 POC,我尝试对子树成本高于阈值和/或底层执行计划运算符满足一些潜在问题条件的节点进行着色。

enter image description here

为此,我将模板生成代码中方法中的代码更改为Execute

private void Execute(object sender, EventArgs e)
{
   ThreadHelper.ThrowIfNotOnUIThread();

    Dictionary<string, Color> coloringRules = new Dictionary<string, Color>
    {
        [@".//ns:Intrinsic[@FunctionName = ""GetRangeThroughConvert""]"] = Color.Yellow,
        [@".//ns:Intrinsic[@FunctionName = ""GetRangeWithMismatchedTypes""]"] = Color.MediumPurple
    };

    ProofOfConcept.ColorInterestingNodes(coloringRules, costThreshold: 0.0032831);
}

这依赖于以下嵌套类

private class ProofOfConcept
{
    private const string ShowPlanControlTypeFullName = "Microsoft.SqlServer.Management.UI.VSIntegration.Editors.ShowPlan.ShowPlanControl";
    private const string GraphControlTypeFullName = "Microsoft.SqlServer.Management.SqlMgmt.ShowPlan.GraphControl";
    private const string ShowPlanNamespaceUri = "http://schemas.microsoft.com/sqlserver/2004/07/showplan";
    private static readonly XNamespace ns = ShowPlanNamespaceUri;

    [DllImport("user32.dll")]
    public static extern IntPtr GetFocus();

    public static void ColorInterestingNodes(Dictionary<string, Color> coloringRules, double costThreshold)
    {
        IntPtr focus = GetFocus();

        Control activeControl = Control.FromChildHandle(focus);
        Control rootControl = FindRootControl(activeControl); 

        List <Control> graphControls = new List<Control>();

        FindAllDescendantControlsOfType(rootControl, graphControls, GraphControlTypeFullName);

        XElement[] qpElements = GetShowPlanXMLQueryPlans(rootControl);

        //TODO: More robust method of matching up the query plan XML elements with the display elements.
        //e.g. "Use database;" statement will show a graph in "estimated" plan but not "actual" - and not have a QueryPlan element in the XML
        if (graphControls.Count != qpElements.Count())
        {
            MessageBox.Show("Mismatch between graph control count (" + graphControls.Count + ") and query plan count (" + qpElements.Count() + "). Exiting");
            return;
        }

        for (var index = 0; index < graphControls.Count; index++)
        {
            Control graphControl = graphControls[index];
            XElement qpElement = qpElements[index];

            Dictionary<int, Color> nodeBackgroundColors = GetNodeBackgroundColors(qpElement, coloringRules);

            foreach (dynamic item in ((dynamic)graphControl).Nodes)
            {
                var nodeId = item.NodeOriginal["NodeId"] ?? -1;

                if (item.NodeOriginal.Cost >= costThreshold)
                {
                    item.TextColor = Color.Red;
                    item.BackgroundColor = Color.White;
                    item.UseBackgroundColor = true;
                }

                if (nodeBackgroundColors.TryGetValue(nodeId, out Color color))
                {
                    item.BackgroundColor = color;
                    item.UseBackgroundColor = true;
                }
            }

            graphControl.Refresh();

        }
    }

    private static Dictionary<int, Color> GetNodeBackgroundColors(XElement queryPlan, Dictionary<string, Color> coloringRules)
    {
        var returnValue = new Dictionary<int, Color>();

        NameTable nt = new NameTable();
        XmlNamespaceManager namespaceManager = new XmlNamespaceManager(nt);
        namespaceManager.AddNamespace("ns", "http://schemas.microsoft.com/sqlserver/2004/07/showplan");

        foreach (var coloringRule in coloringRules)
        {
            var foundElements = queryPlan.XPathSelectElements(coloringRule.Key, namespaceManager);

            foreach (var foundNode in foundElements)
            {
                var nodeId = foundNode.AncestorsAndSelf(ns + "RelOp").FirstOrDefault()?.Attribute("NodeId")?.Value;

                if (nodeId != null)
                {
                    returnValue[int.Parse(nodeId)] = coloringRule.Value;
                }
            }
        }

        return returnValue;
    }

    private static XElement[] GetShowPlanXMLQueryPlans(Control rootControl)
    {
        List<Control> showPlanControls = new List<Control>();

        FindAllDescendantControlsOfType(rootControl, showPlanControls, ShowPlanControlTypeFullName);

        Assembly sqlEditorsAssembly = Assembly.Load("SQLEditors");
        Type showPlanControlType = sqlEditorsAssembly.GetType(ShowPlanControlTypeFullName);

        MethodInfo GetShowPlanXmlMethod = showPlanControlType.GetMethod("GetShowPlanXml", BindingFlags.Instance | BindingFlags.NonPublic);

        string xplan = GetShowPlanXmlMethod.Invoke(showPlanControls[0], null) as string;

        XDocument doc = XDocument.Parse(xplan);

        return doc.Descendants(ns + "QueryPlan").ToArray();
    }

    private static Control FindRootControl(Control control)
    {
        while (control.Parent != null)
            control = control.Parent;

        return control;
    }

    private static void FindAllDescendantControlsOfType(Control control, List<Control>  graphControls, string typeFullName)
    {
        if (control.GetType().FullName == typeFullName)
            graphControls.Add(control);

        foreach (Control child in control.Controls)
            FindAllDescendantControlsOfType(child, graphControls, typeFullName);
    }

}