Kirk Evans Blog

.NET From a Markup Perspective

Blynclight for Developers–Daddy is Working Light

This post will show you how to code a custom Visual Studio VSIX add-in that will control a USB light, turning the light the color of your choice when Visual Studio is in the foreground or actively debugging. 

The code is available for download at https://github.com/kaevans/BlyncLightAddin

Background

A few years ago, Andrew Connell and Scott Hanselman posted about getting a USB presence indicator for Lync (Lync + BusyLight = Great Solution for the Home Office, Is Daddy on a call? A BusyLight Presence indicator for Lync for my Home Office).  I received a Blynclight as a speaker gift for speaking at a conference a few years ago and it’s just sat on my desk, turning various colors as my Skype for Business presence changes. 

Blynclight Standard

I am on the phone in meetings, so this does come in handy.  I started thinking that this could be really useful to tell someone I am currently coding and don’t wish to be disturbed as well.  If I am debugging, do not disturb!  Hence, the BlyncLightAddIn, where you can control what color should be shown on the Blynclight when Visual Studio starts and what color should be shown when debugging. 

The code is available for download at https://github.com/kaevans/BlyncLightAddin

Download the SDK

I downloaded the Blynclight SDK from the Blynclight Developer Forum (http://blynclight.proboards.com/thread/2/blync-sdk-create-own-applications).  It required registration in order to download the SDK, but once you download it you can see the binaries, a sample application, and a Word doc that contains information on the API. 

image

Next, I created a Visual Studio 2015 Extensibility project.  I didn’t have the extensibility tools installed, so I was prompted to install the Visual Studio SDK.  Took awhile, but finally I had a few new project template options in Visual Studio. 

Create a Custom Tool Window

Once you have the extensibility tools installed (Visual Studio prompts you to install these if you haven’t already), create a new VSIX project in Visual Studio.

image

Right-click the project and choose Add / New Item, then add a new Custom Tool Window.

image

Visual Studio generates a bunch of files, including a Command class that provides a menu command item to open the custom tool window and a WPF control to define your custom tool window.  It also generates a file, *Package.cs, that loads all the stuff for your add-in.  This is where you set the default window position for your custom tool window.

Set Default Window Pos
  1. [PackageRegistration(UseManagedResourcesOnly = true)]
  2. [InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)] // Info on this package for Help/About
  3. [ProvideMenuResource("Menus.ctmenu", 1)]
  4. [ProvideToolWindow(typeof(BlyncLight), Style = Microsoft.VisualStudio.Shell.VsDockStyle.Tabbed, Window = "3ae79031-e1bc-11d0-8f78-00a0c9110057")]
  5. [Guid(BlyncLightPackage.PackageGuidString)]
  6. [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1650:ElementDocumentationMustBeSpelledCorrectly", Justification = "pkgdef, VS and vsixmanifest are valid VS terms")]
  7. public sealed class BlyncLightPackage : Package
  8. {

Notice line 4 where the custom tool window is loaded, we provide the style as tabbed, meaning it will be a tab in an existing window, and the window is docked to the Solution Explorer window (the GUID value is the GUID of the Solution Explorer).  To see where I stole this code from, see https://msdn.microsoft.com/en-us/library/cc138567.aspx

If you hit F5 right now, you have a functional yet boring addin.  Go to the View / Other Windows menu item to load your custom tool window.

image

Your custom tool window is opened, docked to the Solution Explorer window.

image

Add Blynclight Functionality

Now that the shell of our add-in is working, we can create our custom tool window.  You previously downloaded the SDK which includes Blynclight.dll.  This file is not signed, so you won’t be able to load it.  However, there is a workaround as shown in the post Sign a .NET Assembly with a Strong Name Without Recompiling.  You basically create a strong-named key using sn.exe, disassemble the DLL to intermediate language (IL) using ILDASM.exe, then assemble the IL into a DLL including your strong-named key using ILASM.exe.

image

Following these steps, I then add a reference in Visual Studio to the signed assembly.

image

We can now program against the Blynclight!

Is Visual Studio the Active Window?

If I have Visual Studio open, I usually don’t want to be disturbed.  We can cheat this by registering for a callback any time the active window is changed.  Once changed, we can look at the title of the window and decide if we want to change the light’s color or not. Registering the callback is very simple, especially when someone on Stack Overflow has done the hard lifting for us!  See the post Detect active window changed using C# without polling.  I’ll be the first to admit, this is an amateurish approach to just look for a hard-coded string “Microsoft Visual Studio”… but that’s the beauty of open source, right?  Just fork the repo and implement the rest that I was too lazy to finish.

ActiveWindowWatcher
  1. using System;
  2. using System.Runtime.InteropServices;
  3. using System.Text;
  4.  
  5. namespace BlyncLightAddin
  6. {
  7.     public class ActiveWindowWatcher : IBlyncWatcher
  8.     {        
  9.         public event EventHandler StatusChanged;
  10.         private bool isPaused;
  11.         private const uint WINEVENT_OUTOFCONTEXT = 0;
  12.         private const uint EVENT_SYSTEM_FOREGROUND = 3;
  13.         IntPtr m_hhook;
  14.         WinEventDelegate dele = null;
  15.  
  16.         delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);
  17.  
  18.         [DllImport("user32.dll")]
  19.         static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc, WinEventDelegate lpfnWinEventProc, uint idProcess, uint idThread, uint dwFlags);
  20.  
  21.         [DllImport("user32.dll")]
  22.         static extern bool UnhookWinEvent(IntPtr hWinEventHook);
  23.  
  24.         [DllImport("user32.dll")]
  25.         static extern IntPtr GetForegroundWindow();
  26.  
  27.         [DllImport("user32.dll")]
  28.         static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);
  29.  
  30.         private string GetActiveWindowTitle()
  31.         {
  32.             const int nChars = 256;
  33.             IntPtr handle = IntPtr.Zero;
  34.             StringBuilder Buff = new StringBuilder(nChars);
  35.             handle = GetForegroundWindow();
  36.  
  37.             if (GetWindowText(handle, Buff, nChars) > 0)
  38.             {
  39.                 return Buff.ToString();
  40.             }
  41.             return null;
  42.         }
  43.  
  44.         public void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
  45.         {
  46.             var text = GetActiveWindowTitle();
  47.             if (string.IsNullOrEmpty(text))
  48.             {
  49.                 OnStatusChanged(new StatusChangedEventArgs(Status.Default));
  50.             }
  51.             else
  52.             {
  53.                 if (text.Contains("Microsoft Visual Studio"))
  54.                 {
  55.                     //Raise event
  56.                     OnStatusChanged(new StatusChangedEventArgs(Status.Busy));
  57.                 }
  58.                 else
  59.                 {
  60.                     OnStatusChanged(new StatusChangedEventArgs(Status.Available));
  61.                 }
  62.             }
  63.         }
  64.         public void Initialize()
  65.         {
  66.             dele = new WinEventDelegate(WinEventProc);//<-causing ERROR
  67.             m_hhook = SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, IntPtr.Zero, dele, 0, 0, WINEVENT_OUTOFCONTEXT);
  68.         }
  69.  
  70.         public void Close()
  71.         {
  72.             if (m_hhook.ToInt32() != 0)
  73.             {
  74.                 UnhookWinEvent(m_hhook);
  75.             }
  76.         }
  77.         
  78.         protected virtual void OnStatusChanged(StatusChangedEventArgs e)
  79.         {            
  80.             if (StatusChanged != null && isPaused == false)
  81.             {
  82.                 StatusChanged(this, e);
  83.             }
  84.         }
  85.  
  86.         public void Pause()
  87.         {
  88.             isPaused = true;
  89.         }
  90.  
  91.         public void Resume()
  92.         {
  93.             isPaused = false;
  94.         }
  95.     }
  96. }


