Repository: AvaloniaUI/Avalonia

Issue:
Using ReactiveUserControl in XAML crashes the application
### Describe the bug

Using any control that inherits from `ReactiveUsercontrol<T>` in XAML or code behind immediately crashes the application if `DataContext` is set on a parent control and the data object cannot be cast to `T`.

<details>
<summary>Stack trace</summary>
<pre>
System.ArgumentException: Invalid value for Property 'ViewModel': 'AvaloniaRxTest.ViewModels.MainWindowViewModel' (AvaloniaRxTest.ViewModels.MainWindowViewModel)
   at Avalonia.StyledProperty`1.ShouldSetValue(AvaloniaObject target, Object value, TValue& converted)
   at Avalonia.StyledProperty`1.RouteSetCurrentValue(AvaloniaObject target, Object value)
   at Avalonia.AvaloniaObject.SetCurrentValue(AvaloniaProperty property, Object value)
   at Avalonia.ReactiveUI.ReactiveUserControl`1.OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
   at Avalonia.AvaloniaObject.OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs change)
   at Avalonia.Animation.Animatable.OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs change)
   at Avalonia.AvaloniaObject.RaisePropertyChanged[T](AvaloniaProperty`1 property, Optional`1 oldValue, BindingValue`1 newValue, BindingPriority priority, Boolean isEffectiveValue)
   at Avalonia.PropertyStore.ValueStore.OnAncestorInheritedValueChanged[T](StyledProperty`1 property, T oldValue, T newValue)
   at Avalonia.PropertyStore.ValueStore.OnInheritedEffectiveValueChanged[T](StyledProperty`1 property, T oldValue, EffectiveValue`1 value)
   at Avalonia.PropertyStore.EffectiveValue`1.SetAndRaiseCore(ValueStore owner, StyledProperty`1 property, T value, BindingPriority priority, Boolean isOverriddenCurrentValue, Boolean isCoercedDefaultValue)
   at Avalonia.PropertyStore.EffectiveValue`1.SetLocalValueAndRaise(ValueStore owner, StyledProperty`1 property, T value)
   at Avalonia.PropertyStore.ValueStore.SetLocalValue[T](StyledProperty`1 property, T value)
   at Avalonia.PropertyStore.ValueStore.SetValue[T](StyledProperty`1 property, T value, BindingPriority priority)
   at Avalonia.AvaloniaObject.SetValue[T](StyledProperty`1 property, T value, BindingPriority priority)
   at Avalonia.StyledElement.set_DataContext(Object value)
   at AvaloniaRxTest.App.OnFrameworkInitializationCompleted() in C:\*\AvaloniaRxTest\App.axaml.cs:line 20
   at Avalonia.AppBuilder.SetupUnsafe()
   at Avalonia.AppBuilder.Setup()
   at Avalonia.AppBuilder.SetupWithLifetime(IApplicationLifetime lifetime)
   at Avalonia.ClassicDesktopStyleApplicationLifetimeExtensions.StartWithClassicDesktopLifetime(AppBuilder builder, String[] args, Action`1 lifetimeBuilder)
   at AvaloniaRxTest.Program.Main(String[] args) in C:\*\AvaloniaRxTest\Program.cs:line 13
</pre>
</details>

### To Reproduce

1. Start with Avalonia MVVM template: `dotnet new avalonia.mvvm`
2. Create a new view model and a `ReactiveUserControl` for it:
```cs
public class ChildViewModel
{
}

public class ChildView : ReactiveUserControl<ChildViewModel>
{
}
```
3. Replace default `TextBlock` in _MainWindow.xaml_ with the new control:
```xml
<!-- Other attributes are omitted for readability -->
<Window xmlns:local="using:AvaloniaRxTest.Views">
    <local:ChildView />
</Window>
```
4. Update Avalonia packages to 11.1.0-beta1.
5. Debug the application and observe the crash.

[AvaloniaRxTest.zip](https://github.com/AvaloniaUI/Avalonia/files/14672410/AvaloniaRxTest.zip)


### Expected behavior

Application doesn't crash.

### Avalonia version

11.1.0-beta1

### OS

Windows

### Additional context

Repro doesn't crash on Avalonia 11.0.10.

Workaround for 11.1.0-beta1 is to "wrap" views that cause the crash in `ViewModelViewHost` but that implies a performance hit because view location is not free.

#14477 seems to be where the bug was introduced but I can't say for sure because I can't get `git bisect` to work due to some funky issues with Git submodules.


Hints:
Adding `&& typeof(TViewModel).IsAssignableFrom(change.NewValue)` here should fix it:

https://github.com/AvaloniaUI/Avalonia/blob/bad2e8dbdcd3bd4ffd75a35fe94c20ff8ca2b880/src/Avalonia.ReactiveUI/ReactiveUserControl.cs#L51

The same fix should be applied to `ReactiveWindow`.

It won't pass through converters, but I don't think that's needed here.
why not just `change.NewValue is TViewModel` ?
Because it can be `null`, but you're right, I think it can be `change.NewValue is null or TViewModel`, as the `ReferenceEquals` check ensures it's not a value type.

Patch:
diff --git a/src/Avalonia.ReactiveUI/ReactiveUserControl.cs b/src/Avalonia.ReactiveUI/ReactiveUserControl.cs
index 94090f26141..76a4f29b161 100644
--- a/src/Avalonia.ReactiveUI/ReactiveUserControl.cs
+++ b/src/Avalonia.ReactiveUI/ReactiveUserControl.cs
@@ -48,14 +48,15 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
 
             if (change.Property == DataContextProperty)
             {
-                if (Object.ReferenceEquals(change.OldValue, ViewModel))
+                if (ReferenceEquals(change.OldValue, ViewModel)
+                    && change.NewValue is null or TViewModel)
                 {
                     SetCurrentValue(ViewModelProperty, change.NewValue);
                 }
             }
             else if (change.Property == ViewModelProperty)
             {
-                if (Object.ReferenceEquals(change.OldValue, DataContext))
+                if (ReferenceEquals(change.OldValue, DataContext))
                 {
                     SetCurrentValue(DataContextProperty, change.NewValue);
                 }
diff --git a/src/Avalonia.ReactiveUI/ReactiveWindow.cs b/src/Avalonia.ReactiveUI/ReactiveWindow.cs
index 727c22f0167..4048ef1e257 100644
--- a/src/Avalonia.ReactiveUI/ReactiveWindow.cs
+++ b/src/Avalonia.ReactiveUI/ReactiveWindow.cs
@@ -48,14 +48,15 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
 
             if (change.Property == DataContextProperty)
             {
-                if (Object.ReferenceEquals(change.OldValue, ViewModel))
+                if (ReferenceEquals(change.OldValue, ViewModel)
+                    && change.NewValue is null or TViewModel)
                 {
                     SetCurrentValue(ViewModelProperty, change.NewValue);
                 }
             }
             else if (change.Property == ViewModelProperty)
             {
-                if (Object.ReferenceEquals(change.OldValue, DataContext))
+                if (ReferenceEquals(change.OldValue, DataContext))
                 {
                     SetCurrentValue(DataContextProperty, change.NewValue);
                 }


Test patch:
diff --git a/tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs
index 4bf999bed01..67790789e31 100644
--- a/tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs
+++ b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs
@@ -134,6 +134,22 @@ public void Should_Inherit_DataContext()
             Assert.Same(vm2, view.ViewModel);
         }
 
+        // https://github.com/AvaloniaUI/Avalonia/issues/15060
+        [Fact]
+        public void Should_Not_Inherit_DataContext_Of_Wrong_Type()
+        {
+            var view = new ExampleView();
+            var root = new TestRoot(view);
+            
+            Assert.Null(view.DataContext);
+            Assert.Null(view.ViewModel);
+
+            root.DataContext = this;
+
+            Assert.Same(this, view.DataContext);
+            Assert.Null(view.ViewModel);
+        }
+
         [Fact]
         public void Should_Not_Overlap_Change_Notifications()
         {