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?
I am the maintainer of Elmish.WPF.
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):
SubModel
sample and especially this line.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 interfaceICommand
.