Tuesday, March 17, 2009

Silverlight with F#: Background Thread and Animation

I am working through recipe 2.11 in the book Silverlight 2 Recipes, which introduce concepts of background threads and animation in Silverlight.  In developing recipe 2.11 in F#, I ran into problems getting animation to work. I managed to narrow the problem down to use of PropertyPath in my code.  If I use the following line of code, the Silverlight application would work.

Storyboard.SetTargetProperty(animation,new PropertyPath(UIElement.OpacityProperty))

But if I use the following line of code:

Storyboard.SetTargetProperty(animation,new PropertyPath("(UIElement.Opacity)"))

it would fail with the following message:

proppatherr

Matthew MacDonald seemed to support the second usage of PropertyPath in chapter 9 of his book Pro Silverlight 2 in C# 2008.  I’m not sure right now what’s causing it to break when using that code form with F#.  Due to problems I’ve encountered with PropertyPath, I’m not able to implement recipe 2.11 completely.  I did managed to implement a stripped down version of recipe 2.11 that demonstrates background threads with animation.  Here’s the code that did work for me:


#light
namespace SilverLightFSharp

open System
open System.ComponentModel
open System.IO
open System.IO.IsolatedStorage
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

// Recipe 2-11 from Silverlight 2 Recipes book
// Executing work on a background thread with updates
// Simplified due to problems with Storyboard.SetTargetProperty and PropertyPath
// Added error page for debugging purposes
type MyPage() = class
inherit UserControl()

do
let xamlControls =
"<Grid xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:d='http://schemas.microsoft.com/expression/blend/2008'
xmlns:mc='http://schemas.openxmlformats.org/markup-compatibility/2006'
mc:Ignorable='d'
>
<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='5,8,6,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

//let storyboard = Resources.createStoryBoard()
// Must preset opacity value
statusEllipse.Opacity <- 0.2
let storyboard = new Storyboard()
let animation = new DoubleAnimation(From=new Nullable<float>(0.2),
To=new Nullable<float>(1.0),
Duration= new Duration(TimeSpan.FromSeconds(5.0)))
Storyboard.SetTarget(animation,statusEllipse)
// This following line of code does not work - gives NullReference exception
//Storyboard.SetTargetProperty(animation,new PropertyPath("(UIElement.Opacity)"))
Storyboard.SetTargetProperty(animation,new PropertyPath(UIElement.OpacityProperty))
storyboard.Children.Add(animation)

let worker = new BackgroundWorker(WorkerReportsProgress = true,
WorkerSupportsCancellation = true)


// Implement DoWorkButton_Click
doWorkButton.Click.Add(fun e ->
if worker.IsBusy = false then
textarea.Text <- sprintf "Started : %s" (DateTime.Now.ToString())
worker.RunWorkerAsync(textarea.Text)
storyboard.AutoReverse <- true
storyboard.RepeatBehavior <- RepeatBehavior.Forever
storyboard.Begin()
)

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

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

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

// Implement DoWork
worker.DoWork.Add(fun e ->
[1..30] |> iter (fun i ->
if worker.CancellationPending = true then
e.Cancel <- false
else
System.Threading.Thread.Sleep(1000);
(int (floor (float i)*100.0/30.0))
|> worker.ReportProgress)
e.Result <- sprintf "\nCompleted : %s" (DateTime.Now.ToString()))

// Implement RunWorkerCompleted
worker.RunWorkerCompleted.Add(fun e ->
storyboard.Stop()
if e.Cancelled = false then
statusEllipse.Fill <- new SolidColorBrush(Color.FromArgb(255uy,0uy,255uy,0uy))
textarea.Text <- textarea.Text + e.Result.ToString()
ToolTipService.SetToolTip(statusEllipse, "Work Complete.")
else
statusEllipse.Fill <- new SolidColorBrush(Color.FromArgb(255uy,255uy,255uy,0uy))
let msg = sprintf "%s \nCanceled@: %s" textarea.Text <| DateTime.Now.ToString()
ToolTipService.SetToolTip(statusEllipse, msg))

// Implement ProgressChanged
worker.ProgressChanged.Add(fun e ->
if cancelCanvas.Visibility = Visibility.Collapsed then
let msg = sprintf "%s Percent Complete. Click to cancel..." (e.ProgressPercentage.ToString())
ToolTipService.SetToolTip(statusEllipse, msg))

base.Width <- 400.0
base.Height <- 300.0
base.Content <- grid
//base.Content <- testerror
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

2 comments:

Art said...

Thanks John!
Your work is directly applicable and totally relevant.
On point.

Anonymous said...

Create progress UI thread animated through BackgroundWorker