elmish
elmish copied to clipboard
How to pass an external event into a program?
Description
I use a port of Elmish.WPF to Uno and WinUI. So that I have the half Elmish with XAML views. UWP/iOS/Android heads can generate some events from the platform, like network connectivity change or app went to background or paused. So that I want to pass them into my root Elmish program.
Repro code
Now I do it this way: https://github.com/xperiandri/Elmish.Uno/blob/7e2b81ab927d010bdae86d7163bf47240c4e1f18/src/Templates/SolutionTemplate/SolutionTemplate/Helpers/Elmish.fs#L42
static member WithSubscription (program, subscribe) =
Program.withSubscription subscribe program
https://github.com/xperiandri/Elmish.Uno/blob/7e2b81ab927d010bdae86d7163bf47240c4e1f18/src/Templates/SolutionTemplate/SolutionTemplate.Shared/App.xaml.cs#L87
var program =
appProgram.Value.Program
.WithSubscription(AppProgram.GetLifecycleEventsSubscription(SubscribeToLifecycleEvents))
.WithSubscription(AppProgram.GetNetworkStatusSubscription(SubscribeToNetworkStatus));
https://github.com/xperiandri/Elmish.Uno/blob/7e2b81ab927d010bdae86d7163bf47240c4e1f18/src/Templates/SolutionTemplate/SolutionTemplate.Shared/App.xaml.cs#L208
private void SubscribeToLifecycleEvents(
Action<Exception, string, bool, Action<bool>> onUnhandledException,
Action<DateTimeOffset, Action> onSuspending,
Action<object> onResuming,
Action<Action> onEnteredBackground,
Action<Action> onLeavingBackground)
{
this.UnhandledException +=
(_, e) => onUnhandledException(e.Exception, e.Message, e.Handled, isHandled => e.Handled = isHandled);
#if HAS_UNO || NETFX_CORE
void OnSuspending(object sender, SuspendingEventArgs e)
{
var op = e.SuspendingOperation;
var deferral = op.GetDeferral();
onSuspending(op.Deadline, () => deferral.Complete());
}
this.Suspending += OnSuspending;
this.Resuming += (_, o) => onResuming(o);
...
https://github.com/xperiandri/Elmish.Uno/blob/7e2b81ab927d010bdae86d7163bf47240c4e1f18/src/Templates/SolutionTemplate/SolutionTemplate/Programs/App.Program.fs#L121
static member GetLifecycleEventsSubscription (
addHandlers : Action<Action<exn, string, bool, Action<bool>>,
Action<DateTimeOffset, Action>,
Action<obj>,
Action<Action>,
Action<Action>>)
: Model -> Cmd<ProgramMessage<RootMsg, Msg>> =
fun (_ : Model) ->
fun (dispatch : Dispatch<ProgramMessage<RootMsg, Msg>>) ->
addHandlers.Invoke (
(fun ex message handled setHandled ->
UnhandledException (ex, message) |> Local |> dispatch;
setHandled.Invoke true),
(fun deadline completed -> Suspend (deadline, completed) |> Local |> dispatch),
(fun o -> Resuming |> Local |> dispatch),
(fun completed -> (EnteredBackground completed) |> Local |> dispatch),
(fun completed -> (LeavingBackground completed) |> Local |> dispatch))
|> Cmd.ofSub
Which looks quite ugly.
Expected and actual results
Some method on Elmish program to dispatch an Elmish Cmd.
var program = appProgram.Value.Program;
void OnSuspending(object sender, SuspendingEventArgs e)
{
var op = e.SuspendingOperation;
var deferral = op.GetDeferral();
program.Dispatch(Cmd.OfMsg(new Msg.Suspend(e.Deadline, () => deferral.Complete())));
}
this.Suspending += OnSuspending;
Related information
- Elmish version: 3.1
- Platforms: UWP, WinUI, Uno Platform
Have you tried using
https://github.com/elmish/elmish/blob/44231c1ff132d17b3df531579bfe6ab0ac25d137/src/program.fs#L54
?
This is exactly what I do
static member WithSubscription (program, subscribe) =
Program.withSubscription subscribe program
I've just created an extension method to be used in C#
And you can see the final result with is totally unreadable.
Oh, I now see that you had mentioned the line I quoted at the beginning of your "Repo code" section. Sorry for missing that.
I am still confused though. Your title is
How to pass an external event into a program?
but based on your OP and especially this line
Which looks quite ugly.
my impression is that you are frustrated with some interop between C# and F#.
- Are you asking how to pass an external event into a program, or
- do you know how to pass an external event into a program and are asking how to make such code look good?
The second one
Can you share a GitHub repo with a minimal reproducible example?
You can create my template by installing
dotnet new -i "Elmish.Uno.ProjectTemplates.Dotnet6::1.0.0-ci-*" --nuget-source "https://www.myget.org/F/elmish_uno/api/v3/index.json"
from here https://www.myget.org/gallery/elmish_uno
You need 2 files:
- App.Program.fs
- App.xaml.cs
Is my answer a working approach?
I am confused. I thought you already had a working approach and were just looking for a working approach that is also good looking.
I have a working approach and it looks ugly. If you create a project from template you can see it
Can you share a link to a GitHub repo containing your working but ugly approach?
Yes, https://github.com/xperiandri/Elmish.Uno/tree/NET_6/src/Templates/SolutionTemplate
You can create my template by installing
dotnet new -i "Elmish.Uno.ProjectTemplates.Dotnet6::1.0.0-ci-*" --nuget-source "https://www.myget.org/F/elmish_uno/api/v3/index.json"from here https://www.myget.org/gallery/elmish_uno
Same code
Sorry my delay.
I just looked at your repo. It seems far from a minimal reproducible example to me.
Can you share a GitHub repo containing a minimal reproducible example of your working but ugly approach?
This is the smallest possible reproduction ElmishCrazyInterop.zip
Great. Can you put it in a GitHub repo and share a link to it?
https://github.com/xperiandri/Elmish.CrazyInterop
I have having problems getting your example to compile.
First, Visual Studio says I need to install something, but when I click "install", it says I already have it installed.

Second, the code doesn't compile for me.

I was expecting that a minimal working example of your issue would not involve any GUI; that a console interface would suffice. Is my expectation accurate? Can you make a working example (that is more minimal) by having the UI be a console instead of a GUI?
Just switch target version to whatever SDK version you have installed
See the last one SDK you have

Choose the maximum one
I removed unnecessary dependencies. Now it must work for you
Now I do it this way:
All three of your links work, but I can't check them out. GitHub says
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Can you replace those links with links that exist in the underlying repo?
static member GetLifecycleEventsSubscription ( addHandlers : Action<Action<exn, string, bool, Action<bool>>, Action<DateTimeOffset, Action>, Action<obj>, Action<Action>, Action<Action>>) : Model -> Cmd<ProgramMessage<RootMsg, Msg>> = fun (_ : Model) -> fun (dispatch : Dispatch<ProgramMessage<RootMsg, Msg>>) -> addHandlers.Invoke ( (fun ex message handled setHandled -> UnhandledException (ex, message) |> Local |> dispatch; setHandled.Invoke true), (fun deadline completed -> Suspend (deadline, completed) |> Local |> dispatch), (fun o -> Resuming |> Local |> dispatch), (fun completed -> (EnteredBackground completed) |> Local |> dispatch), (fun completed -> (LeavingBackground completed) |> Local |> dispatch)) |> Cmd.ofSubWhich looks quite ugly.
I think this looks ugly because you have bundled five otherwise unrelated things together into one command subscription.
Have you tried splitting those into five separate command subscriptions? If so, does that look less ugly to you?
Yes, I tried. No it does not look less ugly
Can you replace those links with links that exist in the underlying repo?
Do you mean the links in the first message?
Do you mean the links in the first message?
Yes
If you want a branch use NET_6
I rebased it. You strictly need that link to direct you to the branch, I can do that.
But it is not the last rebase. So they will change again
The Action-based structure is hard to understand. I would move each Sub into a module function. I tend to call this module Fx, meaning effects aka side-effects.
module Fx =
let resume . . . : Sub<Msg> =
fun dispatch ->
. . . // side effects
dispatch Resuming
let otherEffect : Sub<Msg> =
fun dispatch ->
dispatch . . .
. . .
Then use those individual effects as needed.
let update msg model : Model * Cmd<Msg> =
match msg with
| . . . ->
{ model with . . . },
[ Fx.resume . . .
Fx.otherEffect ]
// and/or
let subscribe model : Cmd<Msg> =
[ Fx.resume . . .
Fx.otherEffect ]