Skip to content

Commit 31bc7cf

Browse files
Merge pull request #37 from TheEightBot/feature/reactiveui-24-and-windows-platforms
Fix Uno/Avalonia WhenAnyValue going silent after first value; bump Uno.WinUI to 6.6.184
2 parents d4f3d97 + 6b75ff3 commit 31bc7cf

5 files changed

Lines changed: 186 additions & 4 deletions

File tree

Directory.Packages.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@
7474
Stellar.Avalonia does. -->
7575
<ItemGroup>
7676
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="2.3.1" />
77-
<PackageVersion Include="Uno.WinUI" Version="6.6.176" />
77+
<PackageVersion Include="Uno.WinUI" Version="6.6.184" />
7878
</ItemGroup>
7979

8080
<!-- Source generators. Roslyn is pinned to 5.0.0 rather than the newest
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
using System.Linq.Expressions;
2+
using System.Reactive.Disposables;
3+
using System.Reactive.Linq;
4+
using Avalonia;
5+
using ReactiveUI;
6+
7+
namespace Stellar.Avalonia;
8+
9+
/// <summary>
10+
/// Creates change notifications for <see cref="AvaloniaProperty"/>-backed properties on
11+
/// <see cref="AvaloniaObject"/>s (windows, controls) so that <c>WhenAnyValue</c> chains and
12+
/// bindings rooted at a view keep emitting after the initial value. Replaces
13+
/// Avalonia.ReactiveUI's AvaloniaObjectObservableForProperty, which has no ReactiveUI
14+
/// 24-compatible release. Without it, resolution falls back to
15+
/// <c>POCOObservableForProperty</c> (AvaloniaObject does not implement
16+
/// INotifyPropertyChanged), which only ever produces the current value.
17+
/// </summary>
18+
public sealed class AvaloniaObjectObservableForProperty : ICreatesObservableForProperty
19+
{
20+
public int GetAffinityForObject(Type type, string propertyName) =>
21+
GetAffinityForObject(type, propertyName, beforeChanged: false);
22+
23+
// 4 matches ReactiveUI's own platform property handlers: above the POCO fallback,
24+
// below the IReactiveObject and INPC handlers. Only claims properties actually
25+
// registered with Avalonia's property system; plain CLR properties resolve elsewhere.
26+
// The PropertyChanged event only reports values after they change, so beforeChanged
27+
// requests are declined and resolve elsewhere.
28+
public int GetAffinityForObject(Type type, string propertyName, bool beforeChanged) =>
29+
!beforeChanged &&
30+
typeof(AvaloniaObject).IsAssignableFrom(type) &&
31+
AvaloniaPropertyRegistry.Instance.FindRegistered(type, propertyName) is not null
32+
? 4
33+
: 0;
34+
35+
public IObservable<IObservedChange<object?, object?>> GetNotificationForProperty(object sender, Expression expression, string propertyName) =>
36+
GetNotificationForProperty(sender, expression, propertyName, beforeChanged: false, suppressWarnings: false);
37+
38+
public IObservable<IObservedChange<object?, object?>> GetNotificationForProperty(object sender, Expression expression, string propertyName, bool beforeChanged) =>
39+
GetNotificationForProperty(sender, expression, propertyName, beforeChanged, suppressWarnings: false);
40+
41+
public IObservable<IObservedChange<object?, object?>> GetNotificationForProperty(object sender, Expression expression, string propertyName, bool beforeChanged, bool suppressWarnings)
42+
{
43+
if (beforeChanged)
44+
{
45+
return Observable.Never<IObservedChange<object?, object?>>();
46+
}
47+
48+
if (sender is not AvaloniaObject avaloniaObject)
49+
{
50+
throw new ArgumentException($"Sender must be an AvaloniaObject, but was {sender?.GetType().FullName ?? \"null\"}.", nameof(sender));
51+
}
52+
53+
var property = AvaloniaPropertyRegistry.Instance.FindRegistered(sender.GetType(), propertyName);
54+
55+
if (property is null)
56+
{
57+
if (suppressWarnings)
58+
{
59+
return Observable.Never<IObservedChange<object?, object?>>();
60+
}
61+
62+
throw new ArgumentException($"No AvaloniaProperty named '{propertyName}' is registered on {sender.GetType().FullName}.", nameof(propertyName));
63+
}
64+
return Observable.Create<IObservedChange<object?, object?>>(
65+
observer =>
66+
{
67+
void Handler(object? _, AvaloniaPropertyChangedEventArgs args)
68+
if (args.Property == property)
69+
{
70+
observer.OnNext(new ObservedChange<object?, object?>(sender, expression, default));
71+
}
72+
}
73+
74+
avaloniaObject.PropertyChanged += Handler;
75+
76+
return Disposable.Create(() => avaloniaObject.PropertyChanged -= Handler);
77+
});
78+
}
79+
}

