Monday 25 April 2011

SQL Server Management Studio SSMS 2008 R2 Add-In From Scratch

So there have been so many features I wish I had in SSMS, so I have been wanting to add-In for various things but it is quite a mission to get it working and once you do little changes needs to be compiled and deployed, so I decided to create an extensible add-in do the dirty work once and from there have a clean and easy way to extend on the fly, add new add-ins and explore the Environment using my favourite scripting language Python (IronPython). But first I'll go over the steps to create the raw add-in code and get it registered and loading in SSMS 2008.

I won't go into many explanations about why certain things are done, but all I can say is that there aren't much clear documentation on how to create specific add-ins for SSMS 2008 even though there are for 2005 a lot has changed and some things don't work anymore for 2008 and worse Microsoft have changed some of the interfaces again in 2008 R2 so considering that the dependencies on the API's are flaky which is even more reason to create a flexible plugin with an exploratory environment. With that said I will dive right in and just give the instructions as it is now to get it working in SSMS 2008 R2 using Visual Studio 2010 and .NET 4.0.


Start VS 2010 and Create a new Project
File -> New -> Project


Other Project Types -> Extensibility -> Visual Studio Add-In

The Wizard will appear


Choose Create an Add-In using Visual Studio C#

Click next to add the default support for hosts.


Now Name the Add-In


Ignore the next page's options for now, click next


Lets skip the next page as well:

Click Finish:

The template will be created.

But there is one problem, the default template currently is incorrect and will fail with the latest SSMS open the Connect.cs class and notice the following code:
public void OnConnection(object application, ext_ConnectMode connectMode, object addInInst, ref Array custom)
  {
   _applicationObject = (DTE2)application;
   _addInInstance = (AddIn)addInInst;

  
  }

You Have to change this code slightly as follows:
public void OnConnection(object application, ext_ConnectMode connectMode, object addInInst, ref Array custom)
  {
            // the application object is no longer the DTE object
   //_applicationObject = (DTE2)application;

   _addInInstance = (AddIn)addInInst;
            // this is the new application object
            _applicationObject = (DTE2)_addInInstance.DTE;
  
  }

The Application object is no longer the DTE application object in the latest SSMS.

Then lets add an interface to our Connect class:

IDTCommandTarget

as follows:

