Seiten

Samstag, 5. Mai 2012

Lazy Levenshtein Visualisation. F# WPF MVVM.

Hier habe ich eine F# WPF(MVVM) Anwendung geschrieben um den Lazy Levenshtein Algorithmus zu visualisieren.



Code on GitHub

Ein paar Sätze, wie man das "Spielfeld" mit dem WPF-MVVM implementieren kann. Ich habe dazu ein empfehlenswertes F# MVVM Template genommen.
Als erstes die ViewModelBase-Klasse auf die Implementierung mit F# Quotations umgestellt.
//ViewModelBase.fs
namespace LazyLevenshteinMVVM.ViewModel

open System

open System.ComponentModel
open Microsoft.FSharp.Quotations
open Microsoft.FSharp.Quotations.Patterns
// implement the INotifyPropertyChanged interface with F# quotations.
type ViewModelBase() =
    let propertyChangedEvent = new Event<PropertyChangedEventHandler, PropertyChangedEventArgs>()
    interface INotifyPropertyChanged with
        [<CLIEvent>]
        member x.PropertyChanged = propertyChangedEvent.Publish
    member x.OnPropertyChanged (expr: Expr) = 
        match expr with
        | PropertyGet(_, methodInfo, _) ->
            let propertyName = methodInfo.Name
            propertyChangedEvent.Trigger(x, new PropertyChangedEventArgs(propertyName))
        | other -> failwith "not implemented" 
Als nächstes brauchen wir zwei View Model Klassen: eine für eine Zelle des Spielfeldes und eine für das Spielfeld selbst.
//BoardViewModel.fs
namespace LazyLevenshteinMVVM.ViewModel

open System
open System.Windows
open System.Windows.Threading
open System.Windows.Input
open System.ComponentModel
open System.Collections.ObjectModel
open LazyLevenshteinMVVM.Model

//View Model for a board cell.
type BoardCell() =
    inherit ViewModelBase()
    let mutable cellValue = ""
    let mutable isEvaluated = false
    let mutable isEvaluatedFrom =false
    let mutable isEvaluate = false
    let mutable isThunk = false
    let mutable isDiagStep = false
    let mutable evaluateText = String.Empty
    let mutable evaluatedFrom = String.Empty

    member x.FromPoint (point : LazyLevenshteinMVVM.Model.Point) =
        match x.IsEvaluated with
        | true -> 
            match point.isDiagonalStep, point.isEvaluatedFrom with 
            | true, true ->
                x.IsDiagStep <- true
                x.IsEvaluatedFrom <- true
            | true, false ->  x.IsDiagStep <- true
            | false, true ->  x.IsEvaluatedFrom <- true
            | _ -> ()
        | false ->
            x.IsEvaluate <- point.isEvaluate
            x.IsDiagStep <- point.isDiagonalStep
            x.IsThunk <- point.isThunk
            match point.value with
            | Some v -> 
                match point.isEvaluate, point.isEvaluated, point.isEvaluatedFrom, point.isThunk with
                | true, _, _, _ ->   x.EvaluateText <- v
                | _, true, _, _ -> 
                    x.IsEvaluated <- point.isEvaluated
                    x.EvaluatedFrom <- point.evaluatedFrom
                    x.CellValue <- v
                |_, _, true, _ ->    
                    x.IsEvaluatedFrom <- point.isEvaluatedFrom
                | _, _, _, true -> 
                    x.IsThunk <- point.isThunk
                    x.CellValue <- v
                |_->()
            | None -> ()
    member x.IsEvaluated 
        with get() = isEvaluated
        and set value = 
            isEvaluated <- value
            x.OnPropertyChanged(<@x.IsEvaluated@>)
    member x.IsEvaluate 
        with get() = isEvaluate
        and set value = 
            isEvaluate <- value
            x.OnPropertyChanged(<@x.IsEvaluate@>)
    member x.IsThunk 
        with get() = isThunk
        and set value = 
            isThunk <- value
            x.OnPropertyChanged(<@x.IsThunk@>)
    member x.IsDiagStep 
        with get() = isDiagStep
        and set value = 
            isDiagStep <- value
            x.OnPropertyChanged(<@x.IsDiagStep@>)
    //computed edit distance for given letters from string A and B.
    member x.CellValue
        with get() = cellValue
        and set value = 
            cellValue <- value
            x.OnPropertyChanged(<@x.CellValue@>)
    //shows the letters for which a calculation should evaluate.
    member x.EvaluateText
        ...
    //describes how the value was computed. 
    //An element depends only on the elements to the west, north-west and north
    member.EvaluatedFrom
        ....    
    member x.IsEvaluatedFrom
        ...
