【问题标题】:Combining and filtering multiple events with WhenAnyObservable使用 WhenAnyObservable 组合和过滤多个事件
【发布时间】:2018-08-01 23:54:40
【问题描述】:

我有一个 ViewModel:

  • 一个列表 ReactiveList
  • 单个 MyObject
  • IsBusy 布尔值

当 MyObject 列表或单个 MyObject 中的“Active”属性都不为真时,或者在任何情况下 IsBusy 为真时,我想禁用该命令。

在将 IsBusy 添加到图片之前,我想出的最佳解决方案是:

var canSave = this.WhenAnyObservable(
    x => x.MyObjectsList.ItemChanged, x => x.MyObject.Changed)
    .Where(x => x.PropertyName == "Active")
    .Select(_ => MyObjectsList.Any(x => x.Active) || MyObject.Active);

SaveCommand = ReactiveCommand.Create(Save, canSave);

这个想法是每次 Active 属性更改时都会重新评估。 不确定这是否是最好的解决方案(因此欢迎任何改进它的建议),但我绝对无法将 IsBusy 添加到图片中,以便在 IsBusy 时重新评估 Select 子句(包括 IsBusy 状态)变化。

【问题讨论】:

  • 看来我找到了办法。 var canSave = this.WhenAnyObservable(x => x.MyObjectsList.ItemChanged, x => x.MyObject.Changed) .Where(x => x.PropertyName == "Active").Select(_ => Unit.Default) .Merge(this.WhenAny(x=>x.IsBusy, _ => Unit.Default)) .Select(_ => (MyObjectsList.Any(x => x.Active) || MyObject.Active) && IsBusy ==假);
  • 使用.Switch() 有更好的解决方案。稍后我会试着把它弄起来。
  • WhenAnyObservable() 在这种情况下充当开关。 Merge() 之后的 Select() 可以通过使用 CombineLatest() 而不是这两个语句来消除。

标签: c# .net system.reactive reactiveui


【解决方案1】:

我认为你的答案可以简化。

var itemChangedObs = this.WhenAnyValue(x => x.MyObject.Active);
var isBusyObs = this.WhenAnyValue(x => x.IsBusy);
var listItemChangedObs = this.WhenAnyObservable(x => x.MyObectsList.ItemChanged).Where(x => x.PropertyName == "Active").Select(_ => MyObjectsList.Any(x => x.Active)).StartsWith(false)
var canRunCommand = itemChangedObs.CombineLatest(listItemChangedObs, isBusyObs, (itemActive, listItemActive, isBusy) => (itemActive || listItemActive) && !isBusy);

这个版本本质上使用了一个 CombineLatest,它在将两个 observable 组合在一起后接受一个你想要的值的 Lambda。

CombineLatest() 在两个 Observable 都发出值之前不会产生值,因此 listItemChanged 在前面有一个 StartsWith(false)。

WhenAnyValue() 将始终默认发出默认值 (T) 作为初始值,因此您不需要 StartsWith 与这些语句。

