Changing an Elmish.WPF model from inside an async function

165 views Asked by At

What is the accepted way in Elmish.WPF to have a Binding.cmd calling an async computation expression (CE) allow the delayed result of the async CE change the shared top-level model?

I want to do this without causing the UI thread to hang or starve (though having it somehow show busy is fine).

I tried having a part of the Model be mutable and mutating just that part of the record inside the async CE. This failed probably because the async CE is operating on its own copy of the Model.

Is there a way to pass a message with the delayed new value up to the top-level update function and update the global shared Model?

The functioning test code is in a repo here: make 'FandCo_SingleCounter` the Startup Project to run and test in VS2022

The important bits of my code is:

MainWindow.XAML fragment:

...begin of <Window>...
    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Top" Margin="0,10,0,0">
        <TextBlock Text="{Binding DISPLAY_state}" Width="110" Margin="0,5,10,5" />
        <Button Command="{Binding CMD_get_state}" Content="DO IT!" Width="50" Margin="0,5,10,5" />
    </StackPanel>
...to end of </Window>...

Program.fs fragment(s):

type Model =
  { mutable DISPLAY_state: string
   }

type Msg =
  | CMD_get_state

let init =
  { DISPLAY_state = "foo"
   }

let ASYNC_get_state (m: Model) :Model =
  async {
      Console.WriteLine( "async 0: "
                          + System.DateTime.Now.Millisecond.ToString()
                          + " - "
                          + m.DISPLAY_url_state)
      m.DISPLAY_url_state <- "bar"
      Console.WriteLine( "async +: "
                          + System.DateTime.Now.Millisecond.ToString()
                          + " - "
                          + m.DISPLAY_state)
      Threading.Thread.Sleep(5000)
      Console.WriteLine( "async ++: "
                          + System.DateTime.Now.Millisecond.ToString()
                          + " - "
                          + m.DISPLAY_state)
     }
     |> Async.StartImmediate
  Console.WriteLine( "m.Display_url_state 0: "
                 + System.DateTime.Now.Millisecond.ToString()
                 + " - "
                 + m.DISPLAY_state)
  Console.WriteLine( "m.Display_state 1: "
                 + System.DateTime.Now.Millisecond.ToString()
                 + " - "
                 + m.DISPLAY_state)
  { m with DISPLAY_state = "baz" }

let update msg m =
  | CMD_get_state => ASYNC_get_state m

let bindings () : Binding<Model, Msg> list = [
    "DISPLAY_url_state" |> Binding.oneWay (fun m -> m.DISPLAY_url_state)
    "CMD_get_url" |> Binding.cmd CMD_get_url
  ]
...etc...to end of Elmish.WPF Core F# file.

The result of this is:

async 0: 275 - foo
async +: 591 - bar
async ++: 160 - True
m.Display_state 0: 164 - True
m.Display_state 1: 167 - True
[13:26:18 VRB] New message: CMD_get_state
Updated state:
{ DISPLAY_url_state = "baz" }
[13:26:18 VRB] [main] PropertyChanged DISPLAY_state
[13:26:18 VRB] [main] TryGetMember DISPLAY_state

At the end DISPLAY_state binding in the MainWindow.XAML updates to the value baz and not the desired value bar.

How is this supposed to be done?

1

There are 1 answers

0
Tyson Williams On

I am the maintainer of Elmish.WPF.

Is there a way to pass a message with the delayed new value up to the top-level update function and update the global shared Model?

This is not exactly a question specific to Elmish.WPF in the sense that all derivates of Elmish (and in fact all MVU architectures) have to provide a way to appropriately execute async code that returns a message.

There are two ways to do this with Elimsh (and thus Elmish.WPF):

  1. A subscription. See the SubModel sample and especially this line.
  2. An (Elmish) command. See the FileDialogs sample and especially these lines.

The Elmish.WPF binding named cmd has nothing to do with this. This binding is named after the WPF interface ICommand.