Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Using the Surface Dial as a Debug Tool

DZone's Guide to

Using the Surface Dial as a Debug Tool

The Surface Dial is a nifty little device, it feels great and can add some nice capabilities to your apps. However, the very best thing about it is that is has an API. The author had a go with it and decided to try and turn the dial into a debugging device.

· Web Dev Zone
Free Resource

Tips, tricks and tools for creating your own data-driven app, brought to you in partnership with Qlik.

Here’s a small video of the extension in action:

The Surface Dial is a nifty little device, it feels great and can add some nice capabilities to your apps. However, the very best thing about it is that is has an API. I had a go with it and decided to try and turn the dial into a debugging device.

The Goal

The Dial has a few gestures. It’s a button that you can press, and it rotates clockwise and counter-clockwise. My goal was to start a debugging session by clicking the Dial, Step-Over (F10) by rotating clockwise and Step-Into (F11) by rotating counter-clockwise.

Pre-requisites

We’ll need some stuff to get going:

Setting up a Visual Studio Extension

To start creating a Visual Studio Extension, launch Visual Studio 2015 and start a new project. If the SDK is installed correctly there should be an Extensibility option with a VSIX Project template.

The project it creates is basically an empty box, just some boilerplate code to have a VSIX installer and have it install your extension in VS. To start adding some functionality to the extension we need to add an extra item, in this case, it will be a package, these are added as a new item into the project.

Almost ready, all we need to add next is when our extension should load. We’re going to choose for load on solution load in this sample. This is done by adding an attribute on the package class.

[ProvideAutoLoad(UIContextGuids80.SolutionExists)]

Thrown together, we get:

[PackageRegistration(UseManagedResourcesOnly = true)]
[InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)] // Info on this package for Help/About
[Guid(DialDebugPackage.PackageGuidString)]
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1650:ElementDocumentationMustBeSpelledCorrectly", Justification = "pkgdef, VS and vsixmanifest are valid VS terms")]
[ProvideAutoLoad(UIContextGuids80.SolutionExists)]
[ProvideMenuResource("Menus.ctmenu", 1)]
public sealed class DialDebugPackage : Package
{
…

If you press F5 now, an experimental instance of Visual Studio should launch, with the extension installed.

Connecting to the Dial

This was the hardest part of the project, the Dial API is done in UWP. This is how we can grab the Dial instance in UWP (currently only one dial is supported per system)

var controller = RadialController.CreateForCurrentView();

The problem here is not the fact that it’s a UWP API, we can use those from win32. The problem is that the CreateForCurrentView() method requires a handle to the current window, a UWP window, which we obviously don’t have. I found the solution for this in one of the official Microsoft samples. They have a sample in the windows-classic-samples repository that shows how to access the Dial from a WinForms application.

I took the RadialControllerInterop class from that sample and added it to the extension. however, as it turned out, it was only part of the solution.

[System.Runtime.InteropServices.Guid("1B0535C9-57AD-45C1-9D79-AD5C34360513")]
[System.Runtime.InteropServices.InterfaceType(System.Runtime.InteropServices.ComInterfaceType.InterfaceIsIInspectable)]
public interface IRadialControllerInterop
{     RadialController CreateForWindow(     IntPtr hwnd,     [System.Runtime.InteropServices.In]ref Guid riid);
}
 
[System.Runtime.InteropServices.Guid("787cdaac-3186-476d-87e4-b9374a7b9970")]
[System.Runtime.InteropServices.InterfaceType(System.Runtime.InteropServices.ComInterfaceType.InterfaceIsIInspectable)]
public interface IRadialControllerConfigurationInterop
{     RadialControllerConfiguration GetForWindow(     IntPtr hwnd,     [System.Runtime.InteropServices.In]ref Guid riid);
}

The interop interfaces actually reference the RadialController class that lives in Windows.UI.Input, a UWP namespace that we currently have no reference to. That’s where the UwpDesktop NuGet package comes in. This package makes it easy to reference UWP classes from win32 style applications. Once added through NuGet we can add the Windows.UI.Input namespace to the interop class.

using System;
using Windows.UI.Input;

namespace DialDebug
{
    [System.Runtime.InteropServices.Guid("1B0535C9-57AD-45C1-9D79-AD5C34360513")]
    [System.Runtime.InteropServices.InterfaceType(System.Runtime.InteropServices.ComInterfaceType.InterfaceIsIInspectable)]
    public interface IRadialControllerInterop
    {
        RadialController CreateForWindow(
        IntPtr hwnd,
        [System.Runtime.InteropServices.In]ref Guid riid);
    }

