2
module StackTenButtons.Try2

open System
open System.Windows
open System.Windows.Controls

open System.Reactive.Linq
open System.Reactive.Disposables
open FSharp.Control.Reactive

let control c l = 
    Observable.Create (fun (sub : IObserver<_>) ->
        let c = c()
        let d = new CompositeDisposable()
        List.iter (fun x -> d.Add(x c)) l
        sub.OnNext(c)
        d :> IDisposable
        )
let do' f c = f c; Disposable.Empty
let prop s v c = Observable.subscribe (s c) v

let w =
    control Window [
        prop (fun t v -> t.Content <- v) <| control StackPanel [
            do' (fun pan ->
                Observable.range 0 10
                |> Observable.subscribe (fun x -> pan.Children.Add(Button(Content=sprintf "Button %i" x)) |> ignore)
                |> ignore
                )
            ]
        ]

[<STAThread>]
[<EntryPoint>]
let main _ = w.Subscribe (Application().Run >> ignore); 0

I am trying to make a small proof of concept library for reactive UIs and I've encountered this problem when trying to write a function that adds more than a single control to the parent. Standard property setting works when they are singletons, but not when using functions like Observable.range which are iterators.

Is it possible to make this work?

As F# needs some stuff to be added manually to the project file so WPF can be used, here is the repo for this.

Marko Grdinić
  • 3,798
  • 3
  • 18
  • 21
  • It works when I change `Observable.range 0 10` to `Observable.rangeOn Scheduler.Immediate 0 10`. One thing I am having trouble is figuring out where the `DispatcherScheduler` disappeared in .NET Core 3.1. I am not sure what I should substitute it with. I do not necessarily want to `Scheduler.Immediate` for everything so it would be good to have it. – Marko Grdinić Apr 08 '20 at 13:31
  • Rather than creating a control, suppose I was getting a resource from somewhere asynchronously. How would I observe it on the UI thread? – Marko Grdinić Apr 08 '20 at 13:41
  • If I do `Observable.rangeOn ThreadPoolScheduler.Instance 0 10` that gives me the `System.InvalidOperationException` because only the UI thread can mutate the UI control. `Observable.rangeOn ThreadPoolScheduler.Instance 0 10 |> Observable.observeOn Scheduler.Immediate` also gives me the same exception. – Marko Grdinić Apr 08 '20 at 13:46

1 Answers1

3

The default scheduler for Range is Scheduler.CurrentThread.

CurrentThread and Immediate have behavior that sometimes results in a perpetual trampoline or a deadlock particularly when attempting to be used synchronously with an Observable.Create or similar un-scheduled cold observables.

The exact reasons why they lock up are difficult to describe, but are similar to the behavior found here and here.

 Observable.Create (fun (sub : IObserver<_>) -> 
        sub.OnNext(1)
        sub.OnNext(2)
        sub.OnNext(3)
        d :> IDisposable //<-- this dispose should cancel all `OnNext`
 )

The above dispose can never be called, to prevent items from being emitted - until after the items have been emitted. If you manually construct an observable, try to make it take in a scheduler argument.

DispatcherScheduler being back in Rx.NET core might still be some ways off. Here's an implementation of a minimal DispatcherScheduler:

type DispatcherScheduler =
    static member Instance = {
     new IScheduler with
         member _.Now = DateTimeOffset.Now
         member _.Schedule<'S>(state: 'S, action: Func<IScheduler, 'S, IDisposable>) : IDisposable = 
            let op = Application.Current.Dispatcher.InvokeAsync(fun () -> action.Invoke(DispatcherScheduler.Instance, state))
            Disposable.Create(fun () -> op.Abort() |> ignore)
         member _.Schedule<'S>(state: 'S, dueTime: TimeSpan, action: Func<IScheduler, 'S, IDisposable>) : IDisposable = failwith "Not Impl"
         member _.Schedule<'S>(state: 'S, dueTime: DateTimeOffset, action: Func<IScheduler, 'S, IDisposable>) : IDisposable = failwith "Not Impl"
    }
Asti
  • 12,447
  • 29
  • 38
  • ``` let a = Application(); use __ = w.Subscribe (fun w -> a.MainWindow <- w; w.Show()); a.Run() ``` One way of getting out the deadlock is to start the program like this. Then passing `Scheduler.Immediate` to `range` becomes unnecessary. But now I am stuck on how to observe on the UI thread. Any idea how to get the dispatcher scheduler on .NET Core 3.1? – Marko Grdinić Apr 08 '20 at 16:17
  • AFAIK, there isn't a dispatcher wasn't implemented for .Net Core 3. – Asti Apr 08 '20 at 16:29
  • But https://github.com/microsoft/WPF-Samples/blob/cd9c2db92aea857a886a71c937af0d172f8826a0/Accessibility/FetchTimer/FetchTimer.csproj#L4 has them using a ` true` in the project file. Can you try if you can add `System.Windows.Threading` with this flag set? – Asti Apr 08 '20 at 16:31
  • I already had to use `` and `true`. The `DispatcherScheduler` even though it is in that namespace seems to have been a part of the Rx package, but it is not any longer. Still, even though that thing is not there just how is observing on the UI thread done in UWP and Xamarin. There has to be some way of doing it. One way that I've found is to use the `Application` instance's `Dispatcher`. `Observable.subscribe (fun x -> dis.Invoke (fun () -> pan.Children.Add(Button(Content=sprintf "Button %i" x)) |> ignore))` for example. – Marko Grdinić Apr 08 '20 at 16:38
  • I can pass it as an argument like that and invoke it. I can't `ObserveOn` it though. – Marko Grdinić Apr 08 '20 at 16:38
  • I've implemented a minimal `DispatcherScheduler` - it's not tested, but the idea is similar. A dispatcher, much like a scheduler is way of serializing notifications. – Asti Apr 08 '20 at 17:15
  • 'A' for effort. I've figured out how to use the `SynchronizationContextScheduler` though. [See here.](https://github.com/dotnet/wpf/issues/2856) – Marko Grdinić Apr 09 '20 at 07:50
  • Good find. WPF on the .Net Core is only supported on Windows, so I've still stuck to using 4.8 so that I don't have to deal with all the quirks. Any particular reason why you're targeting Core? – Asti Apr 09 '20 at 07:54
  • No, either for WPF or .NET Core. I could be using Xamarin or something else, but I used WPF 4 years ago and am specifically looking into whether I can introduce some new techniques now that I've studied reactive extensions. If you look at the Lithe repo you'll see that I've succeeded in implementing the MVU (Elm) pattern using Rx. This is a significant improvement on 4 years ago. Sure, today you can use a library like Fabulous to take care of this, but that was not around back then. And this is more versatile than just UIs. Later I'll be using this to do editor support for the Spiral language. – Marko Grdinić Apr 09 '20 at 13:30
  • I just checked out Spiral lang on your Github - that's a fascinating project! Most people who use WPF with Rx end up using ReactiveUI - that's MVVM though, which WPF is still very partial to. – Asti Apr 09 '20 at 14:31