Department of InformatiX
Microsoft .NET Micro Framework Tools & Resources

Button is not a key

You will probably hear this important fact couple of times more: button is not a key. Its purpose is neither to type letters nor numbers. It is much more similar to the concept of Commands in the Windows Presentation Foundation rather than to the keyboard input (though, button is not a command, either). The .NET Micro Framework project predeccessor - SPOT running eg. in smart watches - does not accept any textual input and that's why we have ButtonDown and ButtonUp routed events rather than KeyDown/KeyUp ones.

The Microsoft.SPOT.Input.Button enum (in 2.x framework) represents altogether 13 buttons: Menu, Select, Up, Down, Left, Right, Play, Pause, FastForward, Rewind, Stop, Back and Home. Sure, if you are creating some type of remote control, or some secured product requiring the user to enter a code, then that's not enough. But you need to realize that your problem is not the small number of buttons, your problem is that there are actually no keys at all in the first place! If you think for a while about it, hardly any device (not requiring user input) needs more than thirteen buttons. And have you seen the watches? Cool there are defined all these buttons...

Okay, the previous paragraph does not work for .NET Micro Framework 3.0 - there are almost 300 buttons defined now. However, I'm strongly against the way it was done (more on this later), and this article at least shows how easily you can rename the constants.

Extending the Button enum

Understanding the problem is very important thing. Now let's look what we can do with it right now.

Firstly, you do not have to use items like Play or Menu. If your device has just one green and one red button, you do not need additional button values. Just declare your buttons and assign them one of the predefined buttons, it does not matter which ones, they are just numbers!

public static class FunnyButton { public const Button Red = Button.FastForward; public const Button Green = Button.Home; }

If you don't need more than 13 buttons, this trick will work without any dramatical changes to the application:

Secondly, it is standard Enum like any other. That means, it can hold any number of the type it derives from (int). So technically, you could use it to represent up to 4294967294 buttons. To store any number in the enum, use explicit casting:

Button UltraViolet = (Button)12345;

 

The following applies to 2.x version only. Thank you for voting!

However, using such button requires some additional code. If you try to use it to construct ButtonEventArgs, an ArgumentOutOfRangeException is thrown.

UncheckedButtonEventArgs

I personally do not see any single reason why the ButtonEventArgs should put any constraints on the button value, but at least the class is not sealed, so we can create our own and inherit it.

But there are two problems. The first one is, that the ButtonEventArgs.Button is field, so it can't be overriden (and I doubt it would have been marked as virtual either). The second problem is, that it is declared as readonly; in other words, it can be modified only in the constructor. So we have the worst situation for deriving - we have to hide the underlaying member:

public class UncheckedButtonEventArgs : ButtonEventArgs { private readonly Button _button; public new Button Button { get { return _button; } } public UncheckedButtonEventArgs(PresentationSource source, TimeSpan timestamp, Button button) : base(null /* buttonDevice */, source, timestamp, (button > Button.None && button <= Button.Last) ? button : Button.Last) { _button = button; } }

For compatibility reasons, we set the base's Button to the desired value if it is in the valid range, and to Button.Last otherwise. The buttonDevice is not used at all, so I pass null to the base, although I'm not so sure how good decision is this for the future.

This solution, however, does not work on its own. Due to hiding the base's member, you have to deal directly with the UncheckedButtonEventArgs type to access the new one. This simple check can be done in the button handler:

protected override void OnButtonDown(ButtonEventArgs e) { Button button = e is UncheckedButtonEventArgs ? (e as UncheckedButtonEventArgs).Button : e.Button; ... }

Injecting your buttons into the WPF input system

Using the GPIOButtonInputProvider is a bit trickier. Open it and locate the ButtonPad class. It has a method called Interrupt, which looks like this:

void Interrupt(Cpu.Pin port, bool state, TimeSpan time) { RawButtonActions action = state ? RawButtonActions.ButtonUp : RawButtonActions.ButtonDown; RawButtonInputReport report = new RawButtonInputReport(sink.source, time, button, action); // Queue the button press to the input provider site. sink.Dispatcher.BeginInvoke(sink.callback, report); }

This is the final method which pushes the input into the WPF system. The dispatcher then calls sink.callback (delegate to InputProviderSite.ReportInput method) which more or less causes the PreviewButtonDown and ButtonDown events to be fired and routed. Unfortunately, the ButtonEventArgs is instantiated inside this process and we cannot just change it. Moreover, due to lack of input system extensibility in WPF initial release, we aren't able to replace this process with another one.

Not everything is lost yet. We can't plug us into the input system, but at least we can fire the events manually. The ButtonDown and ButtonUp are routed events so we need to fire them throughout the control tree. This is exactly what UIElement.RaiseEvent(RoutedEventArgs args) does. The last thing we need to decide is on which element we should call it. But that's a simple one, on the one with focus! Now we can put together the method which the dispatcher needs to call (and its delegate signature):

private delegate void RaiseEventCallback(RoutedEventArgs args); private void RaiseEvent(RoutedEventArgs args) { UIElement target = Buttons.FocusedElement; if (target != null) target.RaiseEvent(args); }

Put it directly to the GPIOButtonInputProvider class and create a delegate and assign the method to it in the constructor:

public sealed class GPIOButtonInputProvider { ... private ReportInputCallback callback; // current delegate to the InputSite private RaiseEventCallback eventCallback; // delegate to our method // This class maps GPIOs to Buttons processable by Microsoft.SPOT.Presentation public GPIOButtonInputProvider(PresentationSource source) { ... callback = new ReportInputCallback(site.ReportInput); // current delegate eventCallback = new RaiseEventCallback(RaiseEvent); // our new one ... }

And finally, modify the Interrupt routine above (in ButtonPad class) to send the exended buton range to our method instead of the InputProviderSite:

... // Queue the button press to the input provider site. if (button > Button.None && button <= Button.Last) sink.Dispatcher.BeginInvoke(sink.callback, report); else { RoutedEventArgs args = new UncheckedButtonEventArgs(null, time, button); args.RoutedEvent = state ? Buttons.ButtonDownEvent : Buttons.ButtonUpEvent; sink.Dispatcher.BeginInvoke(sink.eventCallback, args); } }

Well, everything is ready now, you are free to use your buttons in the provider:

... ButtonPad[] buttons = new ButtonPad[] {-- // Associate the buttons to the pins as setup in the emulator/hardware new ButtonPad(this, Button.Up , Cpu.Pin.GPIO_Pin0), new ButtonPad(this, FunnyButton.UltraViolet , Cpu.Pin.GPIO_Pin1), }; ...

And my last note today: Keep in mind this is a simple workaround. If you would really want to make it the right way, you should also call the preview events using the tunnel routing strategy and check if the input was handled before firing the actual button events. Have a nice day!

Comments
Sign in using Live ID to be able to post comments.