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 :


#light
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"));
sr.ReadToEnd()

// 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
storyboard.Begin())

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
)

end

type ErrorPage = class
inherit UserControl

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

let textarea = new TextBlock(Text=msg,
TextWrapping=TextWrapping.Wrap,
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))
sp.Children.Add(textarea)

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


type MyApp = class
inherit Application

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


<Canvas
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Name="GameCanvas" >
<Canvas.Resources>
<RadialGradientBrush x:Key="IceBrush">
<GradientStop Color="#FFFFFFFF"/>
<GradientStop Color="#FF6F74AB" Offset="1"/>
</RadialGradientBrush>
<LinearGradientBrush x:Key="MessageBorderBrush" EndPoint="0.501999974250793,1"
StartPoint="0.497999995946884,0">
<GradientStop Color="#FF000000"/>
<GradientStop Color="#33FFFFFF" Offset="1"/>
</LinearGradientBrush>
<Storyboard x:Name="SpinGameBallStoryboard" x:Key="SpinGameBallStoryboard" RepeatBehavior="Forever">
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="RadioactiveBall"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[2].(RotateTransform.Angle)">
<SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
<SplineDoubleKeyFrame KeyTime="00:00:02" Value="360">
<SplineDoubleKeyFrame.KeySpline>
<KeySpline ControlPoint1="0,0" ControlPoint2="1,1"/>
</SplineDoubleKeyFrame.KeySpline>
</SplineDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</Canvas.Resources>

<Canvas.Background>
<RadialGradientBrush>
<GradientStop Color="#FFFFFFFF"/>
<GradientStop Color="#FFB3BBE8" Offset="1"/>
</RadialGradientBrush>
</Canvas.Background>
<Border Background="{StaticResource MessageBorderBrush}" Height="Auto"
x:Name="WelcomeMessage" Width="Auto" Canvas.Left="173" Canvas.Top="119"
CornerRadius="10,10,10,10">
<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>
<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" />
</Border>
<Ellipse Height="50" Width="50" Canvas.Left="259" Canvas.Top="168.879"
Stroke="#FF000000" Visibility="Collapsed" x:Name="RadioactiveBall"
RenderTransformOrigin="0.5,0.5">
<Ellipse.RenderTransform>
<TransformGroup>
<ScaleTransform/>
<SkewTransform/>
<RotateTransform/>
<TranslateTransform/>
</TransformGroup>
</Ellipse.RenderTransform>
<Ellipse.Fill>
<LinearGradientBrush EndPoint="0.959999978542328,0.0219999998807907"
StartPoint="0.0199999995529652,1.06200003623962">
<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"/>
</LinearGradientBrush>
</Ellipse.Fill>
</Ellipse>
<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"/>
</Canvas>


Thursday, April 09, 2009

Silverlight with F#: Managing embedded resources

One of my blog reader asked why I inline XAML instead of putting it into a separate file and hooking up the variables manually in F#.  My answer was laziness: I did not spend the time to figure out how. (If you subscribe to Larry Wall’s philosophy, laziness is a virtue …)  Well, that question prompted me to look into how to externalize the xaml file and load it in.  This dovetailed nicely with recipe 2.14 of the book Silverlight 2 Recipes, which shows how to load embedded resources.  I put the xaml code in a separate file called page.xaml and added the following compiler flag:

 --resource page.xaml

Here’s the xaml file:


<Canvas
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Name="LayoutRoot" Background="White">
<TextBlock>Hi There Reader</TextBlock>
<TextBlock Canvas.Left="20" Canvas.Top="20">Hi There</TextBlock>
<TextBlock Canvas.Left="40"
Canvas.Top="40">Silverlight</TextBlock>
<TextBlock Canvas.Left="60" Canvas.Top="60">
Silverlight</TextBlock>
<TextBlock Canvas.Left="80" Canvas.Top="80">
Silverlight</TextBlock>
<TextBlock Canvas.Left="100" Canvas.Top="100">
Silverlight</TextBlock>
</Canvas>

Here's the code that loads the xaml file and hook the control with a variable:


#light
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 List

// Recipe 2-14 and XAML from Recipe 3-3 of the Silverlight 2 Recipes book
type MyPage() = class
inherit UserControl()

do

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

// Load xaml dynamically
let canvas = XamlReader.Load(xamlControls) :?> Canvas


base.Width <- 400.0
base.Height <- 300.0
base.Content <- canvas
end

type ErrorPage = class
inherit UserControl

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

let textarea = new TextBlock(Text=msg,
TextWrapping=TextWrapping.Wrap,
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))
sp.Children.Add(textarea)

base.Width <- 600.0
base.Height <- 1000.0
base.Content <- sp
end


type MyApp = class
inherit Application

new () as this = {} then
this.Startup.Add(fun _ ->
let rootPanel = new StackPanel()
try
this.RootVisual <- rootPanel
rootPanel.Children.Add(new MyPage())
with _ as e ->
let msg = e.Message + e.StackTrace
rootPanel.Children.Clear()
rootPanel.Children.Add(new ErrorPage(msg)))

this.UnhandledException.Add(fun args ->
args.Handled <- true
try
let msg = args.ExceptionObject.Message + args.ExceptionObject.StackTrace
let errmsg = msg.Replace('"','\'').Replace("\r\n",@"\n")
"throw new Error(\"Unhandled error in Silverlight 2 Application " + msg + "\");"
|> HtmlPage.Window.Eval |> ignore
with _ -> HtmlPage.Window.Eval("throw new Error(\"Custom Error!\");") |> ignore)
end

Wednesday, April 01, 2009

Silverlight with F#: Animation via XAML (revisited)

I should have took the time to read Matthew MacDonald’s book Pro Silverlight 2 in C# 2008 before I started tackling recipe 2.11 in the book Silverlight 2 Recipes.  The code I created in the last blog post was made more complex due to my poor workaround to extracting the Storyboard object from XAML.  After reading chapter 2 of the book Pro Silverlight 2 in C# 2008, it was obvious that in order to extract the Storyboard object from XAML, all I had to do was add the Key attribute to the Storyboard markup and add the Storyboard markup to the Grid.Resources property. 

In addition, I’m reading through Chapter 13 of Don Syme’s book Expert F# trying to understand more how concurrency works with F#.  I decided to try to replicate the Iterative Worker example in the book, replacing the simple BackgroundWorker example in recipe 2.11.

Here is the revised code with Storyboard as part of the resources of the Grid object and the reworked background worker code.


#light
namespace SilverLightFSharp

open System
open System.ComponentModel
open System.ServiceModel
open System.Windows
open System.Windows.Browser
open System.Windows.Controls
open System.Windows.Markup
open System.Windows.Media
open System.Windows.Media.Animation
open System.Windows.Shapes
open List

// SimulateBackgroundWorker modelled after
// IterativeBackgroundWorker example from Chapter 13 of Expert F#
type SimulateBackgroundWorker<'a>(delay:int, numIterations:int) =
let worker = new BackgroundWorker(WorkerReportsProgress=true,
WorkerSupportsCancellation=true)
let triggerCompleted,completed = Event.create()
let triggerError ,error = Event.create()
let triggerCancelled,cancelled = Event.create()
let triggerProgress ,progress = Event.create()

do worker.DoWork.Add(fun args ->
let rec iterate i =
if worker.CancellationPending then
args.Cancel <- true
elif i < numIterations then
System.Threading.Thread.Sleep(delay)

// Report the percentage computation
let percent = int((float (i+1)/float numIterations) * 100.0)
do worker.ReportProgress(percent)
iterate (i+1)
else ()
iterate 0)

do worker.RunWorkerCompleted.Add(fun args ->
if args.Cancelled then triggerCancelled()
elif args.Error<> null then triggerError args.Error
else triggerCompleted())

do worker.ProgressChanged.Add(fun args ->
triggerProgress(args.ProgressPercentage))

member x.WorkerCompleted = completed
member x.WorkerCancelled = cancelled
member x.WorkerError = error
member x.ProgressChanged = progress

// Delegate the remaining memebers to the underlying worker
member x.RunWorkerAsync() = worker.RunWorkerAsync()
member x.CancelAsync() = worker.CancelAsync()
member x.IsBusy = worker.IsBusy




// Recipe 2-11 from Silverlight 2 Recipes book
// Using XAML to configure the animation
type MyPage() = class
inherit UserControl()

do
// added x:Key='AnimateStatusEllipse' to Storyboard
let xamlControls =
"<Grid
xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
x:Name='LayoutRoot' Background='#FFFFFFFF'>
<Grid.Resources>
<Storyboard x:Name='AnimateStatusEllipse' x:Key='AnimateStatusEllipse'>
<ColorAnimationUsingKeyFrames BeginTime='00:00:00' Storyboard.TargetName='StatusEllipse'
Storyboard.TargetProperty='(Shape.Fill).(SolidColorBrush.Color)'>
<SplineColorKeyFrame KeyTime='00:00:00' Value='#FF008000'/>
<SplineColorKeyFrame KeyTime='00:00:01.5000000' Value='#FFFFFF00'/>
<SplineColorKeyFrame KeyTime='00:00:03' Value='#FF008000'/>
<SplineColorKeyFrame KeyTime='00:00:04.5000000' Value='#FF008000'/>
<SplineColorKeyFrame KeyTime='00:00:06' Value='#FFFFFF00'/>
<SplineColorKeyFrame KeyTime='00:00:07.5000000' Value='#FF008000'/>
</ColorAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime='00:00:00' Storyboard.TargetName='StatusEllipse'
Storyboard.TargetProperty='(UIElement.Opacity)'>
<SplineDoubleKeyFrame KeyTime='00:00:00' Value='0.7'/>
<SplineDoubleKeyFrame KeyTime='00:00:01.5000000' Value='0.5'/>
<SplineDoubleKeyFrame KeyTime='00:00:03' Value='0.5'/>
<SplineDoubleKeyFrame KeyTime='00:00:04.5000000' Value='0.7'/>
<SplineDoubleKeyFrame KeyTime='00:00:06' Value='0.5'/>
<SplineDoubleKeyFrame KeyTime='00:00:07.5000000' Value='0.5'/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime='00:00:00' Storyboard.TargetName='StatusEllipse'
Storyboard.TargetProperty='(UIElement.RenderTransform).(TransformGroup.Children)[1].(SkewTransform.AngleX)'>
<SplineDoubleKeyFrame KeyTime='00:00:00' Value='0'/>
<SplineDoubleKeyFrame KeyTime='00:00:01.5000000' Value='0'/>
<SplineDoubleKeyFrame KeyTime='00:00:03' Value='0'/>
<SplineDoubleKeyFrame KeyTime='00:00:04.5000000' Value='0'/>
<SplineDoubleKeyFrame KeyTime='00:00:06' Value='0'/>
<SplineDoubleKeyFrame KeyTime='00:00:07.5000000' Value='0'/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime='00:00:00' Storyboard.TargetName='StatusEllipse'
Storyboard.TargetProperty='(UIElement.RenderTransform).(TransformGroup.Children)[1].(SkewTransform.AngleY)'>
<SplineDoubleKeyFrame KeyTime='00:00:00' Value='0'/>
<SplineDoubleKeyFrame KeyTime='00:00:01.5000000' Value='0'/>
<SplineDoubleKeyFrame KeyTime='00:00:03' Value='0'/>
<SplineDoubleKeyFrame KeyTime='00:00:04.5000000' Value='0'/>
<SplineDoubleKeyFrame KeyTime='00:00:06' Value='0'/>
<SplineDoubleKeyFrame KeyTime='00:00:07.5000000' Value='0'/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</Grid.Resources>
<Grid.ColumnDefinitions>
<ColumnDefinition Width='0.068*'/>
<ColumnDefinition Width='0.438*'/>
<ColumnDefinition Width='0.495*'/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height='0.08*'/>
<RowDefinition Height='0.217*'/>
<RowDefinition Height='0.61*'/>
<RowDefinition Height='0.093*'/>
</Grid.RowDefinitions>
<Button HorizontalAlignment='Stretch' Margin='5,8,5,8' VerticalAlignment='Stretch'
Grid.Column='1' Grid.Row='1' Content='Save Form Data'/>
<StackPanel HorizontalAlignment='Stretch' Margin='4.8270001411438,8,6.19799995422363,8'
Grid.Column='1' Grid.Row='2'>
<TextBlock Height='Auto' Width='Auto' Text='Work Results Appear Below' TextWrapping='Wrap' Margin='4,4,4,4'/>
<TextBox Height='103' Width='Auto' Text='' TextWrapping='Wrap' Margin='4,4,4,4' x:Name='WorkResultsTextData'/>
</StackPanel>
<Button HorizontalAlignment='Stretch' Margin='12,8,8,8' VerticalAlignment='Stretch' Grid.Column='2' Grid.Row='1' Content='Load Form Data'/>
<Button HorizontalAlignment='Stretch' Margin='10,2,8,6' VerticalAlignment='Stretch' Grid.Column='1' Grid.Row='3' Content='Kick Off Work' x:Name='DoWorkButton'/>
<Border Grid.Column='2' Grid.Row='2' Grid.RowSpan='2' CornerRadius='10,10,10,10' Margin='1.80200004577637,2,2,2'>
<Border.Background>
<LinearGradientBrush EndPoint='0.560000002384186,0.00300000002607703' StartPoint='0.439999997615814,0.996999979019165'>
<GradientStop Color='#FF586C57'/>
<GradientStop Color='#FFA3BDA3' Offset='0.536'/>
<GradientStop Color='#FF586C57' Offset='0.968999981880188'/>
</LinearGradientBrush>
</Border.Background>
<StackPanel Margin='4,4,4,4' x:Name='FormData'>
<TextBlock Height='Auto' Width='Auto' Text='First Name:' TextWrapping='Wrap' Margin='2,2,2,0'/>
<TextBox Height='Auto' Width='Auto' Text='' TextWrapping='Wrap' x:Name='Field1' Margin='2,0,2,4'/>
<TextBlock Height='Auto' Width='Auto' Text='Last Name:' TextWrapping='Wrap' Margin='2,4,2,0'/>
<TextBox Height='Auto' x:Name='Field2' Width='Auto' Text='' TextWrapping='Wrap' Margin='2,0,2,4'/>
<TextBlock Height='Auto' Width='Auto' Text='Company:' TextWrapping='Wrap' Margin='2,4,2,0'/>
<TextBox Height='Auto' x:Name='Field3' Width='Auto' Text='' TextWrapping='Wrap' Margin='2,0,2,2'/>
<TextBlock Height='22.537' Width='182' Text='Title:' TextWrapping='Wrap' Margin='2,4,2,0'/>
<TextBox Height='20.772' x:Name='Field4' Width='182' Text='' TextWrapping='Wrap' Margin='2,0,2,2'/>
</StackPanel>
</Border>
<Ellipse x:Name='StatusEllipse' Margin='4,2,2,2' Grid.Row='3' Stroke='#FF000000' Fill='#FF2D4DE0' RenderTransformOrigin='0.5,0.5' >
<Ellipse.RenderTransform>
<TransformGroup>
<ScaleTransform/>
<SkewTransform/>
<RotateTransform/>
<TranslateTransform/>
</TransformGroup>
</Ellipse.RenderTransform>
<ToolTipService.ToolTip>
<ToolTip Content='Click button to start work.' />
</ToolTipService.ToolTip>
</Ellipse>
<Canvas HorizontalAlignment='Stretch' Margin='0,0,2,8' Grid.RowSpan='4' Grid.ColumnSpan='3' x:Name='PromptCancelCanvas' Visibility='Collapsed'>
<Rectangle Height='300' Width='400' Fill='#FF808080' Stroke='#FF000000' Stretch='Fill' Opacity='0.6'/>
<Canvas Height='106' Width='289' Canvas.Left='46' Canvas.Top='85'>
<Rectangle Height='106' Width='289' Fill='#FFFFFFFF' Stroke='#FF000000' RadiusX='23' RadiusY='23' Opacity='0.85'/>
<Button Height='34' x:Name='ButtonConfirmCancelYes' Width='100' Canvas.Left='15' Canvas.Top='49' Content='Yes'/>
<Button Height='34' x:Name='ButtonConfirmCancelNo' Width='100' Canvas.Left='164' Canvas.Top='49' Content='No'/>
<TextBlock Width='134.835' Canvas.Left='75' Canvas.Top='12.463' Text='Cancel Operation?' TextWrapping='Wrap'/>
</Canvas>
</Canvas>
<TextBlock Margin='67.8270034790039,0,-88.802001953125,0' Grid.Column='1' Grid.ColumnSpan='1' Text='BackgroundWorker Thread' TextWrapping='Wrap'/>
</Grid>"

// Load xaml dynamically
let grid = XamlReader.Load(xamlControls) :?> Grid

// Find the controls...
let statusEllipse = grid.FindName("StatusEllipse") :?> Ellipse
let textarea = grid.FindName("WorkResultsTextData") :?> TextBox
let doWorkButton = grid.FindName("DoWorkButton") :?> Button
let cancelNoButton = grid.FindName("ButtonConfirmCancelNo") :?> Button
let cancelYesButton = grid.FindName("ButtonConfirmCancelYes") :?> Button
let cancelCanvas = grid.FindName("PromptCancelCanvas") :?> Canvas

// Grab storyboard from grid resources with the extra key specified..
let storyboard = grid.Resources.["AnimateStatusEllipse"] :?> Storyboard

// Remove code to add storyboard children objects...

// SimulateBackgroundWorker modelled after Don Syme's Iterative Background worker
let worker = new SimulateBackgroundWorker<_>(1000,30)

doWorkButton.Click.Add(fun e ->
worker.RunWorkerAsync()
textarea.Text <- sprintf "(2) Started : %s" (DateTime.Now.ToString())
storyboard.AutoReverse <- true
storyboard.RepeatBehavior <- RepeatBehavior.Forever
storyboard.Begin())

cancelYesButton.Click.Add(fun e ->
worker.CancelAsync()
cancelCanvas.Visibility <- Visibility.Collapsed)

cancelNoButton.Click.Add(fun e ->
cancelCanvas.Visibility <- Visibility.Collapsed)

statusEllipse.MouseLeftButtonDown.Add(fun e ->
if worker.IsBusy = true then
cancelCanvas.Visibility <- Visibility.Visible)

worker.WorkerCompleted.Add(fun () ->
statusEllipse.Fill <- new SolidColorBrush(Color.FromArgb(255uy,0uy,255uy,0uy))
let msg = sprintf "\nCompleted : %s" (DateTime.Now.ToString())
textarea.Text <- textarea.Text + msg
ToolTipService.SetToolTip(statusEllipse, "Work Complete.")
)

worker.ProgressChanged.Add(fun (progressPercentage) ->
if cancelCanvas.Visibility = Visibility.Collapsed then
let msg = sprintf "%i Percent Complete. Click to cancel..." progressPercentage
ToolTipService.SetToolTip(statusEllipse, msg))

worker.WorkerCancelled.Add(fun () ->
statusEllipse.Fill <- new SolidColorBrush(Color.FromArgb(255uy,255uy,255uy,0uy))
let msg = sprintf "%s \nCanceled@: %s" textarea.Text <| DateTime.Now.ToString()
textarea.Text <- msg
ToolTipService.SetToolTip(statusEllipse, msg))

base.Width <- 400.0
base.Height <- 300.0
base.Content <- grid
end

type ErrorPage = class
inherit UserControl

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

let textarea = new TextBlock(Text=msg,
TextWrapping=TextWrapping.Wrap,
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))
sp.Children.Add(textarea)

base.Width <- 600.0
base.Height <- 1000.0
base.Content <- sp
end


type MyApp = class
inherit Application

new () as this = {} then
this.Startup.Add(fun _ ->
let rootPanel = new StackPanel()
try
this.RootVisual <- rootPanel
rootPanel.Children.Add(new MyPage())
with _ as e ->
let msg = e.Message + e.StackTrace
rootPanel.Children.Clear()
rootPanel.Children.Add(new ErrorPage(msg)))

this.UnhandledException.Add(fun args ->
args.Handled <- true
try
let msg = args.ExceptionObject.Message + args.ExceptionObject.StackTrace
let errmsg = msg.Replace('"','\'').Replace("\r\n",@"\n")
"throw new Error(\"Unhandled error in Silverlight 2 Application " + msg + "\");"
|> HtmlPage.Window.Eval |> ignore
with _ -> HtmlPage.Window.Eval("throw new Error(\"Custom Error!\");") |> ignore)
end