Stellar.Avalonia/Extensions/AppBuilderExtensions.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ public static AppBuilder UseStellarComponents(this AppBuilder appBuilder)
2323
RxAppBuilder
2424
.CreateReactiveUIBuilder()
2525
.WithRegistration(
26-
static resolver => resolver.RegisterConstant<IActivationForViewFetcher>(new AvaloniaActivationForViewFetcher()))
26+
static resolver =>
27+
{
28+
resolver.RegisterConstant<IActivationForViewFetcher>(new AvaloniaActivationForViewFetcher());
29+
resolver.RegisterConstant<ICreatesObservableForProperty>(new AvaloniaObjectObservableForProperty());
30+
})
2731
.WithMainThreadScheduler(AvaloniaScheduler.Instance)
2832
.WithTaskPoolScheduler(Schedulers.ShortTermThreadPoolScheduler)
2933
.Build();
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
using System.Linq.Expressions;
2+
using System.Reflection;
3+
using Microsoft.UI.Xaml;
4+
5+
namespace Stellar.Uno;
6+
7+
/// <summary>
8+
/// Creates change notifications for <see cref="DependencyProperty"/>-backed properties on
9+
/// <see cref="DependencyObject"/>s (pages, controls) so that <c>WhenAnyValue</c> chains and
10+
/// bindings rooted at a view keep emitting after the initial value. ReactiveUI's own
11+
/// implementation of this lives in its WinUI/Uno platform packages, none of which have a
12+
/// ReactiveUI 24-compatible Uno release, so Stellar registers this one. Without it,
13+
/// resolution falls back to <c>POCOObservableForProperty</c>, which only ever produces the
14+
/// current value.
15+
/// </summary>
16+
public sealed class DependencyObjectObservableForProperty : ICreatesObservableForProperty
17+
{
18+
public int GetAffinityForObject(Type type, string propertyName) =>
19+
GetAffinityForObject(type, propertyName, beforeChanged: false);
20+
21+
// 4 matches ReactiveUI's own DependencyObject handlers: above the POCO fallback,
22+
// below the IReactiveObject and INPC handlers, which serve those objects better.
23+
// DependencyProperty callbacks cannot observe values before they change, so
24+
// beforeChanged requests are declined and resolve elsewhere.
25+
public int GetAffinityForObject(Type type, string propertyName, bool beforeChanged)
26+
{
27+
if (beforeChanged || !typeof(DependencyObject).IsAssignableFrom(type))
28+
{
29+
return 0;
30+
}
31+
32+
return GetDependencyProperty(type, propertyName) is null ? 0 : 4;
33+
}
34+
35+
public IObservable<IObservedChange<object?, object?>> GetNotificationForProperty(object sender, Expression expression, string propertyName) =>
36+
GetNotificationForProperty(sender, expression, propertyName, beforeChanged: false, suppressWarnings: false);
37+
38+
public IObservable<IObservedChange<object?, object?>> GetNotificationForProperty(object sender, Expression expression, string propertyName, bool beforeChanged) =>
39+
GetNotificationForProperty(sender, expression, propertyName, beforeChanged, suppressWarnings: false);
40+
41+
public IObservable<IObservedChange<object?, object?>> GetNotificationForProperty(object sender, Expression expression, string propertyName, bool beforeChanged, bool suppressWarnings)
42+
{
43+
if (beforeChanged)
44+
{
45+
return Observable.Never<IObservedChange<object?, object?>>();
46+
}
47+
48+
if (sender is not DependencyObject dependencyObject)
49+
{
50+
throw new ArgumentException($"Sender must be a DependencyObject, but was {sender?.GetType().FullName ?? \"null\"}.", nameof(sender));
51+
}
52+
53+
var dependencyProperty = GetDependencyProperty(sender.GetType(), propertyName);
54+
55+
if (dependencyProperty is null)
56+
{
57+
if (suppressWarnings)
58+
{
59+
return Observable.Never<IObservedChange<object?, object?>>();
60+
}
61+
62+
throw new ArgumentException($"No DependencyProperty named '{propertyName}Property' was found on {sender.GetType().FullName}.", nameof(propertyName));
63+
}
64+
return Observable.Create<IObservedChange<object?, object?>>(
65+
observer =>
66+
{
67+
var token = dependencyObject.RegisterPropertyChangedCallback(
68+
dependencyProperty,
69+
(_, _) => observer.OnNext(new ObservedChange<object?, object?>(sender, expression, default)));
70+
71+
return Disposable.Create(() => dependencyObject.UnregisterPropertyChangedCallback(dependencyProperty, token));
72+
});
73+
}
74+
75+
private static DependencyProperty? GetDependencyProperty(Type type, string propertyName)
76+
{
77+
var memberName = propertyName + "Property";
78+
79+
for (var current = type; current is not null; current = current.BaseType)
80+
{
81+
if (current.GetProperty(memberName, BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly)?.GetValue(null) is DependencyProperty fromProperty)
82+
{
83+
return fromProperty;
84+
}
85+
86+
if (current.GetField(memberName, BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly)?.GetValue(null) is DependencyProperty fromField)
87+
{
88+
return fromField;
89+
}
90+
}
91+
92+
return null;
93+
}
94+
}

Stellar.Uno/Extensions/BuilderExtensions.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,16 @@ public static IServiceCollection UseStellarComponents(this IServiceCollection se
3030
"UseStellarComponents must be called from the UI thread so the DispatcherQueue can be captured.");
3131

3232
// There is no ReactiveUI 24-compatible Uno platform package, so the activation
33-
// fetcher and schedulers are registered here directly, mirroring Stellar.Avalonia.
33+
// fetcher, DependencyProperty observation and schedulers are registered here
34+
// directly, mirroring Stellar.Avalonia.
3435
RxAppBuilder
3536
.CreateReactiveUIBuilder()
3637
.WithRegistration(
37-
static resolver => resolver.RegisterConstant<IActivationForViewFetcher>(new UnoActivationForViewFetcher()))
38+
static resolver =>
39+
{
40+
resolver.RegisterConstant<IActivationForViewFetcher>(new UnoActivationForViewFetcher());
41+
resolver.RegisterConstant<ICreatesObservableForProperty>(new DependencyObjectObservableForProperty());
42+
})
3843
.WithMainThreadScheduler(new UnoScheduler(dispatcherQueue))
3944
.WithTaskPoolScheduler(Schedulers.ShortTermThreadPoolScheduler)
4045
.Build();

0 commit comments

Comments
 (0)