Wednesday, April 15, 2009

Silverlight with F#: Handling Keyboard Input

Recipe 3-15 of the book Silverlight 2 Recipes illustrates handling keyboard input.  The code is suppose to allow you to move a spinning ball with the directional keys.  When I initially implement it, I ran into some issues in trying to capture keyboard input. As I understand keyboard events from reading Chapter 4 of the book Pro Silverlight 2 in C# 2008, they are suppose to bubble up to the top level control.  However, when I attach code to the KeyUp event to the Canvas control, nothing happened.  I eventually realized that I did not know where is the input focus when I generate the keyboard event.  The only control that I am sure will receive the keyboard event is at the top level control.  In my scaffolding code for Silverlight, I had MyPage added as a children of a StackPanel that I called rootPanel, so even my custom UserControl was not getting the keyboard events.  For this particular example, I had to get rid of the rootPanel and then I could capture the keyboard input from my custom UserControl

Just to verify that keyboard events do bubble up, I mocked up another small code where I embed a TextBox deep inside a visual hierarchy.  When I set my focus to the TextBox and press a key, all the containing controls were able to receive the keyboard events; thereby validating, for my benefit, the fact that keyboard events bubble up.

The other thing to point out is in the last blog entry, I did not properly dispose the created StreamReader object. In this post, I change the let binding to StreamReader to use binding to resolve the problem.

Here’s the working F# code along with the XAML code :

namespace SilverLightFSharp

open System
open System.Resources
open System.Reflection
open System.IO
open System.Windows
open System.Windows.Browser
open System.Windows.Controls
open System.Windows.Markup
open System.Windows.Input
open System.Windows.Media
open System.Windows.Media.Animation
open System.Windows.Shapes
open List

// Recipe 3-15 of the Silverlight 2 Recipes book
type MyPage = class
inherit UserControl

new () as this = {} then

let xamlControls =
let assembly = Assembly.GetExecutingAssembly();
use sr = new StreamReader(assembly.GetManifestResourceStream("page.xaml"));

// Load xaml dynamically
let canvas = XamlReader.Load(xamlControls) :?> Canvas
let clicktoplay = canvas.FindName("ClickToPlay") :?> Border
let ball = canvas.FindName("RadioactiveBall") :?> Ellipse
let welcomeMessage = canvas.FindName("WelcomeMessage") :?> Border
let gameMessage = canvas.FindName("GameMessage") :?> TextBlock

let storyboard = canvas.Resources.["SpinGameBallStoryboard"] :?> Storyboard

this.Width <- 600.0
this.Height <- 400.0
this.Content <- canvas

let moveSpeed = 10.0

let getPosition (b:Ellipse) =
((b.GetValue(Canvas.LeftProperty) :?> double), (b.GetValue(Canvas.TopProperty) :?> double))

let draw (x,y) =
(Canvas.LeftProperty,x) |> ball.SetValue
(Canvas.TopProperty,y) |> ball.SetValue

