【问题标题】:ReactiveUI WhenAny behavior dependent on when ReactiveObject is added to an ObservableCollectionReactiveUI WhenAny 行为取决于何时将 ReactiveObject 添加到 ObservableCollection
【发布时间】:2015-02-15 00:08:30
【问题描述】:

我遇到了一个奇怪的问题,不知道为什么会发生。我已将其确定为 WhenAnyWhenAnyValue 方法。

我有两个具有父子关系的视图模型。父母有一个ObservableCollection 的孩子。父母监控每个孩子的HasError 属性,以便重新评估它自己的HasErrors 属性。

最初,孩子被添加到ObservableCollection 中,其HasErrors 属性由WhenAnyValue 监控。此后,HasErrors 属性按预期设置为true,因为WhenAnyValue 调用了它的Validate 方法。

问题是当孩子变为有效并且其HasErrors 属性设置为false 时,父母不会通过WhenAnyValue 得到通知,因此无法重新评估自己的HasErrors

解决方案似乎是使用WhenAnyValue 或使用ObservableForProperty 将孩子添加到集合之后。这个顺序很重要,因为ReactiveObjectObservableCollection 似乎是独立的,这似乎很奇怪。

以下是不起作用的代码,但交换最后两行或使用ObservableForProperty 使其工作:

private void AddChild()
{
    var child = new ChildViewModel();
    Children.Add(child);
    child.WhenAnyValue(p => p.HasErrors).Subscribe(p => Validate());
}

我使用的是 ReactiveUI 版本 6.2.1.1,但版本 6.0.1 中存在相同的行为。

完整代码示例:

public class ParentViewModel : ReactiveObject
{
    private bool _hasErrors;

    public ObservableCollection<ChildViewModel> Children { get; private set; }

    public ReactiveCommand<object> AddChildCommand { get; private set; }

    public bool HasErrors
    {
        get { return _hasErrors; }
        set { this.RaiseAndSetIfChanged(ref _hasErrors, value); }
    }

    public ParentViewModel()
    {
        Children = new ObservableCollection<ChildViewModel>();
        AddChildCommand = ReactiveCommand.Create();
        AddChildCommand.Subscribe(p => AddChild());
    }

    public bool Validate()
    {
        HasErrors = !Children.All(p => p.Validate());
        return HasErrors;
    }

    private void AddChild()
    {
        var child = new ChildViewModel();
        Children.Add(child);
        child.WhenAnyValue(p => p.HasErrors).Subscribe(p => Validate());
    }
}

public class ChildViewModel : ReactiveObject, IDataErrorInfo
{
    private string _name;
    private bool _hasErrors;

    public string Name
    {
        get { return _name; }
        set { this.RaiseAndSetIfChanged(ref _name, value); }
    }

    public bool HasErrors
    {
        get { return _hasErrors; }
        set { this.RaiseAndSetIfChanged(ref _hasErrors, value); }
    }

    public string Error
    {
        get { return null; }
    }

    public string this[string columnName]
    {
        get { return Validate() ? null : "Required"; }
    }

    public bool Validate()
    {
        HasErrors = string.IsNullOrWhiteSpace(Name);
        return !HasErrors;
    }
}

<Window x:Class="MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"
        Height="350"
        Width="525">
    <StackPanel Margin="5">
        <StackPanel Orientation="Horizontal">
            <TextBlock Text="HasErrors: " />
            <TextBlock Text="{Binding HasErrors}"
                       Margin="3,0,0,0" />
        </StackPanel>
        <Button Content="Add"
                Command="{Binding AddChildCommand}" />
        <ItemsControl ItemsSource="{Binding Children}"
                      Height="500">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <TextBox Text="{Binding Name, ValidatesOnDataErrors=True}"
                             Width="200" />
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </StackPanel>
</Window>

编辑 1: 我想我发现了一些可以解释为什么会出现问题的东西。虽然我不确定为什么会这样。

举个例子:

var child = new ChildViewModel();
child.WhenAnyValue(p => p.HasErrors).Subscribe(p => child.HasErrors = true);
child.HasErrors = false;

最初使用 WhenAnyValue 设置 observable 时,HasErrors 的值是 false。然后,订阅作为初始设置的一部分被调用,并将HasErrors 设置为true。但是,WhenAnyValue 没有接受此更改。因此,稍后将HasErrors 设置为false 时,不会将其作为更改拾取。在确定更改是否实际发生时,似乎维护了一些不同步的状态。

还有一件有趣的事:

var child = new ChildViewModel();
child.WhenAnyValue(p => p.Name).Subscribe(p => child.Name = Guid.NewGuid().ToString());
child.Name = Guid.NewGuid().ToString();

对于上面的示例,堆栈溢出直到第 3 行才发生。这是另一个似乎某些状态不同步的示例。

【问题讨论】:

  • 我不太清楚你的问题是什么,但是 ReactiveUI 有一个 ReactiveList 类,它可以/可以替换很多围绕 ObservaableCollection 的逻辑。也许这会帮助您入门stackoverflow.com/questions/20225705/reactivelist-and-whenany
  • @kenny 我试过了,但结果还是一样。
  • 我认为你最终会更好,因为你可以让任何 ReactiveList 调用你父母对更改的验证,我不认为你的 AddChild() 中的 child.WheyAny* 是一个好策略。

标签: c# .net reactiveui


【解决方案1】:

如果你使用 ReactiveList 并有点聪明,你可以这样做:

var validators = Children.CreateDerivedCollection(
    selector: x => x.WhenAnyValue(p => p.HasErrors).Subscribe(p => Validate()),
    onRemoved: x => x.Dispose());

但更好的方法就是这样做:

var allErrors = Children.CreateDerivedCollection(x => x.HasErrors);

var anyoneHasErrors = validators.Changed.StartWith(null)
    .Select(_ => allErrors.Any(x => x != false));

【讨论】:

  • 这似乎比在添加时订阅孩子更迂回的解决方案。此外,如果父母被告知要验证,我们希望孩子得到验证,而不是仅仅依靠他们当前的 HasValues 状态。
  • 另外,我不太明白为什么会出现这个问题。
  • “这似乎比在添加时订阅孩子更迂回的解决方案”。当你意识到你必须处理重置、清除和被移除的项目时,这不是迂回
  • 我明白你在说什么,我同意。不过,对于这个示例,我们假设项目只能使用 AddChild 添加并且不能删除。我用一些可能确定根本问题的附加信息更新了我的问题。
猜你喜欢
  • 1970-01-01
  • 2013-08-11
  • 1970-01-01
  • 1970-01-01
  • 2013-10-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-10-21
相关资源
最近更新 更多