Thursday, December 06, 2007

Learning WPF with F# - The Dock and the Grid and Problems with Static Readonly Fields

Working through Chapter 8 of Petzold's "Applications = Code + Markup: A Guide to the Microsoft Windows Presentation Foundation" book.

Working on the SpaceButton examples forced me to work with static members and static constructors in F#. Unfortunately, I'm completely stumped. In the SpaceButton example, I don't know how to setup a static readonly field in F# and I don't know how to initialize SpaceProperty outside of SpaceButton class due to initialization dependency with SpaceButton. The closest thing that I can find on the web is the following blog entry by Lewis Bruck which indicates that F# and SQL2005 CLR has problems because F# does not generate static readonly fields. Don Syme apparently has thought about this subject as he has published an article An Alternative Approach to Initializing Mutually Referential Objects". But it wasn't obvious to me how to resolve my problem with initializing SpaceProperty field. I post the code I have so far, but it's nonfunctional. If anyone else knows a solution, please let me know!


SetFontSizeProperty

#light
#I @"C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0"
#r @"WindowsBase.dll"
#r @"PresentationCore.dll"
#r @"PresentationFramework.dll"

open System
open System.Windows
open System.Windows.Controls
open System.Windows.Documents
open System.Windows.Input
open System.Windows.Media


(* From Chap 8 - SetFontSizeProperty.cs *)
type SetFontSizeProperty = class
inherit Window as base


new () as this = {} then
this.Title <- "Set FontSize Property";
this.SizeToContent <- SizeToContent.WidthAndHeight;
this.ResizeMode <- ResizeMode.CanMinimize;
this.FontSize <- 16.0
let fntsizes = [|8.0;16.0;32.0|]

// Create Grid Panel
let grid = new Grid()
this.Content <- grid

// Define row and columns
for i in [0..1] do
let row = new RowDefinition()
row.Height <- GridLength.Auto
grid.RowDefinitions.Add(row)

for i in [0..(fntsizes.Length-1)] do
let col = new ColumnDefinition()
col.Width <- GridLength.Auto
grid.ColumnDefinitions.Add(col)

// Create six buttons
for i in [0..(fntsizes.Length-1)] do
let btn = new Button()
btn.Content <- new TextBlock
(new Run("Set window FontSize to " + Float.to_string(fntsizes.[i])))
btn.Tag <- fntsizes.[i]
btn.HorizontalAlignment <- HorizontalAlignment.Center
btn.VerticalAlignment <- VerticalAlignment.Center
// Implement WindowFontSizeOnClick
btn.Click.Add
(fun _ -> this.FontSize <- (btn.Tag :?> double))
grid.Children.Add(btn) |> ignore
Grid.SetRow(btn,0)
Grid.SetColumn(btn,i)

let btn = new Button()
btn.Content <- new TextBlock
(new Run("Set button FontSize to " + Float.to_string(fntsizes.[i])))
btn.Tag <- fntsizes.[i]
btn.HorizontalAlignment <- HorizontalAlignment.Center
btn.VerticalAlignment <- VerticalAlignment.Center
// Implement ButtonFontSizeOnClick
btn.Click.Add
(fun _ -> btn.FontSize <- (btn.Tag :?> double))
grid.Children.Add(btn) |> ignore
Grid.SetRow(btn,1)
Grid.SetColumn(btn,i)
()
end

#if COMPILED
[<STAThread()>]
do
let app = new Application() in
app.Run(new SetFontSizeProperty()) |> ignore
#endif

SetSpaceProperty Example - broken

#light
#I @"C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0"
#r @"WindowsBase.dll"
#r @"PresentationCore.dll"
#r @"PresentationFramework.dll"

open System
open System.Text
open System.Windows
open System.Windows.Controls
open System.Windows.Input
open System.Windows.Media
//
(* From Chap 8 - SetSpaceProperty example with DependencyProperty *)
//
(* From Chap 8 - SpaceButton.cs *)
type SpaceButton = class
inherit Button as base

val mutable txt: string

new () as this = {txt = null}


static member initSpaceProperty =
let metadata = new FrameworkPropertyMetadata()
metadata.DefaultValue <- 1
metadata.AffectsMeasure <- true
metadata.Inherits <- true
metadata.PropertyChangedCallback <- new PropertyChangedCallback(SpaceButton.OnSpacePropertyChanged)
DependencyProperty.Register("Space",
typeof<int>,
typeof<SpaceButton>,
metadata,
// callback method for value validation
(fun obj -> let i = (obj :?> int) in (i >= 0)))