【讨论】:

  • 喜欢这种方法和可读性。但是 listItemChangedObs 仅在“更改的对象”处于活动状态时才进行标记,而不是在“任何项目”处于活动状态时进行标记。所以我会将第 3 行的末尾改为 .Select(_ => MyObjectsList.Any(x => x.Active).StartsWith(false);
  • 并且 CombineLatest 逻辑应该是 (itemActive || listItemActive) && !isBusy
【解决方案2】:

这是另一个替代方案,它避免了对具有 O(n) 复杂性的 MyObjectList.Any() 的需要。此解决方案涉及更多,但有可能提高效率。除了计算他的 listItemChangedObs observable 的方式之外,它与 Glenn 的 CombineLatest 方法相同。此版本保留列表中活动对象的总数。因此,每次触发 ItemChanged 时,它只需执行 +1 或 -1。然后它只是检查它是否大于 0。

public MyViewModel()
{
    var itemChangedObs = this.WhenAnyValue(x => x.MyObject.Active);
    var isBusyObs = this.WhenAnyValue(x => x.IsBusy);

    // Recalculate the # of active objects each time ObjectList is reassigned.
    var activeListItemCountInitializedObs = this
        .WhenAnyValue(x => x.ObjectList)
        .Select(
            list =>
            {
                // Return 0 if ObjectList is null.
                return list == null ? Observable.Return(0) : list
                    .ToObservable()
                    // Otherwise, increment by 1 for each active object.
                    .Select(x => x.Active ? 1 : 0)
                    // We use Aggregate, which is a single value sequence, because
                    // we're only interested in the final result.
                    .Aggregate((acc, current) => acc + current);
            })
        // We no longer need the inner observable from the last time active item count
        // was initialized. So unsubscribe from that one and subscribe to this most recent one.
        .Switch();

    var activeListItemCountChangedObs = this
        .WhenAnyObservable(x => x.ObjectList.ItemChanged)
        .Where(x => x.PropertyName == "Active")
        // Increment or decrement the number of active objects in the list.
        .Select(x => x.Sender.Active ? 1 : -1);

    // An IObservable<bool> that signals if *any* of objects in the list are active.
    var anyListItemsActiveObs = activeListItemCountInitializedObs
        .Select(
            // Use the initialized count as the starting value for the Scan accumulator.
            initialActiveCount =>
            {
                return activeListItemCountChangedObs
                    .Scan(initialActiveCount, (acc, current) => acc + current)
                    // Return true if one or more items are active.
                    .Select(x => x > 0)
                    .StartWith(initialActiveCount > 0);
            })
        // ObjectList was completely reassigned, so the previous Scan accumulator is
        // no longer valid. So we "reset" it by "switching" to the new one.
        .Switch();

    var canRunCommand = itemChangedObs
        .CombineLatest(
            anyListItemsActiveObs,
            isBusyObs,
            (itemActive, listItemActive, isBusy) => (itemActive || listItemActive) && !isBusy);

    Save = ReactiveCommand.CreateFromObservable(() => Observable.Return(Unit.Default), canRunCommand);
}

这是我运行代码时通过的单元测试。它基本上检查 ReactiveCommand 的 CanExecute 改变状态的次数,以及它是真还是假,每次其中一个变量改变时。

[Fact]
public void TestMethod1()
{
    var objectList = new ReactiveList<IMyObject>(
        initialContents: new[] { new MyObject(), new MyObject() },
        resetChangeThreshold: 0.3,
        scheduler: ImmediateScheduler.Instance);

    objectList.ChangeTrackingEnabled = true;

    IMyViewModel myViewModel = new MyViewModel
    {
        ObjectList = objectList,
        MyObject = new MyObject()
    };

    var canExecute = myViewModel.Save
        .CanExecute
        .CreateCollection(scheduler: ImmediateScheduler.Instance);

    Assert.Equal(1, canExecute.Count);
    Assert.False(canExecute[0]);

    myViewModel.ObjectList[0].Active = true;
    Assert.Equal(2, canExecute.Count);
    Assert.True(canExecute[1]);

    myViewModel.MyObject.Active = true;
    Assert.Equal(2, canExecute.Count);

    myViewModel.IsBusy = true;
    Assert.Equal(3, canExecute.Count);
    Assert.False(canExecute[2]);

    myViewModel.IsBusy = false;
    Assert.Equal(4, canExecute.Count);
    Assert.True(canExecute[3]);

    myViewModel.MyObject.Active = false;
    Assert.Equal(4, canExecute.Count);

    var object1 = new MyObject { Active = true };
    var object2 = new MyObject { Active = true };
    myViewModel.ObjectList = new ReactiveList<IMyObject>(
        initialContents: new[] { object1, object2 },
        resetChangeThreshold: 0.3,
        scheduler: ImmediateScheduler.Instance);

    Assert.Equal(4, canExecute.Count);

    object1 = new MyObject { Active = false };
    object2 = new MyObject { Active = false };
    myViewModel.ObjectList = new ReactiveList<IMyObject>(
        initialContents: new[] { object1, object2 },
        resetChangeThreshold: 0.3,
        scheduler: ImmediateScheduler.Instance);

    Assert.Equal(5, canExecute.Count);
    Assert.False(canExecute[4]);
}

【讨论】:

    猜你喜欢
    • 2020-10-14
    • 2020-06-06
    • 1970-01-01
    • 2017-01-16
    • 1970-01-01
    • 2020-01-12
    • 2021-10-17
    • 1970-01-01
    • 2017-05-04
    相关资源
    最近更新 更多