elmish icon indicating copy to clipboard operation
elmish copied to clipboard

How to pass an external event into a program?

Open xperiandri opened this issue 3 years ago • 30 comments

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

xperiandri avatar Apr 19 '22 16:04 xperiandri

Have you tried using

https://github.com/elmish/elmish/blob/44231c1ff132d17b3df531579bfe6ab0ac25d137/src/program.fs#L54

?

TysonMN avatar Apr 19 '22 17:04 TysonMN

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.

xperiandri avatar Apr 19 '22 17:04 xperiandri

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?

TysonMN avatar Apr 20 '22 01:04 TysonMN

The second one

xperiandri avatar Apr 20 '22 08:04 xperiandri

Can you share a GitHub repo with a minimal reproducible example?

TysonMN avatar Apr 20 '22 10:04 TysonMN

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

xperiandri avatar Apr 20 '22 11:04 xperiandri

You need 2 files:

  • App.Program.fs
  • App.xaml.cs

xperiandri avatar Apr 20 '22 11:04 xperiandri

Is my answer a working approach?

xperiandri avatar Apr 23 '22 11:04 xperiandri

I am confused. I thought you already had a working approach and were just looking for a working approach that is also good looking.

TysonMN avatar Apr 23 '22 11:04 TysonMN

I have a working approach and it looks ugly. If you create a project from template you can see it

xperiandri avatar Apr 23 '22 13:04 xperiandri

Can you share a link to a GitHub repo containing your working but ugly approach?

TysonMN avatar Apr 23 '22 13:04 TysonMN

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

xperiandri avatar Apr 23 '22 14:04 xperiandri

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?

TysonMN avatar May 15 '22 02:05 TysonMN

This is the smallest possible reproduction ElmishCrazyInterop.zip

xperiandri avatar May 15 '22 10:05 xperiandri

Great. Can you put it in a GitHub repo and share a link to it?

TysonMN avatar May 15 '22 11:05 TysonMN

https://github.com/xperiandri/Elmish.CrazyInterop

xperiandri avatar May 15 '22 11:05 xperiandri

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.

2022-05-15_07-30-05_706_devenv

Second, the code doesn't compile for me.

2022-05-15_07-35-20_707_devenv


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?

TysonMN avatar May 15 '22 12:05 TysonMN

Just switch target version to whatever SDK version you have installed

xperiandri avatar May 15 '22 12:05 xperiandri

See the last one SDK you have image

xperiandri avatar May 15 '22 12:05 xperiandri

Choose the maximum one

xperiandri avatar May 15 '22 12:05 xperiandri

I removed unnecessary dependencies. Now it must work for you

xperiandri avatar May 15 '22 12:05 xperiandri

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?

TysonMN avatar May 21 '22 12:05 TysonMN

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.

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?

TysonMN avatar May 21 '22 12:05 TysonMN

Yes, I tried. No it does not look less ugly

xperiandri avatar May 21 '22 12:05 xperiandri

Can you replace those links with links that exist in the underlying repo?

Do you mean the links in the first message?

xperiandri avatar May 21 '22 12:05 xperiandri

Do you mean the links in the first message?

Yes

TysonMN avatar May 21 '22 12:05 TysonMN

If you want a branch use NET_6

xperiandri avatar May 21 '22 12:05 xperiandri

I rebased it. You strictly need that link to direct you to the branch, I can do that.

xperiandri avatar May 21 '22 12:05 xperiandri

But it is not the last rebase. So they will change again

xperiandri avatar May 21 '22 12:05 xperiandri

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 ]

kspeakman avatar Sep 20 '22 22:09 kspeakman