Many device-oriented apps (bluetooth, MIDI, etc.) require you to
restart the app when you want to detect a new device. I've seen
this both in Windows Store/Universal apps, and even in big desktop
apps. Instead, these apps should detect changes during runtime, and
respond gracefully.
Example:
You load up a synthesizer app. After it loads, you realize you
forgot to plug in your keyboard controller. You then plug in the
controller, but the app doesn't do anything.
Why does that happen? It happens because the app enumerates the
devices at startup, but doesn't register for change notifications
using the DeviceWatcher. The app likely enumerates devices using
code similar to this:
var selector = MidiInPort.GetDeviceSelector();
var devices = await DeviceInformation.FindAllAsync(selector);
if (devices != null && devices.Count > 0)
{
// MIDI devices returned
foreach (var device in devices)
{
list.Items.Add(device);
}
}
That's a nice and simply approach to listing devices, but it
doesn't allow for change notification.
The rest of this post will show how to enumerate devices (using
MIDI devices and the preview WinRT MIDI API on NuGet) and register
to receive change notifications. The project will eventually be a
test project for my Novation Launchpads, but for this post, we'll
focus only on the specific problem of enumeration and change
notification.
A custom device metadata class
First, rather than use the DeviceInformation class directly,
I've used my own metadata class. This provides more flexibility for
the future by decreasing reliance on the built-in class. It's also
lighter weight, holding only the information we care about.
namespace LaunchpadTest.Devices
{
class MidiDeviceInformation
{
public string Id { get; set; }
public string Name { get; set; }
public bool IsDefault { get; set; }
public bool IsEnabled { get; set; }
}
}
With that out of the way, let's look at the real code.
The MIDI class
Next, we'll create a class for interfacing with the MIDI device
information in Windows. I'll list the entire source here and refer
to it in the description that follows
using System;
using System.Collections.Generic;
using System.Linq;
using WindowsPreview.Devices.Midi;
using Windows.Devices.Enumeration;
namespace LaunchpadTest.Devices
{
class Midi
{
public List<MidiDeviceInformation> ConnectedInputDevices { get; private set; }
public List<MidiDeviceInformation> ConnectedOutputDevices { get; private set; }
private DeviceWatcher _inputWatcher;
private DeviceWatcher _outputWatcher;
public event EventHandler InputDevicesEnumerated;
public event EventHandler OutputDevicesEnumerated;
public event EventHandler InputDevicesChanged;
public event EventHandler OutputDevicesChanged;
// using an Initialize method here instead of the constructor in order to
// prevent a race condition between wiring up the event handlers and
// finishing enumeration
public void Initialize()
{
ConnectedInputDevices = new List<MidiDeviceInformation>();
ConnectedOutputDevices = new List<MidiDeviceInformation>();
// set up watchers so we know when input devices are added or removed
_inputWatcher = DeviceInformation.CreateWatcher(MidiInPort.GetDeviceSelector());
_inputWatcher.EnumerationCompleted += InputWatcher_EnumerationCompleted;
_inputWatcher.Updated += InputWatcher_Updated;
_inputWatcher.Removed += InputWatcher_Removed;
_inputWatcher.Added += InputWatcher_Added;
_inputWatcher.Start();
// set up watcher so we know when output devices are added or removed
_outputWatcher = DeviceInformation.CreateWatcher(MidiOutPort.GetDeviceSelector());
_outputWatcher.EnumerationCompleted += OutputWatcher_EnumerationCompleted;
_outputWatcher.Updated += OutputWatcher_Updated;
_outputWatcher.Removed += OutputWatcher_Removed;
_outputWatcher.Added += OutputWatcher_Added;
_outputWatcher.Start();
}
private void OutputWatcher_EnumerationCompleted(DeviceWatcher sender, object args)
{
// let other classes know enumeration is complete
if (OutputDevicesEnumerated != null)
OutputDevicesEnumerated(this, new EventArgs());
}
private void OutputWatcher_Updated(DeviceWatcher sender, DeviceInformationUpdate args)
{
// this is where you capture changes to a specific ID
// you could change this to be more specific and pass the changed ID
if (OutputDevicesChanged != null)
OutputDevicesChanged(this, new EventArgs());
}
private void OutputWatcher_Removed(DeviceWatcher sender, DeviceInformationUpdate args)
{
// remove from our collection the item with the specified ID
var id = args.Id;
var toRemove = (from MidiDeviceInformation mdi in ConnectedOutputDevices
where mdi.Id == id
select mdi).FirstOrDefault();
if (toRemove != null)
{
ConnectedOutputDevices.Remove(toRemove);
// notify clients
if (OutputDevicesChanged != null)
OutputDevicesChanged(this, new EventArgs());
}
}
private void OutputWatcher_Added(DeviceWatcher sender, DeviceInformation args)
{
var id = args.Id;
// you could use DeviceInformation directly here, using the
// CreateFromIdAsync method. However, that is an async method
// and so adds a bit of delay. I'm using a trimmed down object
// to hold MIDI information rather than using the DeviceInformation class
#if DEBUG
// this is so you can see what the properties contain
foreach (var p in args.Properties.Keys)
{
System.Diagnostics.Debug.WriteLine("Output: " + args.Name + " : " + p + " : " + args.Properties[p]);
}
#endif
var info = new MidiDeviceInformation();
info.Id = id;
info.Name = args.Name;
info.IsDefault = args.IsDefault;
info.IsEnabled = args.IsEnabled;
ConnectedOutputDevices.Add(info);
// notify clients
if (OutputDevicesChanged != null)
OutputDevicesChanged(this, new EventArgs());
}
// Input devices =============================================================
private void InputWatcher_EnumerationCompleted(DeviceWatcher sender, object args)
{
// let other classes know enumeration is complete
if (InputDevicesEnumerated != null)
InputDevicesEnumerated(this, new EventArgs());
}
private async void InputWatcher_Updated(DeviceWatcher sender, DeviceInformationUpdate args)
{
// this is where you capture changes to a specific ID
// you could change this to be more specific and pass the changed ID
if (InputDevicesChanged != null)
InputDevicesChanged(this, new EventArgs());
}
private void InputWatcher_Removed(DeviceWatcher sender, DeviceInformationUpdate args)
{
// remove from our collection the item with the specified ID
var id = args.Id;
var toRemove = (from MidiDeviceInformation mdi in ConnectedInputDevices
where mdi.Id == id
select mdi).FirstOrDefault();
if (toRemove != null)
{
ConnectedInputDevices.Remove(toRemove);
// notify clients
if (InputDevicesChanged != null)
InputDevicesChanged(this, new EventArgs());
}
}
private void InputWatcher_Added(DeviceWatcher sender, DeviceInformation args)
{
var id = args.Id;
// you could use DeviceInformation directly here, using the
// CreateFromIdAsync method. However, that is an async method
// and so adds a bit of delay. I'm using a trimmed down object
// to hold MIDI information rather than using the DeviceInformation class
#if DEBUG
// this is so you can see what the properties contain
foreach (var p in args.Properties.Keys)
{
System.Diagnostics.Debug.WriteLine("Input: " + args.Name + " : " + p + " : " + args.Properties[p]);
}
#endif
var info = new MidiDeviceInformation();
info.Id = id;
info.Name = args.Name;
info.IsDefault = args.IsDefault;
info.IsEnabled = args.IsEnabled;
ConnectedInputDevices.Add(info);
// notify clients
if (InputDevicesChanged != null)
InputDevicesChanged(this, new EventArgs());
}
}
}
There's some debug information in that class. You may find, when
enumerating devices, that the properties contain information that
will be useful to your app. So, I've added code to enumerate those
properties and spit them out to the debug window. This code takes
time, though, so make sure you don't include it in a production
app.
The downloadable version of this class also implements
IDisposable to unhook the events, and eventually dispose of other
resources. This is a standard pattern implemented with code
generated by Visual Studio 2015.
The DeviceWatcher class
The DeviceWatcher is the heart of this class. This is a Windows
Runtime class that lets a developer listen for changes for any type
of device in the system that they can normally find or enumerate
using the DeviceInformation functions.
The developer simply creates a DeviceWatcher using the device
selector for the devices they are interested in. A device selector
can be thought of like a query or filter; it's used to filter the
full device list down to only the ones you're interested in. Most
device interfaces provide a way to easily get the selector for
those devices. For example, MidiOutPort and MidiInPort both expose
GetDeviceSelector methods which return the filter/query string.
_inputWatcher = DeviceInformation.CreateWatcher(MidiInPort.GetDeviceSelector());
_outputWatcher = DeviceInformation.CreateWatcher(MidiOutPort.GetDeviceSelector());
Once the watcher is created, the app should wire up the
appropriate events and then start the watcher.
_outputWatcher.EnumerationCompleted += OutputWatcher_EnumerationCompleted;
_outputWatcher.Updated += OutputWatcher_Updated;
_outputWatcher.Removed += OutputWatcher_Removed;
_outputWatcher.Added += OutputWatcher_Added;
_outputWatcher.Start();
The events are important. The EnumerationCompleted event tells
your app that all of the appropriate devices have had Added events
fired. Typically you'd use this to then load the list into your UI,
or notify the app to do so.
The Updated event tells your app that metadata about a device
has been updated. For MIDI, this is typically not useful. Some
other devices may use this, however.
The Added and Removed events tell the app when a device has been
added or removed from the system. This is the most important part
when it comes to change notifications. These are the two events
that most apps do not pay attention to, and so require restarting
to pick up device changes.
The Start method starts the watcher. Ensure you have wired up
your events before calling this.
The test app
I built a little XAML/C# Universal app to test this out, using
Visual Studio 2015. I unloaded the phone project as the preview
MIDI API is available only for Windows (and only for 64 bit if
you're on a 64 bit machine, or 32 bit if you're on a 32 bit machine
-- "any CPU" is not supported for the preview.)
Here's a cropped version of the app on my PC
The UI is pretty simple. I have two list box controls which show
the MIDI device names, IDs, whether they are enabled and whether or
not they are the default device.
A note on MIDI device names
You may have noticed a few things.
1. The MIDI device names shown are not super helpful.
2. Not all MIDI devices on my system showed up in the list. You
can see, for example, I have no default MIDI device listed.
We're working on both of those. The latter is being tracked as a
bug for certain MOTU and Roland devices; our enumeration code
missed those devices because of how they show up in the device
tree. If you've used the preview MIDI API and have had one
or more devices fail to enumerate, please let us know as soon as
possible. We're testing a lot of devices, but I want to
make sure we have this right when we launch Windows 10, and more
data is better.
As for names, we're working on a proper scheme so the names are
meaningful and consistent. Obviously blank names are not helpful.
In my case, it's some of my Novation products (my two Launchpad S
controllers in particular) that are coming up with blank names.
XAML:
The XAML is just two list box controls with a heading.
<Page
x:Class="LaunchpadTest.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:LaunchpadTest"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid.Resources>
<Style TargetType="TextBlock" x:Key="TextBlockStyle">
<Setter Property="FontSize" Value="20" />
<Setter Property="Margin" Value="5" />
</Style>
<Style TargetType="TextBlock" x:Key="HeaderTextBlockStyle">
<Setter Property="FontSize" Value="26" />
<Setter Property="Margin" Value="10,20,10,20" />
</Style>
</Grid.Resources>
<Grid Margin="50">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Margin="5">
<TextBlock Text="Input Devices" Style="{StaticResource HeaderTextBlockStyle}"/>
<StackPanel Orientation="Horizontal" Margin="10">
<TextBlock Text="Name" Width="240" Style="{StaticResource TextBlockStyle}" />
<TextBlock Text="Enabled" Width="90" Style="{StaticResource TextBlockStyle}" />
<TextBlock Text="Default" Width="90" Style="{StaticResource TextBlockStyle}" />
<TextBlock Text="Id" Style="{StaticResource TextBlockStyle}" />
</StackPanel>
<ListBox x:Name="InputDevices">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="250" />
<ColumnDefinition Width="100" />
<ColumnDefinition Width="100" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Name}" Grid.Column="0" Style="{StaticResource TextBlockStyle}"/>
<TextBlock Text="{Binding IsEnabled}" Grid.Column="1" Style="{StaticResource TextBlockStyle}"/>
<TextBlock Text="{Binding IsDefault}" Grid.Column="2" Style="{StaticResource TextBlockStyle}"/>
<TextBlock Text="{Binding Id}" Grid.Column="3" Style="{StaticResource TextBlockStyle}"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
<StackPanel Grid.Row="1" Margin="5">
<TextBlock Text="Output Devices" Style="{StaticResource HeaderTextBlockStyle}"/>
<StackPanel Orientation="Horizontal" Margin="10">
<TextBlock Text="Name" Width="240" Style="{StaticResource TextBlockStyle}" />
<TextBlock Text="Enabled" Width="90" Style="{StaticResource TextBlockStyle}" />
<TextBlock Text="Default" Width="90" Style="{StaticResource TextBlockStyle}" />
<TextBlock Text="Id" Style="{StaticResource TextBlockStyle}" />
</StackPanel>
<ListBox x:Name="OutputDevices">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="250" />
<ColumnDefinition Width="100" />
<ColumnDefinition Width="100" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Name}" Grid.Column="0" Style="{StaticResource TextBlockStyle}"/>
<TextBlock Text="{Binding IsEnabled}" Grid.Column="1" Style="{StaticResource TextBlockStyle}"/>
<TextBlock Text="{Binding IsDefault}" Grid.Column="2" Style="{StaticResource TextBlockStyle}"/>
<TextBlock Text="{Binding Id}" Grid.Column="3" Style="{StaticResource TextBlockStyle}"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</Grid>
</Grid>
</Page>
x
Code-behind:
For purposes of this test, I put all the code in the main page's
code-behind. The code here is responsible for creating the Midi
class and listening to the events it fires off for device
enumeration and device change.
using LaunchpadTest.Devices;
using System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
namespace LaunchpadTest
{
public sealed partial class MainPage : Page
{
private Midi _midi;
public MainPage()
{
Loaded += MainPage_Loaded;
this.InitializeComponent();
}
private void MainPage_Loaded(object sender, RoutedEventArgs e)
{
_midi = new Midi();
_midi.OutputDevicesEnumerated += _midi_OutputDevicesEnumerated;
_midi.InputDevicesEnumerated += _midi_InputDevicesEnumerated;
_midi.Initialize();
}
private async void _midi_InputDevicesEnumerated(object sender, EventArgs e)
{
if (!Dispatcher.HasThreadAccess)
{
await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
InputDevices.ItemsSource = _midi.ConnectedInputDevices;
// only wire up device changed event after enumeration has completed
_midi.InputDevicesChanged += _midi_InputDevicesChanged;
});
}
}
private async void _midi_InputDevicesChanged(object sender, EventArgs e)
{
if (!Dispatcher.HasThreadAccess)
{
await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
InputDevices.ItemsSource = null;
InputDevices.ItemsSource = _midi.ConnectedInputDevices;
});
}
}
// Output devices ------------------------------------------------
private async void _midi_OutputDevicesEnumerated(object sender, EventArgs e)
{
if (!Dispatcher.HasThreadAccess)
{
await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, ()=>
{
OutputDevices.ItemsSource = _midi.ConnectedOutputDevices;
// only wire up device changed event after enumeration has completed
_midi.OutputDevicesChanged += _midi_OutputDevicesChanged;
});
}
}
private async void _midi_OutputDevicesChanged(object sender, EventArgs e)
{
if (!Dispatcher.HasThreadAccess)
{
await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
OutputDevices.ItemsSource = null;
OutputDevices.ItemsSource = _midi.ConnectedOutputDevices;
});
}
}
}
}
In the code-behind, you can see how I respond to the device
change events by rebinding the data to the list box controls.
Also, you'll see that I do the initial binding only once I
receive notification that the devices have been enumerated. At that
time, I also wire up the device changed events. If you wire them up
earlier, you'll get a bunch of events during enumeration, and that
will be just noise for your app.
Run the app with a USB MIDI interface plugged in. Then, after
the interface shows up in the list, unplug it from Windows. You
should see it disappear from the list box(es).
Summary
I like to work with MIDI and synthesizers, but this approach
will work for any of the WinRT recognized devices your apps can
use. It's a good practice to respond properly to device add and
removal to either show new devices, or ensure you stop
communicating with disconnected ones.
This pattern wasn't exactly obvious from the materials we've
supplied on MSDN, so I hope this post has helped clear up the usage
of the DeviceWatcher.
Now go build some device apps, especially MIDI. :)
References