Sunday, March 22, 2009

Silverlight with F#: Animation via XAML

In my previous blog, I was unable to fully implement recipe 2.11 of the book Silverlight 2 Recipes with F# code due to problems with getting PropertyPath to work as shown in various Silverlight books.  Just to make sure that this is not just a Silverlight issue, I tried the code with WPF and still had the same problem.  I decided to approach this differently and implement the animation in XAML instead and got it to work.  Here’s the code:


#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
// Using XAML to configure the animation
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 = new Storyboard()

(let xaml =
"<ColorAnimationUsingKeyFrames
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'
BeginTime='00:00:00'
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>"
let animation = XamlReader.Load(xaml) :?> ColorAnimationUsingKeyFrames
Storyboard.SetTarget(animation,statusEllipse)
animation
) |> storyboard.Children.Add

(let xaml =
"<DoubleAnimationUsingKeyFrames
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'
BeginTime='00:00:00' 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>"
let animation = XamlReader.Load(xaml) :?> DoubleAnimationUsingKeyFrames
Storyboard.SetTarget(animation,statusEllipse)
animation
) |> storyboard.Children.Add

(let xaml =
"<DoubleAnimationUsingKeyFrames
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'
BeginTime='00:00:00'
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>"
let animation = XamlReader.Load(xaml) :?> DoubleAnimationUsingKeyFrames
Storyboard.SetTarget(animation,statusEllipse)
animation
) |> storyboard.Children.Add

(let xaml =
"<DoubleAnimationUsingKeyFrames
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'
BeginTime='00:00:00'
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>"
let animation = XamlReader.Load(xaml) :?> DoubleAnimationUsingKeyFrames
Storyboard.SetTarget(animation,statusEllipse)
animation
) |> storyboard.Children.Add

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


// Implent 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

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

Sunday, March 08, 2009

F# Silverlight, XAML and opening local files

I’m working through more examples in the book Silverlight 2 Recipes.  While it has been an interesting exercise to manually convert the XAML examples into F# code, it detracted me from learning about other aspects of Silverlight technology.  In this particular sample code, I decided to take the XAML output and dynamically load it as per the example from recipe 2-5.  Of course, once you load the UI controls via XAML, you’ll need to find those controls as per example from recipe 2-3 from the book.  Finally, the recipe I really wanted to develop is recipe 2-7 where the Silverlight app loads local files.  Below is the sample code:


#light
namespace SilverLightFSharp

open System
open System.IO
open System.IO.IsolatedStorage
open System.Windows
open System.Windows.Controls
open System.Windows.Media
open System.ServiceModel
open System.Windows.Markup
open System.Windows.Browser
open List

// Combining recipes 2-3,2-5 and 2-7 from Silverlight 2 Recipes book
// Dynamically loading XAML from managed code
// Find an element in XAML
// Opening local files
type MyPage() = class
inherit UserControl()

do
let xaml =
"<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.Background>
<LinearGradientBrush EndPoint='0.5,1' StartPoint='0.5,0'>
<GradientStop Color='#FF000000'/>
<GradientStop Color='#FFFFFFFF' Offset='1'/>
</LinearGradientBrush>
</Grid.Background>
<Grid.RowDefinitions>
<RowDefinition Height='0.117*'/>
<RowDefinition Height='0.79*'/>
<RowDefinition Height='0.093*'/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width='0.058*'/>
<ColumnDefinition Width='0.252*'/>
<ColumnDefinition Width='0.64*'/>
<ColumnDefinition Width='0.05*'/>
</Grid.ColumnDefinitions>
<Button Height='28.9' HorizontalAlignment='Stretch' Margin='8,8,11,0'
VerticalAlignment='Top' Width='81.8' Grid.Column='1' Grid.Row='1'
Content='Select Files' d:LayoutOverrides='Height' x:Name='ButtonSelectFiles'/>
<TextBlock Margin='4,2,2,2' Grid.Column='1' Grid.Row='2' Text='Status'
TextWrapping='Wrap' Grid.ColumnSpan='2' x:Name='StatusLabel'/>
<Border Grid.Column='2' Grid.Row='1' Margin='0,0,0,0' CornerRadius='12'>
<Border.Background>
<LinearGradientBrush EndPoint='0.916999995708466,0.0890000015497208'
StartPoint='-0.0489999987185001,2.12400007247925'>
<GradientStop Color='#FF1D351E'/>
<GradientStop Color='#FF1D351E' Offset='1'/>
<GradientStop Color='#FFB7D8BA' Offset='0.50900000333786011'/>
</LinearGradientBrush>
</Border.Background>
<ListBox x:Name='FileList' Foreground='#FF000000' Height='217'
Width='236' Opacity='1'/>
</Border>
</Grid>"

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

// Find the controls...
let status = grid.FindName("StatusLabel") :?> TextBlock
let btnSelect = grid.FindName("ButtonSelectFiles") :?> Button
let fileslist = grid.FindName("FileList") :?> ListBox

// open local files
btnSelect.Click.Add(fun e ->
let fileDlg = new OpenFileDialog(Filter="Image Files (*.jpg)|*.jpg|All Files (*.*)|*.*",
Multiselect=true,
FilterIndex=1)

let (=?!) (x: Nullable<'a>) (y:'a) = x.HasValue && x.Value = y

if (fileDlg.ShowDialog() =?! true )
then let files = fileDlg.Files
status.Text <- (files |> Seq.length |> sprintf "%i file(s) selected")
files |> Seq.iter (fileslist.Items.Add)
)

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


end


type MyApp = class
inherit Application

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