Das Spielfeld.
Wenn man den Text in Eingabefelder ändert, wird das Bord sofort über den Aufruf von der BuildBoard-Methode neu erstellt und die Animation mit der Hilfe von DispatcherTimer neu gestartet. Über die GridRows-Eigenschaft werden die Änderungen an das View weitergeleitet.
// View Model for a board.
type Board() as x =
    inherit ViewModelBase()
        
    let mutable rows = new ObservableCollection<ObservableCollection<BoardCell>>()
    let mutable sizex = 0
    let mutable sizey = 0
    let mutable textA = String.Empty
    let mutable textB = String.Empty

    let model = LevenshteinModel.Empty
    let mutable timer = new DispatcherTimer(DispatcherPriority.Normal)
    let mutable distance = 0
    let pointToCell (point : LazyLevenshteinMVVM.Model.Point)  =
                let col = rows.[snd point.xy + 2] 
                let cell = col.[fst point.xy + 2]
                cell.FromPoint point
                cell.IsEvaluatedFrom <- false
                cell.IsDiagStep <- false
                col.[fst point.xy + 2] <- cell
                rows.[snd point.xy + 2] <- col       
    do
        timer.Interval <- new TimeSpan(0, 0, 0, 0, 1500)
        timer.Tick.Add(fun _  -> x.AnimateOneStep () )    
    
    member x.AnimateOneStep () = 
        match rows.Count, model.OneStep () with 
        | _, [] -> timer.Stop()
        | 0, _ -> timer.Stop()
        | _, points ->
            points |> List.iter pointToCell 
            x.OnPropertyChanged(<@x.GridRows@>)
    //build the board.
    member private x.BuildBoard  =
        [for i in 0..sizex-2 do
                rows.[0].[i+2].CellValue <- textA.[i].ToString()
                rows.[1].[i+2].CellValue <- (i+1).ToString() 
                rows.[1].[i+2].IsEvaluated<-true
        ] |> ignore

        rows.[1].[1].CellValue <- "0" 
        [for j in 2..sizey do  
              let c = rows.[j].[0]
              let n = rows.[j].[1]
              c.CellValue <- textB.[j-2].ToString()
              n.CellValue <- (j - 1).ToString()
              n.IsEvaluated<-true
        ] |> ignore
        timer.Stop()

        model.Run(textA, textB)
        match model.distance with
        | None -> x.Distance <- 0
        | Some v -> x.Distance <- v
        x.OnPropertyChanged(<@x.GridRows@>)
        timer.Start()

    member x.TextA
        with get() = textA
        and set value = 
            textA <- value
            x.SizeX <- (if textA.Length > 0 then textA.Length + 1 else 0)
            x.BuildBoard
            x.OnPropertyChanged(<@x.TextA@>)
            
    member x.TextB
        with get() = textB
        and set value = 
            textB <- value
            x.SizeY <- (if textB.Length > 0 then textB.Length + 1 else 0)
            x.BuildBoard
            x.OnPropertyChanged(<@x.TextB@>)
    member x.Distance 
        with get() = distance
        and set value = 
            distance <- value
            x.OnPropertyChanged(<@x.Distance@>)

    member x.GridRows = rows

    member private x.Size =
        sizex <- (if sizex > 0 then sizex else (if sizey > 0 then 1 else 0))
        sizey <- (if sizey > 0 then sizey else (if sizex > 0 then 1 else 0))
        [   for i in 0..sizey do
                let col = new ObservableCollection<BoardCell>()
                for j in 0..sizex do
                    let c = new BoardCell()
                    c.CellValue <-""
                    col.Add(c)

                rows.Add(col) ] |> ignore  
    member x.SizeY
        with get() = sizey
        and set value = 
            sizey <- value
            rows <- new ObservableCollection<ObservableCollection<BoardCell>>()
            x.Size
            x.OnPropertyChanged(<@x.SizeY@>)
    member x.SizeX
        with get() = sizex
        and set value = 
            sizex <- value
            rows <- new ObservableCollection<ObservableCollection<BoardCell>>()
            x.Size
            x.OnPropertyChanged(<@x.SizeX@>)
