Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ReactiveCommand<TInput, TOutput> #249

Closed
erri120 opened this issue Aug 18, 2024 · 5 comments
Closed

Add ReactiveCommand<TInput, TOutput> #249

erri120 opened this issue Aug 18, 2024 · 5 comments

Comments

@erri120
Copy link
Contributor

erri120 commented Aug 18, 2024

R3 has ReactiveCommand<TInput> which inherits from Observable<TInput>. The observable contains the inputs, but there's no variant that also has outputs:

ReactiveCommand<TInput, TOutput> : Observable<TOutput>

Instead of passing Action<TInput>, this variant would use Func<TInput, TOutput>. My team has been using the ReactiveUI counterpart with outputs everywhere because it makes certain UI code much simpler. Since reactive commands are observables, we merge these observables to be able to react to a list of commands in situations where we work with lists or trees of items.

@neuecc
Copy link
Member

neuecc commented Aug 19, 2024

I checked the ReactiveUI code, but it looks needlessly complicated and the performance seems to be very poor...

By the way, is it not possible to use .Select(input => output) for the code that binds ReactiveCommand<TInput> and connects it?

@erri120
Copy link
Contributor Author

erri120 commented Aug 19, 2024

I checked the ReactiveUI code, but it looks needlessly complicated and the performance seems to be very poor...

That's why we're looking at R3 for an alternative.

By the way, is it not possible to use .Select(input => output) for the code that binds ReactiveCommand<TInput> and connects it?

It very much depends on the use-case:

file class Foo
{
    public Foo(Observable<Bar> observable)
    {
        // 1) R3 command
        observable
            .Select(static bar => bar.R3Command.AsObservable())
            .Merge()
            .Subscribe(static unit => { /* no name */ });

        // 2) R3 command with select
        observable
            .Select(static bar => bar.R3Command.Select(bar, static (_, bar) => bar.Name))
            .Merge()
            .Subscribe(static name => { /* */ });

        // 3) ReactiveUI
        observable
            .Select(static bar => bar.ReactiveUICommand.ToObservable())
            .Merge()
            .Subscribe(static name => { /* */ });
    }
}

file class Bar
{
    public string Name { get; }

    public R3.ReactiveCommand<R3.Unit> R3Command { get; }

    public ReactiveUI.ReactiveCommand<System.Reactive.Unit, string> ReactiveUICommand { get; }

    public Bar(string name)
    {
        Name = name;

        R3Command = new R3.ReactiveCommand<R3.Unit>();
        ReactiveUICommand = ReactiveUI.ReactiveCommand.Create<System.Reactive.Unit, string>(_ => Name);
    }
}

This is a common pattern in the application we're working on, where multiple instances of Bar exists in some list or tree, and the parent Foo observes the commands. The commands are usually bound to some button in the UI. While Command.Select can work if the command doesn't actually do anything, besides being a specialized Subject<Unit> that triggers OnNext when a button in the UI is clicked, if the command needs to do some amount of work, we need an output.

Take a file picker for example:

file class Foo
{
    public R3.ReactiveCommand<R3.Unit> PickFileCommand { get; }

    public Foo()
    {
        PickFileCommand = new R3.ReactiveCommand<R3.Unit>(async (_, cancellationToken) =>
        {
            var path = await PickFileAsync(cancellationToken);
        });
    }

    private async ValueTask<AbsolutePath> PickFileAsync(CancellationToken cancellationToken) => throw new NotImplementedException();
}

The command gets bound to a button on the UI, but here we want to do something with the output. Of course, we can just add a Subject<AbsolutePath>:

public R3.ReactiveCommand<R3.Unit> PickFileCommand { get; }
private R3.Subject<AbsolutePath> PickedFileSubject { get; } = new();

public Foo()
{
    PickedFileSubject
        .Where(path => path.FileExists)
        .Subscribe(path => { /* do stuff */ });

    PickFileCommand = new R3.ReactiveCommand<R3.Unit>(async (_, cancellationToken) =>
    {
        var path = await PickFileAsync(cancellationToken);
        PickedFileSubject.OnNext(path);
    });
}

This gets us where we want, but it's not nice to use. I hope this illustrates why you might want an output on the command. It's not the end of the world for us, but it would make our lives much easier.

@neuecc
Copy link
Member

neuecc commented Aug 20, 2024

Thanks for the detailed explanation, I understand now.
ReacitveCommand<TInput, TOutput> was added in v1.2.8, please try it.

@erri120
Copy link
Contributor Author

erri120 commented Aug 20, 2024

Thanks, will try it out.

@erri120
Copy link
Contributor Author

erri120 commented Aug 26, 2024

Works great!

@erri120 erri120 closed this as completed Aug 26, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants