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

3 comments:

Anonymous said...

Quick question- why do you choose to inline the XAML instead of putting it into a separate file and hooking up the variables manually in your F#?

John Liao said...

Laziness. I didn't spend the time to do it right. Your comments forced me to get my development kinks out. All you have to do is add the following code:


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

and add the following compiler flag:

--resource page.xaml

and you should be good to go.

esign said...

Hi there! glad to drop by your page and found these very interesting and informative stuff. Thanks for sharing, keep it up!