Das View.
<!--Board.xaml--> 
<UserControl 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:ViewModel="clr-namespace:LazyLevenshteinMVVM.ViewModel;assembly=App"
 HorizontalAlignment ="Stretch"
 HorizontalContentAlignment ="Stretch"
 VerticalAlignment ="Stretch"
 VerticalContentAlignment ="Stretch"
 Foreground="White">
    <UserControl.DataContext>
        <ViewModel:Board> </ViewModel:Board>
    </UserControl.DataContext>
    <UserControl.Resources>
        <Storyboard x:Key="selectedStory">
            <DoubleAnimation Storyboard.TargetName="TextBlock"
                                  Storyboard.TargetProperty="Opacity"
                                  From="0"
                                  To="0.8"
                                  Duration="0:0:0.8"/>
        </Storyboard>
        <LinearGradientBrush x:Key="BoardBackground" StartPoint="0,0" EndPoint="1,0">
            ...
        </LinearGradientBrush>
        <DataTemplate x:Key ="CellTemplate" >
                <Border x:Name ="Border" BorderBrush ="DimGray" BorderThickness ="1">
                    <Border.Background>
                       ...
                    </Border.Background>
                <StackPanel Orientation="Vertical" >
                    <TextBlock x:Name="evaluateText" Text="{Binding Path=EvaluateText}"/>
                    <TextBox x:Name ="TextBlock" Margin="3"
                     FontWeight ="Bold" FontSize ="12"  Text ="{Binding Path=CellValue}" 
                     HorizontalAlignment ="{Binding ElementName=Border, Path=HorizontalAlignment}" 
                     VerticalAlignment ="Center"
                             Focusable ="False" Opacity="0.8" Background="White">
                        <TextBox.BitmapEffect>
                            <DropShadowBitmapEffect/>
                        </TextBox.BitmapEffect>
                    </TextBox>
                    <TextBlock x:Name="evaluateFrom" Text="{Binding Path=EvaluatedFrom}" 
                                  FontSize="11" />
                </StackPanel>
            </Border>             
            <DataTemplate.Triggers>        
                <DataTrigger Binding ="{Binding IsEvaluated}" Value ="True">
                    <Setter TargetName ="TextBlock" Property ="Foreground" Value="Red"/>
                    <Setter TargetName ="TextBlock" Property ="Background" Value="Blue"/>
                    <DataTrigger.EnterActions>
                        <BeginStoryboard Storyboard="{StaticResource selectedStory}">
                        </BeginStoryboard>
                        <BeginStoryboard x:Name="evaluatedStory">
                            <Storyboard>
                                <DoubleAnimation Storyboard.TargetName="TextBlock"
                                  Storyboard.TargetProperty="FontSize"
                                  From="12"
                                  To="22"
                                  Duration="0:0:0.8" AutoReverse="True" />
                            </Storyboard>
                        </BeginStoryboard>
                    </DataTrigger.EnterActions>
                </DataTrigger>
                
                <DataTrigger Binding ="{Binding IsThunk}" Value ="True">
                    <Setter TargetName ="TextBlock" Property ="Background" Value="LightGreen"/>
                </DataTrigger>
                
                <DataTrigger Binding="{Binding IsEvaluatedFrom}" Value="True">
                    ...
                </DataTrigger>
                
                <DataTrigger Binding="{Binding IsEvaluate}" Value="True">
                    ...
                </DataTrigger>
                
                <DataTrigger Binding="{Binding IsDiagStep}" Value="True">
                    ...
                </DataTrigger>
            </DataTemplate.Triggers>
        </DataTemplate>
        <DataTemplate x:Key ="BoardTemplate">
            <ItemsControl ItemTemplate ="{StaticResource CellTemplate}" ItemsSource ="{Binding}">
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <UniformGrid Rows ="1"/>
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
            </ItemsControl>
        </DataTemplate>        
    </UserControl.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="6*" />
            <ColumnDefinition Width="1*" />
        </Grid.ColumnDefinitions>
        <ItemsControl Grid.Column="0" ItemTemplate ="{StaticResource BoardTemplate}" 
                         ItemsSource ="{Binding Path=GridRows}" x:Name ="MainList">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <UniformGrid Columns ="1" Background="{StaticResource BoardBackground}"/>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
        </ItemsControl>
        <StackPanel Grid.Column="1" Orientation="Vertical" Margin="5">
            <Label Content="Text 1" />
            <TextBox MinWidth="10" MaxHeight="25" MaxLength="12" MaxWidth="200" 
                        Text="{Binding Path=TextA,UpdateSourceTrigger=PropertyChanged}"></TextBox>
            <Label Content="Text 2" />
            <TextBox MinWidth="10" MaxHeight="25" MaxLength="12" MaxWidth="200" 
                        Text="{Binding Path=TextB,UpdateSourceTrigger=PropertyChanged}"></TextBox>
            <Label  Content="N - North" />
            <Label Content="W - West" />
            <Label Content="NW - North West" />
            <Label Content="Distance"  Margin="5" />
            <TextBox  Margin="5" Text="{Binding Path=Distance}" />
        </StackPanel>
    </Grid>
</UserControl>