How to install custom controls

After creating some stuff, you’d like to use it from time to time in a different project. How to install it on VS Toolbar, add to “Add reference…” dialog and how to avoid underwater rocks – all this will be uncovered on next few pages.

Prepare assembly

When you have project with tuned custom controls and you feel fine how they works and looks like – you probably automate install actions. What you have to do to install controls:

  1. Add sign to assembly;
  2. Install assembly to GAC;
  3. Add to Visual Studio “Add reference…” dialog;
  4. Add to Visual Studio Toolbar.

I’m going to show you how to do almost all of this numbers programmatically in code snippets. Assume that control’s library called myCustomControl.dll. It’s up to you how to combine them for nice looking application. ;)

It’s very easy to add a strong name to assembly. Right click on project > Properties > Signing. Check the second option “Sign the assembly”. Combo box will be editable.

Choose “<New…>” and fill text fields. After all press “OK”. Rebuild. This is it. You’ve just created strong key for your assembly.

The next step is…

Add to GAC

I think that more suitable in this case is to use gacutil.exe. This program can be executed from VS command prompt as:

gacutil /i “c:\my controls\myCustomControl.dll”

Enclose path in brackets if there are white spaces. This operation will add assembly to GAC. In our case I used key /if (install force) to overwrite assembly in GAC if already exists. Code below just implement this simply actions.

Gacutil can be found in .NET SDK. For the version 3.5 path is C:\Program Files\Microsoft SDKs\Windows\v6.0A\Bin\gacutil.exe; 4.0 path is C:\Program Files\Microsoft SDKs\Windows\v7.0A\Bin\gacutil.exe. Taking in an account this information we get following code:

public class InstallToGac {
    public void Install(string fullAssemblyLocation) {
        var net35 = @"C:\Program Files\Microsoft SDKs\Windows\v6.0A\Bin\gacutil.exe";
        var net40 = @"C:\Program Files\Microsoft SDKs\Windows\v7.0A\Bin\gacutil.exe";
        var value = File.Exists(net35)
                              ? net35
                              : File.Exists(net40)
                                    ? net40
                                    : "";
        if (value == "") return;
        var process = new Process();

        process.StartInfo.FileName = value;
        process.StartInfo.Arguments = string.Format("/if \"{0}\"", fullAssemblyLocation);
        process.StartInfo.CreateNoWindow = true;
        process.Start();
        process.WaitForExit();
    }
}

You’ll see blinked command window and that’s all. Assembly in GAC. To uninstall assembly use key “/u” and add “f” to force operation.

Add to Visual Studio “Add reference…” dialog

Most of my colleges think that adding assembly to GAC is the only and the last action to makes dll appear in the “Add reference…” dialog. Unfortunately they are wrong. You have to add registry key with the dll name and a path.

Target path is HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\AssemblyFolders. In this path create a new key with the exact assembly name without extension (.dll).  After taking actions you should see something similar like this.

Now, how to make it in code:

public class InstallToRefLibrary {
     public void Install(string assemblyLocation, string dllName) {
         var p = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\AssemblyFolders";
         var key = string.Format("{0}\\{1}", p, dllName);
         var path = Registry.LocalMachine.OpenSubKey(p, true);

         if (path == null) return;

         path.SetValue(key, assemblyLocation);
         path.Close();
    }
}

To uninstall – just delete the key from the registry.

Add to Visual Studio Toolbar

The most obvious and mad things are start here. Most of them because of COM VS origin, some of them appear without any possible logical explanation. Okay, I think, we can start from ridicules restrictions that you can face.

  • If you have an AnkhSVN installed, it can lead to COM error. I’ve tried to install toolbar on Win7 (build 7100) and it gets error until uninstall version control. For the WindowsXP (SP2, SP3) it works fine even with AnkhSVN. You can find a described problem on the Infragistics forum – they are have the same problem with AnkhSVN; and on the ankh forums, but without description why it’s happened and if there any cure. So keep in mind this issue.
  • Toolbar can be installed only if WinForm project exists in current solution. Otherwise only new empty toolbar will appear. Lucky we that it’s rather easy avoid and I’m going to explain how to deal with this.

Before beginning code write you should add project references to

  • EnvDTE;
  • EnvDTE80;
  • EnvDTE90.

Let’s rock!

Install for online mode

I mean that VS has already started and installer runs in the same time. This is kind a simple  example of interaction with studio. For this case I assume that user have a solution with WinForm project. Or what for he want to custom visual components? ;)

First of all we need to somehow define elements that are appears on new toolbar and the toolbar itself. For this information we’ll create help class VSControl. This class will contain information about tab name, assembly path, control’s type. Also we need two constructors: one for a new tab,

public VSControl(string tabGroupName) {
    Control = null;
    AssemblyPath = "";
    TabName = tabGroupName;
    IsToolBoxTab = true;
}

and another one for the element. Looks complicated, but the purpose is easy using in further code.  I think it’s possible to get rid of assemblyPath parameter and use assembly.Location instead.