clicktoplay.MouseLeftButtonDown.Add(fun e ->
clicktoplay.Visibility <- Visibility.Collapsed
//welcomeMessage.Visibility <- Visibility.Collapsed
gameMessage.Text <- "Left mouse button down!"
ball.Visibility <- Visibility.Visible
this.IsEnabled <- true

let leftborder = ball.Width * 0.25
let rightborder = this.Width - ball.Width*1.25
let topborder = ball.Height*0.25
let bottomborder = this.Height - ball.Height*1.25

let moveLeft (x,y) =
(max [x-moveSpeed;leftborder],y)

let moveRight (x,y) =
(min [x+moveSpeed;rightborder],y)

let moveUp (x,y) =
(x,max [y-moveSpeed;topborder])

let moveDown (x,y) =
(x,min [y+moveSpeed;bottomborder])

this.KeyUp.Add(fun e ->
let pt = getPosition ball
let newpos = match (e.Key) with
| Key.Right -> moveRight pt
| Key.Left -> moveLeft pt
| Key.Up -> moveUp pt
| Key.Down -> moveDown pt
| _ -> pt
gameMessage.Text <- sprintf "Ball at %4.1f %4.1f " (fst newpos) (snd newpos)
draw newpos


type ErrorPage = class
inherit UserControl

new (msg:string ) as this = {} then

let textarea = new TextBlock(Text=msg,
Margin=new Thickness(4.0,4.0,4.0,4.0))

let sp = new StackPanel(HorizontalAlignment = HorizontalAlignment.Stretch,
Margin=new Thickness(8.0,8.0,10.0,8.0))

base.Width <- 600.0
base.Height <- 400.0
base.Content <- sp

type MyApp = class
inherit Application

new () as this = {} then
this.Startup.Add(fun _ -> this.RootVisual <- new MyPage())

x:Name="GameCanvas" >
<RadialGradientBrush x:Key="IceBrush">
<GradientStop Color="#FFFFFFFF"/>
<GradientStop Color="#FF6F74AB" Offset="1"/>
<LinearGradientBrush x:Key="MessageBorderBrush" EndPoint="0.501999974250793,1"
<GradientStop Color="#FF000000"/>
<GradientStop Color="#33FFFFFF" Offset="1"/>
<Storyboard x:Name="SpinGameBallStoryboard" x:Key="SpinGameBallStoryboard" RepeatBehavior="Forever">
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
<SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
<SplineDoubleKeyFrame KeyTime="00:00:02" Value="360">
<KeySpline ControlPoint1="0,0" ControlPoint2="1,1"/>

<GradientStop Color="#FFFFFFFF"/>
<GradientStop Color="#FFB3BBE8" Offset="1"/>
<Border Background="{StaticResource MessageBorderBrush}" Height="Auto"
x:Name="WelcomeMessage" Width="Auto" Canvas.Left="173" Canvas.Top="119"
<TextBlock x:Name="GameMessage" Height="Auto" Width="Auto" FontFamily="Comic Sans MS"
FontSize="24" Text="Welcome to !" TextWrapping="Wrap"
Padding="2,2,2,2" Foreground="#FF044FB5" Margin="2,2,2,2"/>
<Border x:Name="ClickToPlay" Height="77" Width="169" Canvas.Top="178"
Canvas.Left="207" CornerRadius="10,10,10,10" Background=
"{StaticResource MessageBorderBrush}" Margin="0,0,0,0" >
<TextBlock Height="Auto" Width="Auto" FontSize="24" Text="Click to Play"
TextWrapping="Wrap" Margin="4,10,4,4" Padding="2,2,2,2" Foreground="#FF044FB5"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" />
<Ellipse Height="50" Width="50" Canvas.Left="259" Canvas.Top="168.879"
Stroke="#FF000000" Visibility="Collapsed" x:Name="RadioactiveBall"
<LinearGradientBrush EndPoint="0.959999978542328,0.0219999998807907"
<GradientStop Color="#FFAED4B2"/>
<GradientStop Color="#FFAED4B2" Offset="1"/>
<GradientStop Color="#FF4A9B53" Offset="0.179"/>
<GradientStop Color="#FF4A9B53" Offset="0.75"/>
<GradientStop Color="#FF98BD9D" Offset="0.4869999885559082"/>
<Path Height="399" Width="25" Stretch="Fill" Stroke="#FF000000"
Data="M0,0 L24,10 L16,18 L21,29 L11,38 L21,54 L14,63 L19,84 L6,90 L15,103 L9,119 L15,136 L9,145 L18,155 L7,165 L23,178 L7,184 L19,200 L10,211 L17,221 L9,234 L17,240 L9,255 L15,264 L9,275 L17,286 L7,293 L17,304 L9,313 L19,329 L10,337 L15,345 L12,361 L13,371 L8,382 L12,389 L0,398 z" Canvas.Top="-0.5" Canvas.Left="-0.5" Fill="{StaticResource IceBrush}" x:Name="LeftIceCaveWall"/>
<Path Height="30" Width="598" Canvas.Left="0.5" Canvas.Top="368.5"
Fill="{StaticResource IceBrush}" Stretch="Fill" Stroke="#FF000000"
Data="M1,397 L20,382 L30,387 L47,381 L63,388 L76,380 L90,385 L100,388 L110,376 L115,384 L127,378 L140,388 L155,381 L166,388 L179,378 L190,386 L206,375 L218,388 L230,372 L235,382 L249,376 L268,386 L284,375 L295,386 L309,374 L321,383 L334,369 L345,381 L357,382 L368,378 L378,391 L392,382 L412,388 L441,380 L456,390 L478,383 L494,389 L501,382 L515,390 L524,375 L534,386 L541,373 L553,369 L567,376 L576,381 L598,398 z" x:Name="IceCaveWallFloor"/>
<Path Height="398" Width="40" Canvas.Left="558.5" Canvas.Top="-0.5"
Fill="{StaticResource IceBrush}" Stretch="Fill" Stroke="#FF000000"
Data="M598,397 L578,389 L572,367 L559,355 L570,339 L559,324 L574,313 L581,294 L576,281 L584,262 L584,243 L573,229 L586,217 L575,206 L584,192 L567,182 L587,161 L576,152 L583,139 L570,131 L578,124 L568,103 L581,76 L568,56 L577,49 L573,20 L584,12 L597,0 z" x:Name="RightIceCaveWall"/>
<Path Height="53" Width="596" Canvas.Left="0.5" Canvas.Top="-0.5"
Fill="{StaticResource IceBrush}" Stretch="Fill" Stroke="#FF000000"
Data="M1,1 L21,19 L34,5 L43,13 L54,11 L65,8 L77,8 L86,11 L106,11 L115,8 L126,8 L138,12 L156,10 L171,8 L184,13 L197,7 L217,14 L231,8 L252,13 L259,8 L283,13 L295,8 L327,12 L336,18 L353,11 L365,30 L375,10 L393,16 L418,10 L437,15 L451,5 L468,17 L483,6 L501,18 L514,32 L526,19 L544,31 L556,26 L596,19 L596,0 z" x:Name="IceCaveCeiling"/>

1 comment:

digital certificate said...

Excellent! Thanks for this - I've been looking at this feature for ages. Followed your instructions and it works a treat!