    [System.Runtime.InteropServices.Guid("787cdaac-3186-476d-87e4-b9374a7b9970")]
    [System.Runtime.InteropServices.InterfaceType(System.Runtime.InteropServices.ComInterfaceType.InterfaceIsIInspectable)]
    public interface IRadialControllerConfigurationInterop
    {
        RadialControllerConfiguration GetForWindow(
        IntPtr hwnd,
        [System.Runtime.InteropServices.In]ref Guid riid);
    }
}

We’ve got everything in place to connect to the Dial from Visual Studio.

Gluing It Together

Back to the package class, there should be an Initialize method, overridden from the base class. This is where we’ll hook everything up.


protected override void Initialize()
{
    base.Initialize();

    _dte = GetService(typeof(DTE)) as DTE;

    if (_dte == null)
    {
        throw new NullReferenceException("DTE is null");
    }

    CreateController();
    CreateMenuItem();
    HookUpEvents();
}

The first thing we’ll do is grab hold of the current Visual Studio instance, which is a DTE object. We keep a reference to this object in a field because we’ll need it later.

Next up is connecting to the Dial

private void CreateController()
{
    IRadialControllerInterop interop = (IRadialControllerInterop)System.Runtime.InteropServices.WindowsRuntime.WindowsRuntimeMarshal.GetActivationFactory(typeof(RadialController));
    Guid guid = typeof(RadialController).GetInterface("IRadialController").GUID;

    _radialController = interop.CreateForWindow(new IntPtr(_dte.ActiveWindow.HWnd), ref guid);
}

In this method we’re using interop to get access to the UWP RadialController class via the interface we took from the classic windows samples. As a handle for the windows we pass in the handle for our current Visual Studio instance which we can get to from the DTE object. At this point we got a reference to the Dial that is connected to our system.

The Dial has a radial menu that we can hook into and add or remove items from it. We’ll add a Debug item to it.

private void CreateMenuItem()
{
    _menuItems = new List<RadialControllerMenuItem>
    {
        RadialControllerMenuItem.CreateFromKnownIcon("Debug", RadialControllerMenuKnownIcon.InkColor),
    };

    foreach (var item in _menuItems)
    {
        _radialController.Menu.Items.Add(item);
    }
}

I’m doing this through a list just in case I ever want to add more items, but at the moment I’m only adding one. A RadialControllerMenuItem is created via the CreateFromKnownIcon method. First parameter is the text that will be displayed in the menu, the second one is an enum value from an enum that contains some known menu icons. A RadialController has a menu property that has a collection of items, all we need to do is add our items to that collection and it will show up in the radial menu.

Final step is adding the event handlers and calling the debugger methods to let the Dial step through the code.

private void HookUpEvents()
{
    _radialController.RotationChanged += OnRotationChanged;
    _radialController.ButtonClicked += OnButtonClicked;

    _dte.Events.SolutionEvents.AfterClosing += () =>
    {
        _radialController.Menu.Items.Clear();
    };
}

As you can see, the events coming from the dial are very straightforward, there is a ButtonClicked and a RotationChanged event.

Here’s what we need to do to start debugging on button click:

private void OnButtonClicked(RadialController sender, RadialControllerButtonClickedEventArgs args)
{
    if (_dte.Application.Debugger.CurrentMode == dbgDebugMode.dbgRunMode)
    {
        _dte.Application.Debugger.Stop();
    }
    else
    {
        _dte.Application.Debugger.Go();
    }
}

The Visual Studio SDK provides an easy to use API surface. We can use the DTE.Application.Debugger class to control the debugger. The Go() method launches a debug session, the Stop() method, well, stops a debugging session.

Next, we’ll handle the rotation events


private void OnRotationChanged(RadialController sender, RadialControllerRotationChangedEventArgs args)
{
    if (args.RotationDeltaInDegrees > 0)
    {
        _dte.Application.Debugger.StepOver();
    }
    else
    {
        _dte.Application.Debugger.StepInto();
    }
}

From the args we get the rotation delta, one step on the rotator is a delta of 10, +10 clockwise and –10 counter-clockwise.

And that’s it, build it in release mode and the bin/release folder will contain a .vsix installer file. Close VS, install the extension, reopen VS, open a solution, select Debug from the radial menu and debug away!

The source code is on GitHub. Here’s a link to the compiled VSIX file.

Disclaimer: the code is provided as-is. I do not plan to publish this on the gallery or maintain the project. Do feel free to pick this up and create an open-source project from it (would be nice to include a reference to this post in the description )

Happy coding!

Explore data-driven apps with less coding and query writing, brought to you in partnership with Qlik.

Topics:
radial ,instance ,api ,visual studio ,web dev

Published at DZone with permission of Nico Vermeir, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}