public VSControl(Assembly assembly, string assemblyPath, string typeName, string tabName) {
    var fullname = assembly.FullName.Split(new[] { ',' })[0];
    Control = assembly.GetType(fullname + "." + typeName);
    AssemblyPath = assemblyPath;
    TabName = tabName;
    IsToolBoxTab = false;
}

The next action is:

  1. Get VS handler;
  2. Get Toolbox handler;
  3. Create new tab;
  4. Create controls.

All of these actions, from my point of view, are better to gather in one class DevEnvironment. This class will have only one public method like RegisterControls with a studio version and controls’ array as parameters.

public class DevEnvironment {
    public static bool RegisterControls(string dteVersion, params VSControl[] controls) { … }
}

In details it will be:

Get visual studio handler

I suggest implementing it as a method that will accept DTE (The top-level object in the Visual Studio automation object model) version and marker that indicates if VS already runs.

(DTE2)Marshal.GetActiveObject("VisualStudio.DTE.9.0")

This code is applicable for the VS2008. If you want to install your toolbox’ tab to the several studios I’d advise you to create enumeration where you can list a possible versions.

[Flags]
public enum DTEVersion {
     None = 0x0000,
     [Description("VisualStudio.DTE.9.0")]
     VS2008 = 0x0001,
     [Description("VisualStudio.DTE.8.0")]
     VS2005 = 0x0002,
     [Description("VisualStudio.DTE.7.0")]
     VS2003 = 0x0004,
     [Description("VisualStudio.DTE.10.0")]
     VS2010 = 0x0008,
}

Using enumeration code will more agile. You can avoid string switches and  RegisterControls also can accept DTEVersion. Within RegisterControls you can write in following way

public static void RegisterControls(DTEVersion dteVersion, params VSControl[] controls) {
    DTE2 dte = null;
    var alreadyCreated = false;

    if ((dteVersion & DTEVersion.VS2008) > DTEVersion.None) {
        dte = (DTE2)Marshal.GetActiveObject("VisualStudio.DTE.9.0");
        // other code
        ...
    }

    if ((dteVersion & DTEVersion.VS2010) > DTEVersion.None) {
        dte = (DTE2)Marshal.GetActiveObject("VisualStudio.DTE.10.0");
        // other code
       ...
    }
}

Strings as parameter in this case looks very strange when we already have enumeration. There is the way how to do it:

internal static string GetEnumDescription(Enum value) {
    var fieldInfo = value.GetType().GetField(value.ToString());
    var attributes = (DescriptionAttribute[])fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), false);
    return (attributes.Length > 0) ? attributes[0].Description : value.ToString();
}

I think from this moment we can refactor

dte = (DTE2)Marshal.GetActiveObject("VisualStudio.DTE.9.0");

to

dte = GetDesignTimeEnvironment(DTEVersion.VS2008, ref alreadyCreated);

where we’ll check some additional stuff. When you getting DTE2, it takes some time befor COM method will be invoked and return expected result. After this we are checking if VS really started by switching to the Property view, for example.

public static DTE2 GetDesignTimeEnvironment(DTEVersion dteVersion, ref bool alreadyCreated) {
    alreadyCreated = false;

    var progID = GetEnumDescription(dteVersion);
    DTE2 result;

    try {
        result = (DTE2)Marshal.GetActiveObject(progID);
        Thread.Sleep(5000);
        try {
             result.ExecuteCommand("View.PropertiesWindow", "");
             alreadyCreated = true;
        }
        catch  {
             result = null;
        }
    }
    catch {   //There is no open VS.Net
        result = null;
    }

    return result;
}

Check results on null and if negative we can proceed to creating toolbar.

Get Toolbox handler

You will be surprised, but it is very easy! Just 2 rows of code.

var toolbox = dte.Windows.Item(Constants.vsWindowKindToolbox);
var tabs = ((ToolBox)toolbox.Object).ToolBoxTabs;

Create new toolbar’s tab

As soon as all new controls (and tab also) are wrapped in VSControl class – new method should accept array of them. Search for tab item in the array and create VS’ tab.

internal static void RegisterControls(DTE2 dte, bool alreadyCreatedDTE, VSControl[] controls) {
    var toolbox = dte.Windows.Item(Constants.vsWindowKindToolbox);
    var tabs = ((ToolBox)toolbox.Object).ToolBoxTabs;
    controls
        .ToList()
        .FindAll(i => i.IsToolBoxTab)
        .ForEach(i => {
                 if (GetToolBoxTab(tabs, i.TabName) == null)
                 tabs.Add(i.TabName);
                 });
}

private static ToolBoxTab3 GetToolBoxTab(ToolBoxTabs tabs, string tabName) {
    foreach (ToolBoxTab3 tab in tabs) {
        if (smenglish.CompareInfo.Compare(tab.Name, tabName,  CompareOptions.IgnoreCase) == 0)
            return tab;
    }

    return null;
}

And the class’ field:

internal static CultureInfo smenglish = CultureInfo.CreateSpecificCulture("en");

Create new controls

