Повторно вызывать реактивную команду, пока условие не будет выполнено

Я использую ReactiveUI для приложения UWP и имею две команды CommandA и CommandB. CommandA при вызове пытается внести изменения в оборудование. CommandB при вызове считывает аппаратное обеспечение и предоставляет последнее значение.

  1. Я хотел бы вызвать CommandA (с параметром в качестве значения ComboBox) при изменении значения ComboBox.
  2. После выполнения CommandA я хотел бы повторно вызывать CommandB, пока не получу значение, совпадающее со значением, выбранным в ComboBox, или если произойдет тайм-аут. По тайм-ауту должна отображаться ошибка.

Чтобы убедиться, что выполнение CommandA завершено, я написал следующий код для [1]

this.WhenAnyValue(x => x.ComboBoxAValue)
    .InvokeCommand(CommandA);

CommandA.IsExecuting
    .Buffer(2,1)
    .Where(t => t[0] == true && t[1] == false)
    .Select( x=> Unit.Default)
    .InvokeCommand(CommandB) // This statement would attempt to invoke CommandB only once

Я не знаю, как это сделать [2].


person resp78    schedule 20.06.2018    source источник
comment
Вам нужно будет смешать его с Observable.Timer. Вы можете использовать Observable.Concat(CommandA, Observable.Timer(TimeSpan.Zero, TimeSpan.FromSeconds(1)) Где вы получите значение для условия отмены, которое вы можете смешать с CombineLatest.   -  person Glenn Watson    schedule 20.06.2018


Ответы (2)


Я убежден, что существует лучшее решение, но здесь у вас есть подход:

public class MyCoolViewModel : ReactiveObject
{
    private readonly Subject<Unit> _ticksToInvokeB;

    private readonly ObservableAsPropertyHelper<bool> _commandAExecutedCorrectly;
    public bool CommandAExecutedCorrectly => _commandAExecutedCorrectly.Value;

    public ReactiveCommand<Unit, bool> CommandA { get; set; }
    public ReactiveCommand<Unit, bool> CommandB { get; set; }

    private string _comboBoxValue;
    public string ComboBoxValue
    {
        get => _comboBoxValue;
        set => this.RaiseAndSetIfChanged(ref _comboBoxValue, value);
    }

    public MyCoolViewModel()
    {
        //Subject implements IObservable<T> and IObserver<T>, also alow us to tick values to its observable
        _ticksToInvokeB = new Subject<Unit>(); 

        CommandA = ReactiveCommand.Create<Unit,bool>( _ =>
        {
            Console.WriteLine("Command A");
            return true;
        });

        CommandB = ReactiveCommand.CreateFromTask<Unit,bool>( async _ =>
        {
            await Task.Delay(3000);
            var isTheSame = DateTime.Now.Second % 2 == 0;
            Console.WriteLine($"CommandB: is the same: {isTheSame}");
            if(!isTheSame)//if the value is not the same, tick a new unit, since we ticked a new value, CommandA will be executed
                _ticksToInvokeB.OnNext(Unit.Default);


            return isTheSame;
        });

        CommandA//We just send commandA execution to an OAPH
            .ToProperty(this, x => x.CommandAExecutedCorrectly, out _commandAExecutedCorrectly);

        this.WhenAnyValue(x => x.ComboBoxValue)
        .Skip(1) //this is because ComboBoxValue has a initial value (null) so we ignore it 
        .Select(_ => Unit.Default) //When it changes simply project an Unit
        .InvokeCommand(CommandA);//Inke CommandA

        this.WhenAnyValue(x => x.CommandAExecutedCorrectly)//When changes maded CommandA will set ChangesMaded to true
        .Where(b => b) // only when Command A gets executed correctly
        .Do(b => TickUnit()) // Tick a new unit
        .Subscribe();

        _ticksToInvokeB
            .Throttle(TimeSpan.FromMilliseconds(200))//delay a little bit the next value
            .InvokeCommand(CommandB);//invokes CommandB

    }

    private void TickUnit()
    {
        Console.WriteLine("Ticking new value");
        _ticksToInvokeB.OnNext(Unit.Default);
    }

}

Дайте мне знать, если это поможет вам.

С Уважением.

person Adrián Romero    schedule 20.06.2018
comment
Как вы упомянули, это может быть улучшено. Как только возникает проблема, проливает код TickUnit в модель представления. Извините, я отвлекся, скоро попробую ваше решение и сообщу вам, как все прошло. - person resp78; 29.06.2018

Во-первых, вместо метода буферизации, который вы использовали для обнаружения завершенной команды, я бы сделал что-то вроде этого. И вместо того, чтобы беспокоиться о создании команды для CommandB, я бы просто выполнил ее как метод. Вы по-прежнему можете использовать команду, если хотите, но в этом примере я буду использовать только асинхронный вызов. Я использую ExecuteUntilItYieldsTheSelectedComboBoxValue для непрерывного выполнения вашей логики CommandB в цикле, пока не будет найдено соответствующее значение. Он использует Observable.Create, поэтому вы можете контролировать, когда запускается OnNext. И вы можете пометить его тайм-аутом и обработать его в расширении подписки.

CommandA.IsExecuting
    // IsExecuting has an initial value of false.  We can skip that first value
    .Skip(1)
    // Filter until the executing state becomes false.
    .Where(isExecuting => !isExecuting)
    // Start an inner observable for your "CommandB" logic.
    .Select(_ => ExecuteUntilItYieldsTheSelectedComboBoxValue())
    // Whenever CommandA is invoked, dispose of the last inner observable subscription and resubscribe.
    .Switch()
    .Subscribe(
        _ => Console.WriteLine("OnNext"),
        ex => [display error message]);

...

private IObservable<Unit> ExecuteUntilItYieldsTheSelectedComboBoxValue()
{
    return Observable
        .Create<Unit>(
            async o =>
            {
                int randNum = -1;
                do
                {
                    randNum = await GetRandomNumberAsync();
                } while(randNum != ComboBoxValue);

                o.OnNext(Unit.Default);
                o.OnCompleted();

                return Disposable.Empty;
            })
        .Timeout(TimeSpan.FromSeconds(3));
}

Обновить

Основываясь на том, что указала Enigmativity, о необходимости вернуть что-то лучшее, чем Disposable.Empty (чтобы отменить любую выполняемую асинхронную задачу и выйти из цикла), я изменяю метод ExecuteUntilItYieldsTheSelectedComboBoxValue следующим образом:

private IObservable<Unit> ExecuteUntilItYieldsTheSelectedComboBoxValue()
{
    return Observable
        // Call our async method and pass in a cancellation token.
        .FromAsync(ct => GetRandomNumberAsync(ct))
        // Keep generating random numbers.
        .Repeat()
        // Until we find one that matches the ComboBoxValue.
        .Where(x => x == ComboBoxValue)
        // Just take the first one so this inner observable will complete.
        .Take(1)
        .Select(_ => Unit.Default)
        .Timeout(TimeSpan.FromSeconds(3));
}

Обратите внимание, что по-прежнему можно заставить метод Observable.Create работать правильно, но это отредактированное решение чище и менее подвержено ошибкам. Дайте знать, если у вас появятся вопросы.

person Colt Bauman    schedule 20.06.2018
comment
Когда вы вызываете return Disposable.Empty; в качестве возврата для метода Observable.Create, вы делаете что-то не так. Он создает наблюдаемое, от которого невозможно отказаться до тех пор, пока оно не завершится естественным образом, а если оно не завершится естественным образом, вы не сможете безопасно остановить свой код. - person Enigmativity; 20.06.2018
comment
@Enigmativity Спасибо, что указали на мою оплошность. Я отредактировал свой ответ другим решением. Возвращение Disposable.Empty в качестве возврата для метода Observable.Create обычно означает, что вы делаете что-то неправильно (как в моем случае), но не всегда. Не всегда есть что убрать или отменить. - person Colt Bauman; 21.06.2018
comment
Когда нечего очищать или отменять, вы, вероятно, можете использовать другой оператор для создания наблюдаемого. Прежде чем сдаться, всегда следует предпринять бесчеловечные усилия по удалению return Disposable.Empty;. - person Enigmativity; 21.06.2018
comment
Извините, я отвлекся, скоро попробую ваше решение и сообщу вам, как все прошло. - person resp78; 29.06.2018