Is Visual Studio Debugging?

This one was a little harder to solve, but turned out so much easier to implement.  I wanted to know if Visual Studio was actively debugging something.  If it is, then changing windows doesn’t matter (debugging usually means that new windows are popping up all over the place), just show the status as busy. 

DebuggerWatcher
  1. using Microsoft.VisualStudio.Shell.Interop;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. using System.Text;
  6. using System.Threading.Tasks;
  7.  
  8. namespace BlyncLightAddin
  9. {
  10.     public class DebuggerWatcher : IBlyncWatcher, IVsDebuggerEvents
  11.     {
  12.         public event EventHandler StatusChanged;
  13.         private uint cookie = default(uint);
  14.         private bool isPaused;
  15.  
  16.  
  17.         public void Initialize()
  18.         {
  19.             IVsDebugger debugService = Microsoft.VisualStudio.Shell.Package.GetGlobalService(typeof(SVsShellDebugger)) as IVsDebugger;
  20.             if (debugService != null)
  21.             {
  22.                 // Register for debug events.
  23.                 // Assumes the current class implements IDebugEventCallback2.
  24.  
  25.                 debugService.AdviseDebuggerEvents(this, out cookie);
  26.             }
  27.         }
  28.  
  29.         public int OnModeChange(DBGMODE dbgmodeNew)
  30.         {
  31.             switch (dbgmodeNew)
  32.             {
  33.                 case DBGMODE.DBGMODE_Break:
  34.                 case DBGMODE.DBGMODE_Run:
  35.                 case DBGMODE.DBGMODE_Enc:
  36.                 case DBGMODE.DBGMODE_EncMask:
  37.                     //Debugging                   
  38.                     OnStatusChanged(new StatusChangedEventArgs(Status.Busy));
  39.                     break;
  40.                 case DBGMODE.DBGMODE_Design:
  41.                     //Debugger detached
  42.                     OnStatusChanged(new StatusChangedEventArgs(Status.Available));
  43.                     break;
  44.                 default:
  45.                     OnStatusChanged(new StatusChangedEventArgs(Status.Default));
  46.                     break;
  47.             }
  48.             return (int)dbgmodeNew;
  49.         }
  50.  
  51.         protected virtual void OnStatusChanged(StatusChangedEventArgs e)
  52.         {
  53.             if (StatusChanged != null && isPaused == false)
  54.             {
  55.                 StatusChanged(this, e);
  56.             }
  57.         }
  58.         public void Close()
  59.         {
  60.             IVsDebugger debugService = Microsoft.VisualStudio.Shell.Package.GetGlobalService(typeof(SVsShellDebugger)) as IVsDebugger;
  61.             if (debugService != null)
  62.             {
  63.                 // Unegister for debug events.                
  64.                 debugService.UnadviseDebuggerEvents(cookie);
  65.             }
  66.         }
  67.  
  68.         public void Pause()
  69.         {
  70.             isPaused = true;
  71.         }
  72.  
  73.         public void Resume()
  74.         {
  75.             isPaused = false;
  76.         }
  77.     }
  78. }


Using the IBlyncWatcher Interface

Using our two implementations above is really simple.  I defined the interface, IBlyncWatcher, that both implement.

IBlyncWatcher
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6.  
  7. namespace BlyncLightAddin
  8. {
  9.     interface IBlyncWatcher
  10.     {
  11.         void Initialize();
  12.  
  13.         void Pause();
  14.  
  15.         void Resume();
  16.  
  17.         void Close();
  18.  
  19.         event EventHandler StatusChanged;
  20.     }
  21. }

In the BlyncLightControl.xaml.cs file, you can see how easy it is to use the interface and the concrete classes.

BlyncLightControl
  1. //——————————————————————————
  2. // <copyright file="BlyncLightControl.xaml.cs" company="Company">
  3. //     Copyright (c) Company.  All rights reserved.
  4. // </copyright>
  5. //——————————————————————————
  6.  
  7. namespace BlyncLightAddin
  8. {
  9.     using System;
  10.     using System.Diagnostics.CodeAnalysis;
  11.     using System.Windows;
  12.     using System.Windows.Controls;
  13.     using System.Runtime.InteropServices;
  14.     using System.Text;
  15.     using Microsoft.VisualStudio.Shell.Interop;
  16.  
  17.     /// <summary>
  18.     /// Interaction logic for BlyncLightControl.
  19.     /// </summary>
  20.     public partial class BlyncLightControl : UserControl
  21.     {
  22.         IBlyncWatcher debugWatcher;
  23.         IBlyncWatcher activeWindowWatcher;
  24.  
  25.         /// <summary>
  26.         /// Initializes a new instance of the <see cref="BlyncLightControl"/> class.
  27.         /// </summary>
  28.         public BlyncLightControl()
  29.         {
  30.             this.InitializeComponent();
  31.             debugWatcher = new DebuggerWatcher();
  32.             debugWatcher.StatusChanged += DebugWatcher_StatusChanged;
  33.             debugWatcher.Initialize();
  34.  
  35.             activeWindowWatcher = new ActiveWindowWatcher();
  36.             activeWindowWatcher.StatusChanged += ActiveWindowWatcher_StatusChanged;
  37.             activeWindowWatcher.Initialize();
  38.         }
  39.  
  40.         private void ActiveWindowWatcher_StatusChanged(object sender, EventArgs e)
  41.         {
  42.             var es = e as StatusChangedEventArgs;
  43.  
  44.             switch (es.Status)
  45.             {
  46.                 case Status.Default:
  47.                     ChangeColor(Blynclight.BlynclightController.Color.Yellow);
  48.                     break;
  49.                     
  50.                 case Status.Busy:
  51.                     ChangeColor(Blynclight.BlynclightController.Color.Red);
  52.                     break;
  53.                 case Status.Available:
  54.                     ChangeColor(Blynclight.BlynclightController.Color.Green);
  55.                     break;
  56.                 default:
  57.                     break;
  58.             }
  59.         }
  60.  
  61.         private void DebugWatcher_StatusChanged(object sender, EventArgs e)
  62.         {
  63.             var es = e as StatusChangedEventArgs;
  64.  
  65.             switch (es.Status)
  66.             {
  67.                 case Status.Default:
  68.                     ChangeColor(Blynclight.BlynclightController.Color.Yellow);
  69.                     break;
  70.  
  71.                 case Status.Busy:
  72.                     activeWindowWatcher.Pause();
  73.                     ChangeColor(Blynclight.BlynclightController.Color.Red);
  74.                     break;
  75.                 case Status.Available:
  76.                     activeWindowWatcher.Resume();
  77.                     ChangeColor(Blynclight.BlynclightController.Color.Green);
  78.                     break;
  79.                 default:
  80.                     break;
  81.             }
  82.         }
  83.  
  84.         ~BlyncLightControl()
  85.         {
  86.             debugWatcher.Close();
  87.             debugWatcher.StatusChanged -= DebugWatcher_StatusChanged;
  88.             activeWindowWatcher.Close();
  89.             activeWindowWatcher.StatusChanged -= ActiveWindowWatcher_StatusChanged;
  90.         }
  91.  
  92.  
  93.         /// <summary>
  94.         /// Handles click on the button by displaying a message box.
  95.         /// </summary>
  96.         /// <param name="sender">The event sender.</param>
  97.         /// <param name="e">The event args.</param>
  98.         [SuppressMessage("Microsoft.Globalization", "CA1300:SpecifyMessageBoxOptions", Justification = "Sample code")]
  99.         [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:ElementMustBeginWithUpperCaseLetter", Justification = "Default event handler naming pattern")]
  100.         private void button1_Click(object sender, RoutedEventArgs e)
  101.         {
  102.             ChangeColor(Blynclight.BlynclightController.Color.Blue);
  103.         }
  104.  
  105.         private void ChangeColor(Blynclight.BlynclightController.Color color)
  106.         {
  107.             Blynclight.BlynclightController controller = new Blynclight.BlynclightController();
  108.             int numDevices = 0;
  109.             try
  110.             {
  111.                 numDevices = controller.InitBlyncDevices();
  112.                 if (numDevices > 0)
  113.                 {
  114.                     switch (color)
  115.                     {
  116.                         case Blynclight.BlynclightController.Color.Blue:
  117.                             controller.TurnOnBlueLight(0);
  118.                             break;
  119.                         case Blynclight.BlynclightController.Color.Cyan:
  120.                             controller.TurnOnCyanLight(0);
  121.                             break;
  122.                         case Blynclight.BlynclightController.Color.Green:
  123.                             controller.TurnOnGreenLight(0);
  124.                             break;
  125.                         case Blynclight.BlynclightController.Color.Off:
  126.                             controller.ResetLight(0);
  127.                             break;
  128.                         case Blynclight.BlynclightController.Color.Purple:
  129.                             controller.TurnOnMagentaLight(0);
  130.                             break;
  131.                         case Blynclight.BlynclightController.Color.Red:
  132.                             controller.TurnOnRedLight(0);
  133.                             break;
  134.                         case Blynclight.BlynclightController.Color.White:
  135.                             controller.TurnOnWhiteLight(0);
  136.                             break;
  137.                         case Blynclight.BlynclightController.Color.Yellow:
  138.                             controller.TurnOnYellowLight(0);
  139.                             break;
  140.                         case Blynclight.BlynclightController.Color.Orange:
  141.                             controller.TurnOnOrangeLight(0);
  142.                             break;
  143.                         default:
  144.                             controller.ResetLight(0);
  145.                             break;
  146.                     }
  147.                 }
  148.             }
  149.             catch (Exception oops)
  150.             {
  151.  
  152.             }
  153.             finally
  154.             {
  155.                 controller.CloseDevices(numDevices);
  156.             }
  157.         }
  158.     }
  159. }

Again, I could have made this more user-driven, even provided a UI for the developer to choose what color happens when, if the light blinks or not, and even support multiple devices.  The Blynclight SDK comes with a sample called BlyncLightTest that is a Windows Forms solution that you can grab some inspiration from. 

image

Summary

This was cathartic for me.  I really just wanted to know how to code a VSIX extension and to spend some time in Visual Studio doing something other than Azure.  I coded this in about a day (gotta love editor inheritance from Stack Overflow). 

The code is available for download at https://github.com/kaevans/BlyncLightAddin

For More Information

Adding a Tool Window

Lync + BusyLight = Great Solution for the Home Office

Is Daddy on a call? A BusyLight Presence indicator for Lync for my Home Office

Blync SDK – Create Your Own Blync Applications (requires free registration)

Sign a .NET Assembly with a Strong Name Without Recompiling

Detect active window changed using C# without polling

The code is available for download at https://github.com/kaevans/BlyncLightAddin