Now we have tabs and it’s time to add controls. For this procedure we should activate proper tab and create new item. It’s not hard. Add this code after the tab creation.

foreach (var control in controls) {
    var tab = GetToolBoxTab(tabs, control.TabName);

    if (tab != null && !control.IsToolBoxTab) {
        tab.Activate();
        tab.ToolBoxItems.Add("anyName", control.Control, vsToolBoxItemFormat.vsToolBoxItemFormatDotNETComponent);
    }
}

There is small nuance: when you add new item by code tab.ToolBoxItems.Add the second parameter is object. If you pass String it treats as an assembly path and all public components will shown. If you pass Type, only this component will be added.

When a program have been done with adding components to the tabs, you should close connection to DTE.

dte.Quit();

That’s all for “online” case.

Install for offline mode

I think you won’t be glad to ask users to launch Visual Studio and create WinForm project before they run installer. And there is a way how to make it automatically.

First of all we have to create new instance of VS to operate with it. I think that the best place for this code is GetDesignTimeEnvironment function.

if (result == null) {
    //Open a new VS.Net
    var type = Type.GetTypeFromProgID(progID);

    result = (DTE2) Activator.CreateInstance(type, true);
}

As I said before, you have to create WinForm project in order to create new items on toolbox’ tab. And it’s possible to create by code also. Open method RegisterControls and add to the beginning

if (!alreadyCreatedDTE) {
     var tmpFile = Path.GetFileNameWithoutExtension(Path.GetTempFileName());
     var tmpDir = string.Format("{0}{1}", Path.GetTempPath(), tmpFile);
     var solution = dte.Solution as Solution2;
     var templatePath = solution.GetProjectTemplate("WindowsApplication.zip", "CSharp");

     solution.AddFromTemplate(templatePath, tmpDir, "dummyproj", false);
}

From my point of view this code is selfdocumenting and no comments are needed. Don’t forget close solution before end of installation.

dte.Solution.Close(false);

Parameters is for question to save or not changes.

Now controls will be installed to VS no matter is it launched or not. Unfortunatelly COM errors may ruin you happynes. During development I got a lot of errors that says something like COM component is still busy and can’t be called. Digging inet gives me following solution.

Avoid COM errors

The main idea is to filter messages from COM objects and get only those on which we’ve been subscribed.

You need to declare interface for the IOleMessageFilter

[ComImport, Guid("00000016-0000-0000-C000-000000000046"),
InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IOleMessageFilter {
    [PreserveSig]
    int HandleInComingCall(
            int dwCallType,
            IntPtr hTaskCaller,
            int dwTickCount,
            IntPtr lpInterfaceInfo);

    [PreserveSig]
    int RetryRejectedCall(
            IntPtr hTaskCallee,
            int dwTickCount,
            int dwRejectType);

    [PreserveSig]
    int MessagePending(
           IntPtr hTaskCallee,
           int dwTickCount,
           int dwPendingType);
}

The next is realisation.

public class MessageFilter : IOleMessageFilter {
    //
    // Class containing the IOleMessageFilter
    // thread error-handling functions.
    // Start the filter.

    public static void Register() {
        IOleMessageFilter newFilter = new MessageFilter();
        IOleMessageFilter oldFilter = null;
        CoRegisterMessageFilter(newFilter, out oldFilter);
    }

    // Done with the filter, close it.

    public static void Revoke() {
        IOleMessageFilter oldFilter = null;
        CoRegisterMessageFilter(null, out oldFilter);
    }

    //
    // IOleMessageFilter functions.
    // Handle incoming thread requests.
    int IOleMessageFilter.HandleInComingCall(int dwCallType, IntPtr hTaskCaller, int dwTickCount, IntPtr lpInterfaceInfo) {
        //Return the flag SERVERCALL_ISHANDLED.
        return 0;
    }

    // Thread call was rejected, so try again.
    int IOleMessageFilter.RetryRejectedCall(IntPtr hTaskCallee, int dwTickCount, int dwRejectType) {
        if (dwRejectType == 2) {
            // flag = SERVERCALL_RETRYLATER.
            // Retry the thread call immediately if return >=0 &
            // <100.
            return 99;
        }

       // Too busy; cancel call.
       return -1;
    }

    int IOleMessageFilter.MessagePending(IntPtr hTaskCallee, int dwTickCount, int dwPendingType) {
        //Return the flag PENDINGMSG_WAITDEFPROCESS.
        return 2;
    }

    // Implement the IOleMessageFilter interface.
    [DllImport("Ole32.dll")]
    private static extern int
         CoRegisterMessageFilter(IOleMessageFilter newFilter, out IOleMessageFilter oldFilter);
    }

Finally you should get like:

MessageFilter.Register();
RegisterControls(dte, alreadyCreated, controls);
MessageFilter.Revoke();

With this code everything shold be cool! =)

I’ve tested it on Win XP, Win 7. Everything finally works very well. I hope to see you comments, question and suggestions. Yes, during writing this post I note some areas for improvement in code, but I left them for future times (they are not critical and I can live with them).

Source code

Hard’n’heavy!

Tagged . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>