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.