F# Fabulous Xamarin:外部事件订阅

F# Fabulous Xamarin: external event subscription

我是 Fabulous 和 MUV 模型的新手,我正在尝试实现适用于 BLE 的应用程序。我对 F# 也有点陌生,过去主要使用 erlang 和 C#,所以对外部事件处理有点迷茫。 CrossBluetoothLE.Current.Adapter 具有 DeviceDiscovered 事件处理程序 (IEvent)。将此事件处理程序链接到 Fabulous 更新函数的最正确方法是什么?

例如在调用 CrossBluetoothLE.Current.Adapter.StartScanningForDevicesAsync() 之后,我希望此事件处理程序将新发现的设备提供给更新功能。

如果我做这样的事情(这行不通):

type MyApp () as app = 
    inherit Application ()

    let deviceDiscovered dispatch = 
        CrossBluetoothLE.Current.Adapter.DeviceDiscovered.Subscribe (fun x -> dispatch (App.Msg.Discovered x.Device) )

    let runner =
        App.program
        |> Program.withConsoleTrace
        |> Program.withSubscription (fun _ -> Cmd.ofSub deviceDiscovered)
        |> XamarinFormsProgram.run app

如果有效,则可以发现设备,因为 CrossBluetoothLE.Current.Adapter 是静态的。然而,在发现设备后,我将需要处理(例如接收通知或回复),并且不可能将动态设备处理程序包含到 Program.withSubscription.

不确定Fabulous在这里是否适用。

好的,我找到了一些解决方案并且现在可以使用了,但是整体架构看起来有点奇怪。所以一般的方法是创建一个外部邮箱,它将消息发送到 MUV 循环。

  1. 描述外部模块中MUV的所有消息,例如:
    type Msg = 
        | Scan
        | Discovered of IDevice
        | Connect of IDevice
        | ClockMsg of System.DateTime
        | TextMsg of string
  1. 创建封装邮箱的类型:
    type DispatchFunc = Msgs.Msg -> unit

    type State = 
        | Initialized of DispatchFunc
        | NotInitialized

    type Mail = 
        | Dispatch of DispatchFunc
        | Msg of Msgs.Msg
        | None

    let rand = System.Random()
    let id = rand.NextDouble()

    let postbox = MailboxProcessor.Start(fun inbox -> 
        let rec messageLoop (state:State) = async{
            let! mail = inbox.Receive()

            let new_state = 
                match mail with 
                | None ->
                    state
                | Msg msg -> 
                    match state with 
                    | NotInitialized -> NotInitialized
                    | Initialized df ->
                        df msg
                        state
                | Dispatch df -> 
                    Initialized df

            return! messageLoop (new_state)
            }

        messageLoop (NotInitialized))

    let post(o) =
        postbox.Post o

此处,邮箱以 NotInitialized 状态启动,等待应用程序启动。当一切都完成后,邮箱收到派发功能,将用于进一步派发外部消息到MUV主循环。

  1. 将调度处理程序传递到邮箱:
type MyApp () as app = 
    inherit Application ()

    // generate initial events + start threads + pass dispatch reference to the mailbox
    let initThreads dispatch =
        // init & start external (e.g. bluetooth receiver) threads here
        // or start them asynchronously from MUV loop
        Postbox.post (Postbox.Dispatch dispatch)
        ()

    let runner = 
        App.program
        |> Program.withConsoleTrace
        |> Program.withSubscription (fun _ -> Cmd.ofSub initThreads)  
        |> XamarinFormsProgram.run app

所以现在,如果你想从外部线程向 MUV 发送事件,只需在 initThreads 内启动它(或者,例如,从 MUV 循环内)并使用类似的东西:Postbox.post (Postbox.Msg (Msgs.TextMsg "It works!"))

例如出于我的目的(BLE 发现),它将如下所示:

    let update msg model =
        match msg with
        | Msgs.Scan -> 
            CrossBluetoothLE.Current.Adapter.StopScanningForDevicesAsync() |> Async.AwaitTask |> ignore
            CrossBluetoothLE.Current.Adapter.DeviceDiscovered.Subscribe (
                    fun (a) ->
                        Postbox.post (Postbox.Msg (Msgs.Discovered a.Device))
                        ()
                ) |> ignore
            CrossBluetoothLE.Current.Adapter.StartScanningForDevicesAsync() |> Async.AwaitTask |> ignore
            model, Cmd.none
        | Msgs.ClockMsg msg ->
            { model with label = msg.ToString() }, Cmd.none
        | Msgs.TextMsg msg ->
            { model with label = msg }, Cmd.none
        | Msgs.Discovered d ->
            { model with gattDevices = d::model.gattDevices; label = "Discovered " + d.ToString() }, Cmd.none
        | Msgs.Connect d -> { model with connectedDevice = d }, Cmd.none

这肯定是一个非常丑陋的解决方案,但我无法想象更漂亮的东西:(。