public class Connect : IDTExtensibility2, IDTCommandTarget
 {

This will allow us to also receive events from any commands that are executed.

So now lets create a controller class, that understands these objects so that this is also more reusable to create future add-ins. where we will handle all these method stubs.

This is the entire class with all the methods we will need:


/// The object for implementing an Add-in.
    public class Connect : IDTExtensibility2, IDTCommandTarget
 {
        AddInController _controller;
  /// Implements the constructor for the Add-in object. Place your initialization code within this method.
  public Connect()
  {
  }

  /// Implements the OnConnection method of the IDTExtensibility2 interface. Receives notification that the Add-in is being loaded.
  /// Root object of the host application.  /// Describes how the Add-in is being loaded.  /// Object representing this Add-in. 
public void OnConnection(object application, ext_ConnectMode connectMode, object addInInst, ref Array custom)
  {
            _controller = new AddInController(addInInst);
            _controller.OnConnection(application, connectMode, addInInst, ref custom);
  }

  /// Implements the OnDisconnection method of the IDTExtensibility2 interface. Receives notification that the Add-in is being unloaded.
  /// Describes how the Add-in is being unloaded.  /// Array of parameters that are host application specific. 
public void OnDisconnection(ext_DisconnectMode disconnectMode, ref Array custom)
  {
            _controller.OnDisconnection(disconnectMode, ref custom);
  }

  /// Implements the OnAddInsUpdate method of the IDTExtensibility2 interface. Receives notification when the collection of Add-ins has changed.
  /// Array of parameters that are host application specific. 
public void OnAddInsUpdate(ref Array custom)
  {
            _controller.OnAddInsUpdate(ref custom);
  }

  /// Implements the OnStartupComplete method of the IDTExtensibility2 interface. Receives notification that the host application has completed loading.
  /// Array of parameters that are host application specific. 
public void OnStartupComplete(ref Array custom)
  {
            _controller.OnStartupComplete(ref custom);
  }

  /// Implements the OnBeginShutdown method of the IDTExtensibility2 interface. Receives notification that the host application is being unloaded.
  /// Array of parameters that are host application specific. 
public void OnBeginShutdown(ref Array custom)
  {
            _controller.OnBeginShutdown(ref custom);
  }

        public void Exec(string CmdName, vsCommandExecOption ExecuteOption, ref object VariantIn, ref object VariantOut, ref bool Handled)
        {
            _controller.Exec(CmdName, ExecuteOption, ref VariantIn, ref VariantOut, ref Handled);
        }

        public void QueryStatus(string CmdName, vsCommandStatusTextWanted NeededText, ref vsCommandStatus StatusOption, ref object CommandText)
        {
            _controller.QueryStatus(CmdName, NeededText, ref StatusOption, ref CommandText);
        }
    }



Next we would like to create some tool windows, for that I will implement the following function in the controller:

public ToolWindowContext CreateToolWindow(string typeName, string assemblyLocation, string Caption, Guid uiTypeGuid, DTE2 applicationObject,
            AddIn addinInstance)
        {
            Windows2 win2 = applicationObject.Windows as Windows2;
            if (win2 != null)
            {
                object controlObject = null;
      
                Window toolWindow = win2.CreateToolWindow2(addinInstance, assemblyLocation, typeName, Caption, "{" + uiTypeGuid.ToString() + "}", ref controlObject);
                Window2 toolWindow2 = (Window2)toolWindow;
                toolWindow.Visible = true;
                return new ToolWindowContext(toolWindow, toolWindow2, controlObject);
            }
            return null;
        }


Next you have to install the add-in, the easiest is to create a deployment package:

Right Click Add -> New Project



Choose  Other Project Types -> Setup And Deployment -> Setup Project



Right Click on the newly created project and choose Add -> Project Output





Right Click on the Detected Dependencies and Select Exclude for the Visual Studio DLL's as these are already installed on the machine:








Next, bring up the Registry Editor. Click View -> Editor -> Registry.


Finally set the main output of the project to register for COM:
Locate the Primary Output and view the properties,
Locate the Register Property,
Click the dropdown
Choose vsdrpCOM



Now we are ready to start the basic add, in but first I wanted to create a basic DLL to host some tool windows. I wanted my first tool window to be a log window simply to capture events from my add-in.

What you need to do is place your GUI controls in a seperate DLL. It has to be a .NET 3.5 preferably as it seems that the ide has problems loading a .NET 4 assembly. However I dont need the latest version of .net here for now considering the controls that I plan to create.

So create a new assembly: Add -> New Project -> Class Library

Right click on the new project and Add -> User Control

Name the user Control: UcLogWindow.cs

NOTE: You have to add an attribute to the class to make the container visible to your add in:

[ComVisible(true)]

Add a method to Log a message to the window:
public void LogMessage(string message)
        {
}

The full declaration of the control

public partial class UcLogWindow : UserControl
  
    [ComVisible(true)]
    public partial class UcLogWindow : UserControl
    {
        public void LogMessage(string message)
        {
            richTextBox1.AppendText(string.Format("[{0:HH:MM:ss}]: {1}{2}", DateTime.Now, message, System.Environment.NewLine);
        }
        public UcLogWindow()
        {
            InitializeComponent();
        }
    }

Now we will use the CreateToolWindow Function we added earlier to create a log window for our add-in.

IToolWindowContext CreateCommonUIControl(string typeName, string caption, Guid guid)
        {
            return _adapter.CreateToolWindow(typeName, _uiDLL, caption, guid);
        }

 void CreateLogWindow()
        {

            _logWindow = CreateCommonUIControl("ACSR.SqlServer.Addin.Core.UI.UcLogWindow", "IronPython LogWindow", new Guid("3ADC13FF-DCF4-4C49-B2EF-3D78DECDC664"));
            _logWindowControl = ((dynamic)_logWindow.ControlObject);

        }


  public void OnConnection(object application, ext_ConnectMode connectMode, object addInInst, ref Array custom)
        {
           _workingDirectory = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
           _uiDLL =  Path.Combine(_workingDirectory, "ACSR.SqlServer.Addin.Core.UI.dll");


           CreateLogWindow();
}

So now we are ready to install the Add-In, Right click on the Setup Project and choose Install:



If you start SSMS you should now see the Add-In Loading with the logwindow.


That's it for preparing the plugin, now we have everything in place to start coding the add-ins.
Our methods are just stubs still and to get them to work and explore all the features of the IDE is big enough for a separate article.

We don't have any IronPython scripting in this Add-In yet, but I have everything I need in place to add script hosting to the next article and Version.