// I'm completely stumped...I don't know how to setup a static readonly property
// in F# and I don't know how to initialize SpaceProperty outside of SpaceButton
// class due to initialization dependency with SpaceButton.
// The closest thing I can find on the web is the following blog by Lewis Bruck
// http://blogs.msdn.com/lbruck/archive/2006/05/24/606653.aspx which also indicates
// that F# and SQL2005 CLR has problems because F# does not generate static readonly
// fields. Don Syme apparently has thought about this subject as he has published an
// article "An Alternative Approach to Initializing Mutually Referential Objects"
// to be found http://research.microsoft.com/~dsyme/papers/valrec-tr.pdf
static member SpaceProperty = SpaceButton.initSpaceProperty

static member OnSpacePropertyChanged (obj:DependencyObject) (args:DependencyPropertyChangedEventArgs) =
let btn = obj :?> SpaceButton
btn.Content <- btn.SpaceOutText btn.txt

member this.Text
with get() = this.txt
and set value =
this.txt <- value
this.Content <- this.SpaceOutText(this.txt)

member this.Space
with get() =
let value = this.GetValue(SpaceButton.SpaceProperty)
(value :?> int)
and set (value:int) = this.SetValue(SpaceButton.SpaceProperty,value)

member this.SpaceOutText (str:string) =

if (str <> null) then
let appendSpace c = String.of_char(c) + new string(' ',this.Space)
let build = String.map_concat appendSpace str
build
else
null

end


(* From Chap 8 - SpaceWindow.cs *)

type SpaceWindow = class
inherit Window as base

new () as this = {}

// A static DependencyProperty
static member SpaceProperty =
let metadata = new FrameworkPropertyMetadata()
metadata.Inherits <- true

// Add owner to SpaceProeprty & override metadata
let prop = SpaceButton.SpaceProperty.AddOwner(typeof<SpaceWindow>)
prop.OverrideMetadata(typeof<SpaceWindow>,metadata)
prop

member this.Space
with get() = (this.GetValue(SpaceWindow.SpaceProperty) :?> int)
and set (value:int) = this.SetValue(SpaceWindow.SpaceProperty,value)

end

(* From Chap 8 - SetSpaceProperty.cs *)

type SetSpaceProperty = class
inherit SpaceWindow as base

new () as this = {} then
this.Title <- "Set Space Property"
this.SizeToContent <- SizeToContent.WidthAndHeight
this.ResizeMode <- ResizeMode.CanMinimize
let iSpaces = [|0;1;2|]

let grid = new Grid()
this.Content <- grid

for i in [0..2] do
let row = new RowDefinition()
row.Height <- GridLength.Auto
grid.RowDefinitions.Add(row)

for i in [0..(iSpaces.Length-1)] do
let col = new ColumnDefinition()
col.Width <- GridLength.Auto
grid.ColumnDefinitions.Add(col)

for i in [0..(iSpaces.Length-1)] do
let btn = new SpaceButton();
btn.Text <- "Set window Space to " + Int32.to_string(iSpaces.[i])
btn.Tag <- iSpaces.[i];
btn.HorizontalAlignment <- HorizontalAlignment.Center
btn.VerticalAlignment <- VerticalAlignment.Center

//btn.Click += WindowPropertyOnClick;
btn.Click.Add(fun _ -> this.Space <- (btn.Tag :?> int))
grid.Children.Add(btn)|>ignore
Grid.SetRow(btn, 0)
Grid.SetColumn(btn, i)


let btn = new SpaceButton()
btn.Text <- "Set button Space to " + Int32.to_string(iSpaces.[i])
btn.Tag <- iSpaces.[i];
btn.HorizontalAlignment <- HorizontalAlignment.Center
btn.VerticalAlignment <- VerticalAlignment.Center

//btn.Click += ButtonPropertyOnClick
btn.Click.Add(fun _ -> btn.Space <- (btn.Tag :?> int))
grid.Children.Add(btn) |> ignore
Grid.SetRow(btn, 1)
Grid.SetColumn(btn, i)

end


#if COMPILED
[<STAThread()>]
do
let app = new Application() in
app.Run(new SetSpaceProperty()) |> ignore
#endif


1 comment:

dsyme said...

Hi John!

This is a known issue, which crops up particularly in GUI code, but there are nearly always workarounds. For example, how about using a

let mutable private initSpaceProperty : DependencyProperty = null

