Using ReactiveUI's BindTo() to update a XAML property generates a warning
Asked Answered
S

2

7

I'm trying to update a property of an element in the XAML of a view:

this.WhenAnyValue(x => x.ViewModel.IsEnabled).BindTo(this, x => x.MyButton.IsEnabled);

This works as expected, however, it generates a warning at runtime:

POCOObservableForProperty: rx_bindto_test.MainWindow is a POCO type and won't send change notifications, WhenAny will only return a single value!

I can get rid of the warning by changing the expression to:

this.WhenAnyValue(x => x.ViewModel.IsEnabled).Subscribe(b => MyButton.IsEnabled = b);

but I'm still wondering why it doesn't work properly with BindTo().

Edit: it appears even the regular Bind and OneWayBind generate this warning.

  1. What am I doing wrong here?
  2. And is it really necessary to define ViewModel as a dependency property on the View to be able to observe it? (when I declare it as a regular property on the View, ReactiveUI generates the same POCO warning) I can't simply make it inherit from ReactiveObject because C# doesn't support multiple inheritance.

MainWindow.xaml.cs

public partial class MainWindow : Window, IViewFor<MyViewModel>, IEnableLogger {
    public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register("ViewModel",
        typeof(MyViewModel), typeof(MainWindow));

    public MyViewModel ViewModel {
        get { return (MyViewModel)GetValue(ViewModelProperty); }
        set { SetValue(ViewModelProperty, value); }
    }

    object IViewFor.ViewModel {
        get { return ViewModel; }
        set { ViewModel = (MyViewModel)value; }
    }

    public MainWindow() {
        InitializeComponent();

        this.WhenAnyValue(x => x.ViewModel).BindTo(this, x => x.DataContext);

        this.WhenAnyValue(x => x.ViewModel.IsEnabled).BindTo(this, x => x.MyButton.IsEnabled);

        ViewModel = new MyViewModel();
        ViewModel.IsEnabled = true;
    }
}

MainWindow.xaml

<Window x:Class="rx_bindto_test.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">
    <Grid>
        <Button x:Name="MyButton">My Button</Button>
    </Grid>
</Window>

MyViewModel.cs

public class MyViewModel : ReactiveObject, IEnableLogger {
    private bool isEnabled;

    public bool IsEnabled {
        get { return isEnabled; }
        set { this.RaiseAndSetIfChanged(ref isEnabled, value); }
    }
}
Sealey answered 20/5, 2015 at 14:21 Comment(0)
M
7

I think the confusion comes from the fact that you get a warning on "MyButton" resolving, and not on the ViewModel.

MyButton is a "constant" object, w/o any lifecycle (neither INPC nor DependencyObject), hence you can safely ignore this warning.

Alternatively you can register the below extra property resolver, which will behaves like the POCO one (minus the warning) for every internal field of a FrameworkElement, which is pretty close to every control from the XAML (I believe):

Locator.CurrentMutable.Register(() => new CustomPropertyResolver(), typeof(ICreatesObservableForProperty));

public class CustomPropertyResolver : ICreatesObservableForProperty
{
    public int GetAffinityForObject(Type type, string propertyName, bool beforeChanged = false)
    {
        if (!typeof(FrameworkElement).IsAssignableFrom(type))
            return 0;
        var fi = type.GetTypeInfo().GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)
          .FirstOrDefault(x => x.Name == propertyName);

        return fi != null ? 2 /* POCO affinity+1 */ : 0;
    }

    public IObservable<IObservedChange<object, object>> GetNotificationForProperty(object sender, System.Linq.Expressions.Expression expression, bool beforeChanged = false)
    {
        var foo = (FrameworkElement)sender;
        return Observable.Return(new ObservedChange<object, object>(sender, expression), new DispatcherScheduler(foo.Dispatcher))
            .Concat(Observable.Never<IObservedChange<object, object>>());
    }
}
Montero answered 16/7, 2015 at 20:49 Comment(5)
This is the correct answer. The warning is correct, just very misleading because it doesn't include which property that was used in a WhenAny (used by BindTo or OneWayBind) that was not observable. Any named UI elements in your XAML will always be un-observable, so if you bind to any named elements you can expect to see this warning once per view that contains named UI elements.Kandi
Why does MyButton need to be observable though? I'm not trying to observe it, just trying to update its value whenever some other observable changes. Is it saying I will run into trouble whenever the ViewModel property changes (because it will then still update the MyButton on the old ViewModel)?Sealey
RxUI doesn't know (w/o your help) that MyButton is constant and doesn't need to be observed. By default it tries to observe any part of the expressions you give, that's why it supports a change in the ViewModel when you write x.ViewModel.IsEnabled (it observes the ViewModel property for changes), and it tries to do the same for x.MyButton.IsEnabledMontero
while the warning itself is valid, the severity is a bit over played (only because RxUI binding doesn't check to see if the path element is private). Using the custom property resolver that Gluck posted in the answer will eliminate the warnings. However, these warnings can be ignored in general. They exist simply to warn the developer that they are attempting to observe an un-observable element.Kandi
Okay thanks guys. It's just confusing for an end-user like me because conceptually I'm not really observing the destination variable, to me it's just a value sink. I overlooked the fact that it probably wants to monitor the target variable in case the object it targets is ever replaced at runtime.Sealey
E
0

I think the main problem here is that you only want this going one direction - so you want oneway bind. What if you tried this:

this.OneWayBind(ViewModel x => x.IsEnabled, x => y.MyButton.IsEnabled);

I think the problem here is that IsEnabled doesn't have a property change notification associated with it (but I am still learning RxUI, and it is a complex codebase!).

To (try to) answer your questions:

  1. See above.
  2. Yeah. I wish too. But VM isn't defined by the window class because not everyone uses MVVM. And for the plumbing to work properly, you need it as a dependency property (otherwise no notifications are sent when it is changed). And I just discovered (painfully) that it is possible for your view to have more than one VM during its lifetime - if you have a large ListView and the View caching stuff turns on.
Ethben answered 24/5, 2015 at 0:5 Comment(1)
For this simplified example it would indeed be better to use a simple binding, but in reality I'm trying to combine multiple properties using a where and select to transform the values into a single output. I also don't understand why the target property needs to have change notification in this case, I'm not trying to observe its value afaik.Sealey

© 2022 - 2024 — McMap. All rights reserved.