1

I am encountering a strange issue and am not sure why it would be occuring. I've nailed it down to the WhenAny or WhenAnyValue methods.

I have two view models with a parent-child relationship. The parent has an ObservableCollection of children. The parent monitors each child's HasError property in order to reevaluate it's own HasErrors property.

Initially, the child is added to the ObservableCollection and its HasErrors property monitored with WhenAnyValue. After this, the HasErrors property is set to true as expected because its Validate method gets called as a result of the WhenAnyValue.

The problem is when the child becomes valid and its HasErrors property gets set to false, the parent does not get notified via WhenAnyValue and thus cannot reevaluate its own HasErrors.

The solution seems to be adding the child to the collection after using WhenAnyValue or using ObservableForProperty instead. It seems strange that this order would matter as the ReactiveObject and ObservableCollection seem independent.

Below is the code that does not work but swapping the last two lines or using ObservableForProperty makes it work:

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

I am using ReactiveUI version 6.2.1.1 but the same behavior is present in version 6.0.1.

Full code example:

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>

Edit 1: I think I've discovered something that might explain why the issue is occuring. Although I'm not sure why it's the case.

Take the following example:

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

When initially setting up the observable using WhenAnyValue the value of HasErrors is false. Then the subscribe gets invoked as part of the initial setup and sets HasErrors to true. However, the WhenAnyValue does not pick up on this change. Therefore, when setting HasErrors to false at a later time, it is not picked up as a change. It seems like there some state maintained that is out of sync when determining if a change actually occurred or not.

Here's another interesting thing:

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

For the example above, the stack overflow doesn't happen until line 3. This is another example of where some state seems to be out of sync.

Jasmin
  • 11
  • 3
  • I'm not exactly clear on what your problem is, but ReactiveUI has a ReactiveList class that would/can replace a lot of the logic wrapped up and around ObservaableCollection. Perhaps this will help you get started http://stackoverflow.com/questions/20225705/reactivelist-and-whenany – kenny Dec 16 '14 at 22:56
  • @kenny I've tried that but still get the same outcome. – Jasmin Dec 16 '14 at 23:20
  • I think you'll end up better, in that you can have any ReactiveList call you parent's validate on change and I don't think child.WheyAny* in your AddChild() is a good strategy. – kenny Dec 16 '14 at 23:23

1 Answers1

2

If you use ReactiveList and a bit of cleverness, you could just do this:

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

But an even better way would just be to do this:

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

var anyoneHasErrors = validators.Changed.StartWith(null)
    .Select(_ => allErrors.Any(x => x != false));
Ana Betts
  • 73,868
  • 16
  • 141
  • 209
  • This seems much more roundabout solution than just subscribing to the child when being added. Also, if the parent is told to validate we want the children to get validated instead of just relying on their current HasValues state. – Jasmin Dec 17 '14 at 15:07
  • Also, what I don't quite understand is why the issue is happening to begin with. – Jasmin Dec 17 '14 at 15:10
  • 1
    "This seems much more roundabout solution than just subscribing to the child when being added". It's not roundabout when you realize you have to handle Reset, Clear, and items being removed – Ana Betts Dec 17 '14 at 15:56
  • I see what you're saying and I agree. For this example though, let's assume that items can only be added with the `AddChild` and cannot be removed. I updated my question with some additional information that might identify the root issue. – Jasmin Dec 17 '14 at 16:32