prior to the first class, then initializing on demand? Full code below (I switched to using implicit construction for the classes)

#light
#I @"C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0"
#r @"WindowsBase.dll"
#r @"PresentationCore.dll"
#r @"PresentationFramework.dll"

open System
open System.Text
open System.Windows
open System.Windows.Controls
open System.Windows.Input
open System.Windows.Media
//
(* From Chap 8 - SetSpaceProperty example with DependencyProperty *)
//
(* From Chap 8 - SpaceButton.cs *)

let mutable private initSpaceProperty : DependencyProperty = null

type SpaceButton() =
inherit Button() as base

let mutable txt = null


static member SpaceProperty =
if initSpaceProperty = null then
let metadata = new FrameworkPropertyMetadata(DefaultValue=1, AffectsMeasure=true, Inherits=true)
metadata.PropertyChangedCallback <- new PropertyChangedCallback(SpaceButton.OnSpacePropertyChanged)
initSpaceProperty <-
DependencyProperty.Register("Space",
typeof<int>,
typeof<SpaceButton>,
metadata,
// callback method for value validation
(fun obj -> let i = (obj :?> int) in (i >= 0)))

initSpaceProperty

static member OnSpacePropertyChanged (obj:DependencyObject) (args:DependencyPropertyChangedEventArgs) =
let btn = obj :?> SpaceButton
btn.Content <- btn.SpaceOutText btn.Text

member this.Text
with get() = txt
and set value =
txt <- value
this.Content <- this.SpaceOutText(txt)

member this.Space
with get() =
let value = this.GetValue(SpaceButton.SpaceProperty)
(value :?> int)
and set (value:int) = this.SetValue(SpaceButton.SpaceProperty,value)

member this.SpaceOutText (str:string) =

if (str <> null) then
let appendSpace c = String.of_char(c) + new string(' ',this.Space)
let build = String.map_concat appendSpace str
build
else
null


(* From Chap 8 - SpaceWindow.cs *)

type SpaceWindow() =
inherit Window() as base

// A static DependencyProperty
static member SpaceProperty =
let metadata = new FrameworkPropertyMetadata()
metadata.Inherits <- true

// Add owner to SpaceProeprty & override metadata
let prop = SpaceButton.SpaceProperty.AddOwner(typeof<SpaceWindow>)
prop.OverrideMetadata(typeof<SpaceWindow>,metadata)
prop

member this.Space
with get() = (this.GetValue(SpaceWindow.SpaceProperty) :?> int)
and set (value:int) = this.SetValue(SpaceWindow.SpaceProperty,value)


(* From Chap 8 - SetSpaceProperty.cs *)

type SetSpaceProperty() as this =
inherit SpaceWindow() as base

do this.Title <- "Set Space Property"
this.SizeToContent <- SizeToContent.WidthAndHeight
this.ResizeMode <- ResizeMode.CanMinimize
let iSpaces = [|0;1;2|]

let grid = new Grid()
this.Content <- grid

for i in [0..2] do
let row = new RowDefinition()
row.Height <- GridLength.Auto
grid.RowDefinitions.Add(row)

for i in [0..(iSpaces.Length-1)] do
let col = new ColumnDefinition()
col.Width <- GridLength.Auto
grid.ColumnDefinitions.Add(col)

for i in [0..(iSpaces.Length-1)] do
let btn = new SpaceButton(Text="Set window Space to " + Int32.to_string(iSpaces.[i]),
Tag=iSpaces.[i],
HorizontalAlignment=HorizontalAlignment.Center,
VerticalAlignment=VerticalAlignment.Center)

//btn.Click += WindowPropertyOnClick;
btn.Click.Add(fun _ -> this.Space <- (btn.Tag :?> int))
grid.Children.Add(btn)|>ignore
Grid.SetRow(btn, 0)
Grid.SetColumn(btn, i)


let btn = new SpaceButton(Text= "Set button Space to " + Int32.to_string(iSpaces.[i]),
Tag=iSpaces.[i],
HorizontalAlignment=HorizontalAlignment.Center,
VerticalAlignment=VerticalAlignment.Center)

//btn.Click += ButtonPropertyOnClick
btn.Click.Add(fun _ -> btn.Space <- (btn.Tag :?> int))
grid.Children.Add(btn) |> ignore
Grid.SetRow(btn, 1)
Grid.SetColumn(btn, i)


#if COMPILED
[<STAThread()>]
do
let app = new Application() in
app.Run(new SetSpaceProperty()) |> ignore
#endif