Bind to TextChanged event for TextBox in F# WPF MVVM application

1.3k Views Asked by At

I'd like to bind to a WPF TextBox in an MVVM F# application built using FsXaml and FSharp.ViewModule. I added a command called "SetA" to the application described here, and attempted to bind to it with the following XAML:

<TextBox Text="{Binding Score.ScoreA, Mode=OneWay}" FontFamily="Lucida Console">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="TextChanged">
            <fsx:EventToCommand Command="{Binding SetA}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
</TextBox>

The TextBox is correctly populated from the ScoreA property, but the SetA command is not called when I type a new value in the TextBox. I'm comfortable with F#, but this is my first WPF MVVM application, so I'm not sure what I'm doing wrong. How do I trigger my SetA handler when the user changes the text value?

This is my ViewModel:

type MainViewModel(controller : Score -> ScoringEvent -> Score) as self = 
    inherit EventViewModelBase<ScoringEvent>()

    let score = self.Factory.Backing(<@ self.Score @>, Score.zero)

    let eventHandler ev =
        score.Value <- controller score.Value ev

    do
        self.EventStream
        |> Observable.add eventHandler

    member this.IncA = this.Factory.EventValueCommand(IncA)
    member this.DecA = this.Factory.EventValueCommandChecked(DecA, (fun _ -> this.Score.ScoreA > 0), [ <@@ this.Score @@> ])
    member this.IncB = this.Factory.EventValueCommand(IncB)
    member this.DecB = this.Factory.EventValueCommandChecked(DecB, (fun _ -> this.Score.ScoreB > 0), [ <@@ this.Score @@> ])
    member this.NewGame = this.Factory.EventValueCommand(New)
    member this.SetA = this.Factory.EventValueCommand(SetA)

    member __.Score = score.Value 

I only added the one line that defines SetA.

1

There are 1 best solutions below

2
On

Most likely the Command is being called, but the result is not what you'd expected.

For testing purposes try

member this.SetA = 
   this.Factory.CommandSync(
        fun _ -> System.Windows.MessageBox.Show("TextChanged","RoutedEv") |> ignore)

Note the CommandSync instead of EventValueCommand. No matter how you change the score, a msgbox will pop up.

You could also keep the EventValueCommand

member this.SetA = this.Factory.EventValueCommand(SetA)

and do the same in the update fun

let update score event =
    match event with
    | IncA -> {score with ScoreA = score.ScoreA + 1}
    | DecA -> {score with ScoreA = max (score.ScoreA - 1) 0}
    | IncB -> {score with ScoreB = score.ScoreB + 1}
    | DecB -> {score with ScoreB = max (score.ScoreB - 1) 0}
    | New -> zero 
    | SetA -> System.Windows.MessageBox.Show("TextChanged","RoutedEv") |> ignore ; score

Getting back to the not what you'd expected part, I assume you want the score in the scoreboard to update, when you change the TextBox.Text value.

As you've noticed there's no (pure) way to bring the TextBox.Text value into the update fun using the current controller. Due to the fact that the SetX behaviour isn't fix, unlike in the case of the IncX, DecX or New events.

Options:

  • You could change the signature of the controller to Score -> ScoringEvent -> string option -> Score and use the option value to handle SetX events in the update fun.

  • You could handle everything in the ViewModel. The immutable nature of the record forces the use of a OneWay Binding. I would keep it immutable, but you can (de)compose everything in the ViewModel.

Try

ViewModel

type MainViewModel(controller : Score -> ScoringEvent -> Score) as self = 
    inherit EventViewModelBase<ScoringEvent>()

    let scoreA = self.Factory.Backing(<@ self.ScoreA @>, 0)
    let scoreB = self.Factory.Backing(<@ self.ScoreB @>, 0)

    let updateVM score = 
        scoreA.Value <- score.ScoreA
        scoreB.Value <- score.ScoreB

    let eventHandler ev = 
        updateVM <| controller {ScoreA = scoreA.Value ; ScoreB = scoreB.Value} ev

    do
        self.EventStream
        |> Observable.add eventHandler

    member this.IncA = this.Factory.EventValueCommand(IncA)
    member this.DecA = this.Factory.EventValueCommandChecked(DecA, (fun _ -> this.ScoreA > 0), [ <@@ this.ScoreA @@> ])
    member this.IncB = this.Factory.EventValueCommand(IncB)
    member this.DecB = this.Factory.EventValueCommandChecked(DecB, (fun _ -> this.ScoreB > 0), [ <@@ this.ScoreB @@> ])
    member this.NewGame = this.Factory.EventValueCommand(New)

    member __.ScoreA
        with get() = scoreA.Value 
         and set v = scoreA.Value <- v
    member __.ScoreB
        with get() = scoreB.Value 
         and set v = scoreB.Value <- v

XAML

<DockPanel LastChildFill="True">
    <StackPanel DockPanel.Dock="Bottom" Height="75" Orientation="Horizontal" Background="#FF3A3A3A">
        <Button Height="70" Width="70" Margin="2" Command="{Binding NewGame}">
            <TextBlock Text="New" FontSize="18"></TextBlock>
        </Button>
        <TextBox Text="{Binding ScoreA, UpdateSourceTrigger=PropertyChanged}" 
                 Margin="2" FontSize="18" HorizontalContentAlignment="Center" VerticalContentAlignment="Center">
        </TextBox>
    </StackPanel>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <Border Background="#FF024D70" Grid.Column="0">
            <Grid>
                <Viewbox>
                    <Label Content="{Binding ScoreA}" ContentStringFormat="D2" FontFamily="Lucida Console"></Label>
                </Viewbox>
                <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
                    <Button Command="{Binding DecA}"  Height="70" Width="70" VerticalAlignment="Bottom" Opacity="0.5" Margin="20">
                        <TextBlock Text="-" FontSize="36" Height="60"></TextBlock>
                    </Button>
                    <Button Command="{Binding IncA}"  Height="70" Width="70" VerticalAlignment="Bottom" Opacity="0.5" Margin="20">
                        <TextBlock Text="+" FontSize="36" Height="60"></TextBlock>
                    </Button>
                </StackPanel>
            </Grid>
        </Border>
        <Border Background="#FF7E0E03" Grid.Column="1">
            <Grid>
                <Viewbox>
                    <Label Content="{Binding ScoreB}" ContentStringFormat="D2"  FontFamily="Lucida Console"></Label>
                </Viewbox>
                <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
                    <Button Command="{Binding DecB}"  Height="70" Width="70" VerticalAlignment="Bottom" Opacity="0.5" Margin="20">
                        <TextBlock Text="-" FontSize="36" Height="60"></TextBlock>
                    </Button>
                    <Button Command="{Binding IncB}"  Height="70" Width="70" VerticalAlignment="Bottom" Opacity="0.5" Margin="20">
                        <TextBlock Text="+" FontSize="36" Height="60"></TextBlock>
                    </Button>
                </StackPanel>
            </Grid>
        </Border>
    </Grid>
</DockPanel>

Notes

  • Score.ScoreX changed to ScoreX
  • Text="{Binding Score.ScoreA, Mode=OneWay}" changed to Text="{Binding ScoreA, UpdateSourceTrigger=PropertyChanged}"
  • Removed the Interaction.Trigger