четверг, 18 марта 2010 г.

F# and WPF or how to make life a bit easier

I like F#. It combines power of functional programming and features of OO languages so you can mix together most convinient features from both worlds. Ex facte it can appear as a esotheric language with unfriendly syntax (especially for fans of C-like languages) but after first week of using it you begin to notice the verbosity of other languages. In fact learning ways of solving problems in F# can make you better programmer on any imperative language you use, because it enforces another way of viewing the task.

Unfortunatly all pleasure of using F# is partially spoiled by level of language support in Visual Studio (comparing to C#). Absence of solution folders, requirement to order files in project (yes, I completely understand the reason but this doesn't mean that I like it), poor navigation possibilities, lack of designer support in WinForms and WPF - and I can keep on...

Not so long ago I was working of small F# script (excellent feature BTW) that performs some data processing and displays summary (using WPF)at the end. All the WPF samples I met in the web utilize object model to create UI, but let's make a confession: making relatively complex UI with object model sucks. It is possible to create complicted user interface from the code but this will take much more efforts than if we use XAML and designer. So I made a small helper that wires up together UI definition in XAML and externally defined behavior, i.e. event handlers.

module FSWpf

#r "WindowsBase"
#r "PresentationCore"
#r "PresentationFramework"
#r "System.Xaml"

open System
open System.Windows

[<AttributeUsage(AttributeTargets.Field, AllowMultiple = false)>]
type UiElementAttribute(name : string) =
inherit System.Attribute()
new() = new UiElementAttribute(null)
member this.Name = name

[<AbstractClass>]
type FsUiObject<'T when 'T :> FrameworkElement> (xamlPath) as this =
let loadXaml () =
use stream = System.IO.File.OpenRead(xamlPath)
System.Windows.Markup.XamlReader.Load(stream)
let uiObj = loadXaml() :?> 'T

let flags = System.Reflection.BindingFlags.Instance ||| System.Reflection.BindingFlags.NonPublic ||| System.Reflection.BindingFlags.Public

do
let fields =
this.GetType().GetFields(flags)
|> Seq.choose(fun f ->
let attrs = f.GetCustomAttributes(typeof<UiElementAttribute>, false)
if attrs.Length = 0 then None
else
let attr = attrs.[0] :?> UiElementAttribute
Some(f, if String.IsNullOrEmpty(attr.Name) then f.Name else attr.Name)
)
for field, name in fields do
let value = uiObj.FindName(name)
if value <> null then
field.SetValue(this, value)
else
failwithf "Ui element %s not found" name

member x.UiObject = uiObj

The helper is very small and simple: it loads XAML file, iterates over annotated fields (should be declared by inheritors) and puts reference to UI element into corresponding field.

XAML file can be created with File -> New -> File -> Xml File. Just change extension to xaml and add declaration of required namespaces in the beginning. After that you can use VS designer. My sample xaml file is not the beautiful masterpiece, its rather the opposite but it demonstrates the simplicity of creating complex UI parts. Believe me, the same thing expressed in code looks much more confusing.

<Window  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Main window" Width="200" Height="300" SizeToContent="WidthAndHeight">
<StackPanel Margin="10">
<Rectangle
Name="MyRectangle"
Width="100"
Height="100"
Fill="Blue">
<Rectangle.Triggers>
<!-- Animates the rectangle's opacity. -->
<EventTrigger RoutedEvent="Rectangle.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="MyRectangle"
Storyboard.TargetProperty="Opacity"
From="1.0" To="0.0" Duration="0:0:5"
AutoReverse="True" RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Rectangle.Triggers>
</Rectangle>
<Button Content="Run!!!" Height="23" Width="75" x:Name="run">
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="run"
Storyboard.TargetProperty="Width"
From="50.0" To="10.0" Duration="0:0:5"
AutoReverse="True" RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
<TextBox Height="23" Width="120" x:Name="text">
</TextBox>
</StackPanel>
</Window>

Finally: usage

#load "FsUiObject.fsx"

open FSWpf

open System.Windows
open System.IO

type MainWindow() as this =
inherit FsUiObject<Window>(Path.Combine(__SOURCE_DIRECTORY__, "MainWindow.xaml"))

[<DefaultValue>]
[<UiElement("run")>]
val mutable runButton : Controls.Button

[<DefaultValue>]
[<UiElement>]
val mutable text : Controls.TextBox

let clickHandler _ =
let txt = this.text.Text
MessageBox.Show(this.UiObject, txt) |> ignore

do
this.runButton.Click.Add(clickHandler)
let window = new MainWindow()
window.UiObject.ShowDialog() |> ignore

Derived class just declares fields (runButton and text) and base class (FsUiObject) is responsible for mapping this fields to UI elements. That’s all.

Комментариев нет:

Отправить комментарий

 
GeekySpeaky: Submit Your Site!