diff --git a/.github/workflows/nuget.yml b/.github/workflows/nuget.yml
index dfa7a67..f14ce38 100644
--- a/.github/workflows/nuget.yml
+++ b/.github/workflows/nuget.yml
@@ -38,6 +38,18 @@ jobs:
- name: Stellar.SourceGenerators
project: Stellar.SourceGenerators/Stellar.SourceGenerators.csproj
output: Stellar.SourceGenerators/bin/Release
+ # The Windows-targeted projects cross-compile on the macOS runner via
+ # EnableWindowsTargeting; Stellar.WinUI additionally disables the PRI/MSIX
+ # tooling on non-Windows hosts in its csproj.
+ - name: Stellar.Wpf
+ project: Stellar.Wpf/Stellar.Wpf.csproj
+ output: Stellar.Wpf/bin/Release
+ - name: Stellar.WinUI
+ project: Stellar.WinUI/Stellar.WinUI.csproj
+ output: Stellar.WinUI/bin/Release
+ - name: Stellar.Uno
+ project: Stellar.Uno/Stellar.Uno.csproj
+ output: Stellar.Uno/bin/Release
steps:
- uses: actions/checkout@v7
- uses: actions/setup-dotnet@v6
diff --git a/Directory.Packages.props b/Directory.Packages.props
index c27665d..fd52e21 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -20,15 +20,22 @@
-
+
-
-
-
+
+
+
+
+
+
-
+
@@ -36,24 +43,23 @@
-
-
-
+
+
-
+
-
-
-
-
-
-
+
+
+
+
@@ -62,6 +68,15 @@
+
+
+
+
+
+
-
-
diff --git a/Stellar.Avalonia/UserControlBase.cs b/Stellar.Avalonia/UserControlBase.cs
index 3fb62e4..a0c9bcd 100644
--- a/Stellar.Avalonia/UserControlBase.cs
+++ b/Stellar.Avalonia/UserControlBase.cs
@@ -1,17 +1,36 @@
-using System.ComponentModel;
+using System.ComponentModel;
using System.Reactive;
using System.Reactive.Disposables;
using Avalonia;
-using Avalonia.ReactiveUI;
+using Avalonia.Controls;
+using ReactiveUI;
namespace Stellar.Avalonia;
-public abstract class UserControlBase : ReactiveUserControl, IStellarView
+// Derives from UserControl directly and implements IViewFor itself: this base class
+// previously came from Avalonia.ReactiveUI's ReactiveUserControl, which has no release
+// compatible with ReactiveUI 24.
+public abstract class UserControlBase : UserControl, IStellarView
where TViewModel : class
{
+ public static readonly StyledProperty ViewModelProperty =
+ AvaloniaProperty.Register, TViewModel?>(nameof(ViewModel));
+
[EditorBrowsable(EditorBrowsableState.Never)]
public ViewManager ViewManager { get; } = new AvaloniaViewManager();
+ public TViewModel? ViewModel
+ {
+ get => GetValue(ViewModelProperty);
+ set => SetValue(ViewModelProperty, value);
+ }
+
+ object? IViewFor.ViewModel
+ {
+ get => ViewModel;
+ set => ViewModel = value as TViewModel;
+ }
+
public IObservable UserControlInitialized => ViewManager.Initialized;
public IObservable Activated => ViewManager.Activated;
@@ -65,20 +84,52 @@ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e
base.OnDetachedFromVisualTree(e);
}
+ protected override void OnDataContextChanged(EventArgs e)
+ {
+ base.OnDataContextChanged(e);
+
+ ViewModel = DataContext as TViewModel;
+ }
+
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
+ if (change.Property == ViewModelProperty)
+ {
+ var newViewModel = change.GetNewValue();
+
+ if (!ReferenceEquals(newViewModel, DataContext))
+ {
+ DataContext = newViewModel;
+ }
+ }
+
ViewManager.PropertyChanged(this, change.Property.Name);
base.OnPropertyChanged(change);
}
}
-public abstract class UserControlBase : ReactiveUserControl, IStellarView
+public abstract class UserControlBase : UserControl, IStellarView
where TViewModel : class
{
+ public static readonly StyledProperty ViewModelProperty =
+ AvaloniaProperty.Register, TViewModel?>(nameof(ViewModel));
+
[EditorBrowsable(EditorBrowsableState.Never)]
public ViewManager ViewManager { get; } = new AvaloniaViewManager();
+ public TViewModel? ViewModel
+ {
+ get => GetValue(ViewModelProperty);
+ set => SetValue(ViewModelProperty, value);
+ }
+
+ object? IViewFor.ViewModel
+ {
+ get => ViewModel;
+ set => ViewModel = value as TViewModel;
+ }
+
public IObservable UserControlInitialized => ViewManager.Initialized;
public IObservable Activated => ViewManager.Activated;
@@ -117,6 +168,9 @@ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e
base.OnDetachedFromVisualTree(e);
}
+ // No ViewModel<->DataContext syncing here: unlike the single-parameter base class,
+ // DataContext holds a TDataModel that is mapped onto the view model, so the two
+ // properties are intentionally independent.
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
ViewManager.PropertyChanged(this, change.Property.Name);
diff --git a/Stellar.Avalonia/WindowBase.cs b/Stellar.Avalonia/WindowBase.cs
index 454f4dc..2083ee7 100644
--- a/Stellar.Avalonia/WindowBase.cs
+++ b/Stellar.Avalonia/WindowBase.cs
@@ -4,17 +4,35 @@
using System.Reactive.Disposables;
using Avalonia;
using Avalonia.Controls;
-using Avalonia.ReactiveUI;
+using ReactiveUI;
using Stellar.ViewModel;
namespace Stellar.Avalonia
{
- public abstract class WindowBase : ReactiveWindow, IStellarView
+ // Derives from Window directly and implements IViewFor itself: this base class
+ // previously came from Avalonia.ReactiveUI's ReactiveWindow, which has no release
+ // compatible with ReactiveUI 24.
+ public abstract class WindowBase : Window, IStellarView
where TViewModel : class
{
+ public static readonly StyledProperty ViewModelProperty =
+ AvaloniaProperty.Register, TViewModel?>(nameof(ViewModel));
+
[EditorBrowsable(EditorBrowsableState.Never)]
public ViewManager ViewManager { get; } = new AvaloniaViewManager();
+ public TViewModel? ViewModel
+ {
+ get => GetValue(ViewModelProperty);
+ set => SetValue(ViewModelProperty, value);
+ }
+
+ object? IViewFor.ViewModel
+ {
+ get => ViewModel;
+ set => ViewModel = value as TViewModel;
+ }
+
public IObservable WindowInitialized => ViewManager.Initialized;
public IObservable WindowActivated => ViewManager.Activated;
@@ -77,8 +95,25 @@ protected override void OnClosing(WindowClosingEventArgs e)
base.OnClosing(e);
}
+ protected override void OnDataContextChanged(EventArgs e)
+ {
+ base.OnDataContextChanged(e);
+
+ ViewModel = DataContext as TViewModel;
+ }
+
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
+ if (change.Property == ViewModelProperty)
+ {
+ var newViewModel = change.GetNewValue();
+
+ if (!ReferenceEquals(newViewModel, DataContext))
+ {
+ DataContext = newViewModel;
+ }
+ }
+
ViewManager.PropertyChanged(this, change.Property.Name);
base.OnPropertyChanged(change);
diff --git a/Stellar.AvaloniaSample/GlobalUsings.cs b/Stellar.AvaloniaSample/GlobalUsings.cs
index eea879e..ae68aa3 100644
--- a/Stellar.AvaloniaSample/GlobalUsings.cs
+++ b/Stellar.AvaloniaSample/GlobalUsings.cs
@@ -2,4 +2,5 @@
global using System.Reactive.Disposables;
global using System.Reactive.Linq;
global using ReactiveUI;
+global using ReactiveUI.Reactive;
global using ReactiveUI.SourceGenerators;
diff --git a/Stellar.AvaloniaSample/Program.cs b/Stellar.AvaloniaSample/Program.cs
index e2a04fa..8a643ff 100644
--- a/Stellar.AvaloniaSample/Program.cs
+++ b/Stellar.AvaloniaSample/Program.cs
@@ -1,6 +1,5 @@
using System;
using Avalonia;
-using Avalonia.ReactiveUI;
using Stellar.Avalonia;
namespace Stellar.AvaloniaSample;
@@ -21,9 +20,8 @@ public static AppBuilder BuildAvaloniaApp() =>
.UsePlatformDetect()
.WithInterFont()
.LogToTrace()
- .UseStellarComponents()
#if DEBUG
.EnableHotReload()
#endif
- .UseReactiveUI();
+ .UseStellarComponents();
}
diff --git a/Stellar.AvaloniaSample/Stellar.AvaloniaSample.csproj b/Stellar.AvaloniaSample/Stellar.AvaloniaSample.csproj
index 6375a9c..f951fe5 100644
--- a/Stellar.AvaloniaSample/Stellar.AvaloniaSample.csproj
+++ b/Stellar.AvaloniaSample/Stellar.AvaloniaSample.csproj
@@ -18,9 +18,6 @@
-
-
-
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Stellar.Blazor/ComponentBase.cs b/Stellar.Blazor/ComponentBase.cs
index 08fc344..c9de932 100644
--- a/Stellar.Blazor/ComponentBase.cs
+++ b/Stellar.Blazor/ComponentBase.cs
@@ -3,7 +3,7 @@
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
-using ReactiveUI.Blazor;
+using ReactiveUI.Reactive.Blazor;
namespace Stellar.Blazor;
diff --git a/Stellar.Blazor/Extensions/BuilderExtensions.cs b/Stellar.Blazor/Extensions/BuilderExtensions.cs
index dea3654..92fe1c1 100644
--- a/Stellar.Blazor/Extensions/BuilderExtensions.cs
+++ b/Stellar.Blazor/Extensions/BuilderExtensions.cs
@@ -1,6 +1,6 @@
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
-using ReactiveUI.Builder;
+using ReactiveUI.Reactive.Builder;
namespace Stellar.Blazor;
diff --git a/Stellar.Blazor/GlobalUsings.cs b/Stellar.Blazor/GlobalUsings.cs
index af4247b..ac002eb 100644
--- a/Stellar.Blazor/GlobalUsings.cs
+++ b/Stellar.Blazor/GlobalUsings.cs
@@ -5,4 +5,5 @@
global using System.Reactive.Linq;
global using System.Reactive.Subjects;
global using ReactiveUI;
-global using ReactiveUI.Blazor;
\ No newline at end of file
+global using ReactiveUI.Reactive;
+global using ReactiveUI.Reactive.Blazor;
\ No newline at end of file
diff --git a/Stellar.Blazor/InjectableComponentBase.cs b/Stellar.Blazor/InjectableComponentBase.cs
index 8bb5ebe..d110f4a 100644
--- a/Stellar.Blazor/InjectableComponentBase.cs
+++ b/Stellar.Blazor/InjectableComponentBase.cs
@@ -1,7 +1,7 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Components;
-using ReactiveUI.Blazor;
+using ReactiveUI.Reactive.Blazor;
namespace Stellar.Blazor;
diff --git a/Stellar.Blazor/LayoutComponentBase.cs b/Stellar.Blazor/LayoutComponentBase.cs
index 8e7b5d0..43bc7a0 100644
--- a/Stellar.Blazor/LayoutComponentBase.cs
+++ b/Stellar.Blazor/LayoutComponentBase.cs
@@ -1,7 +1,7 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Components;
-using ReactiveUI.Blazor;
+using ReactiveUI.Reactive.Blazor;
namespace Stellar.Blazor;
diff --git a/Stellar.Blazor/Stellar.Blazor.csproj b/Stellar.Blazor/Stellar.Blazor.csproj
index 02b5be9..4117fa2 100644
--- a/Stellar.Blazor/Stellar.Blazor.csproj
+++ b/Stellar.Blazor/Stellar.Blazor.csproj
@@ -17,7 +17,7 @@
-
+
diff --git a/Stellar.BlazorSample/GlobalUsings.cs b/Stellar.BlazorSample/GlobalUsings.cs
index 17682b3..cc3bcee 100644
--- a/Stellar.BlazorSample/GlobalUsings.cs
+++ b/Stellar.BlazorSample/GlobalUsings.cs
@@ -2,4 +2,5 @@
global using System.Reactive.Disposables;
global using System.Reactive.Linq;
global using ReactiveUI;
+global using ReactiveUI.Reactive;
global using ReactiveUI.SourceGenerators;
diff --git a/Stellar.Maui.PopUp/Extensions/PopupNavigationObservableExtensions.cs b/Stellar.Maui.PopUp/Extensions/PopupNavigationObservableExtensions.cs
index b2791c6..9b97453 100644
--- a/Stellar.Maui.PopUp/Extensions/PopupNavigationObservableExtensions.cs
+++ b/Stellar.Maui.PopUp/Extensions/PopupNavigationObservableExtensions.cs
@@ -4,6 +4,7 @@
using Mopups.Pages;
using Mopups.Services;
using ReactiveUI;
+using ReactiveUI.Reactive;
using Stellar.Exceptions;
using Stellar.Maui.Exceptions;
using static Stellar.Maui.NavigationObservableExtensions;
diff --git a/Stellar.Maui.PopUp/ReactivePopupPage.cs b/Stellar.Maui.PopUp/ReactivePopupPage.cs
index ca59490..4ac774e 100644
--- a/Stellar.Maui.PopUp/ReactivePopupPage.cs
+++ b/Stellar.Maui.PopUp/ReactivePopupPage.cs
@@ -1,5 +1,6 @@
using Mopups.Pages;
using ReactiveUI;
+using ReactiveUI.Reactive;
namespace Stellar.Maui.Pages;
diff --git a/Stellar.Maui.PopUp/Stellar.Maui.PopUp.csproj b/Stellar.Maui.PopUp/Stellar.Maui.PopUp.csproj
index 7f831b4..c10aaf9 100644
--- a/Stellar.Maui.PopUp/Stellar.Maui.PopUp.csproj
+++ b/Stellar.Maui.PopUp/Stellar.Maui.PopUp.csproj
@@ -33,7 +33,7 @@
-
+
diff --git a/Stellar.Maui/Extensions/MauiAppBuilderExtensions.cs b/Stellar.Maui/Extensions/MauiAppBuilderExtensions.cs
index 020c9f4..ac6edcf 100644
--- a/Stellar.Maui/Extensions/MauiAppBuilderExtensions.cs
+++ b/Stellar.Maui/Extensions/MauiAppBuilderExtensions.cs
@@ -1,7 +1,7 @@
using System.Reflection;
using Microsoft.Extensions.DependencyInjection.Extensions;
-using ReactiveUI.Builder;
-using ReactiveUI.Maui;
+using ReactiveUI.Reactive.Builder;
+using ReactiveUI.Reactive.Maui;
using Splat;
namespace Stellar.Maui;
diff --git a/Stellar.Maui/Extensions/VisualElementExtensions.cs b/Stellar.Maui/Extensions/VisualElementExtensions.cs
index 6fe753f..57404cd 100644
--- a/Stellar.Maui/Extensions/VisualElementExtensions.cs
+++ b/Stellar.Maui/Extensions/VisualElementExtensions.cs
@@ -1,5 +1,4 @@
using System.Runtime.CompilerServices;
-using DynamicData.Diagnostics;
using Stellar.Exceptions;
using Stellar.ViewModel;
diff --git a/Stellar.Maui/GlobalUsings.cs b/Stellar.Maui/GlobalUsings.cs
index 3859fcc..da99508 100644
--- a/Stellar.Maui/GlobalUsings.cs
+++ b/Stellar.Maui/GlobalUsings.cs
@@ -10,4 +10,10 @@
global using System.Reactive.Linq;
global using System.Reactive.Subjects;
global using ReactiveUI;
+
+// Stellar's own MAUI view wrappers (ReactiveStackLayout, ReactiveGrid, ReactiveViewCell)
+// live in the ReactiveUI.Maui namespaces for historical compatibility; the ReactiveUI 24
+// platform types now come from ReactiveUI.Reactive.Maui.
global using ReactiveUI.Maui;
+global using ReactiveUI.Reactive;
+global using ReactiveUI.Reactive.Maui;
diff --git a/Stellar.Maui/Stellar.Maui.csproj b/Stellar.Maui/Stellar.Maui.csproj
index ff80ba7..634273d 100644
--- a/Stellar.Maui/Stellar.Maui.csproj
+++ b/Stellar.Maui/Stellar.Maui.csproj
@@ -33,7 +33,7 @@
-
+
diff --git a/Stellar.Maui/Views/ReactiveViewCell.cs b/Stellar.Maui/Views/ReactiveViewCell.cs
new file mode 100644
index 0000000..3fa53b7
--- /dev/null
+++ b/Stellar.Maui/Views/ReactiveViewCell.cs
@@ -0,0 +1,59 @@
+// StellarUI continues to support ListView for as long as .NET MAUI ships it. MAUI marks
+// ListView and its cell types obsolete in favour of CollectionView, but removing this
+// surface would break every consumer still using it, so the deprecation is suppressed here
+// rather than propagated. Revisit when MAUI actually removes the types.
+#pragma warning disable CS0618 // Type or member is obsolete
+
+// ReactiveUI.Maui shipped a ReactiveViewCell up to version 23; ReactiveUI 24 dropped it,
+// so Stellar provides its own. It lives in this namespace alongside ReactiveGrid and
+// ReactiveStackLayout, which Stellar has always supplied itself.
+namespace ReactiveUI.Maui.Views;
+
+///
+/// This is a that is also an .
+///
+/// The type of the view model.
+///
+///
+public class ReactiveViewCell : ViewCell, IViewFor
+ where TViewModel : class
+{
+ ///
+ /// The view model bindable property.
+ ///
+ public static readonly BindableProperty ViewModelProperty = BindableProperty.Create(
+ nameof(ViewModel),
+ typeof(TViewModel),
+ typeof(ReactiveViewCell),
+ default(TViewModel),
+ BindingMode.OneWay,
+ propertyChanged: OnViewModelChanged);
+
+ ///
+ /// Gets or sets the ViewModel to display.
+ ///
+ public TViewModel? ViewModel
+ {
+ get => (TViewModel)GetValue(ViewModelProperty);
+ set => SetValue(ViewModelProperty, value);
+ }
+
+ ///
+ object? IViewFor.ViewModel
+ {
+ get => ViewModel;
+ set => ViewModel = value as TViewModel;
+ }
+
+ ///
+ protected override void OnBindingContextChanged()
+ {
+ base.OnBindingContextChanged();
+ ViewModel = BindingContext as TViewModel;
+ }
+
+ private static void OnViewModelChanged(BindableObject bindableObject, object oldValue, object newValue)
+ {
+ bindableObject.BindingContext = newValue;
+ }
+}
diff --git a/Stellar.Maui/Views/ViewCellBase.cs b/Stellar.Maui/Views/ViewCellBase.cs
index 9a2e2b3..fc8f29d 100644
--- a/Stellar.Maui/Views/ViewCellBase.cs
+++ b/Stellar.Maui/Views/ViewCellBase.cs
@@ -5,6 +5,7 @@
#pragma warning disable CS0618 // Type or member is obsolete
using System.ComponentModel;
+using ReactiveUI.Maui.Views;
namespace Stellar.Maui.Views;
diff --git a/Stellar.MauiBlazorHybridSample/GlobalUsings.cs b/Stellar.MauiBlazorHybridSample/GlobalUsings.cs
index adfb3db..bffb3e3 100644
--- a/Stellar.MauiBlazorHybridSample/GlobalUsings.cs
+++ b/Stellar.MauiBlazorHybridSample/GlobalUsings.cs
@@ -2,6 +2,7 @@
global using System.Reactive.Disposables;
global using System.Reactive.Linq;
global using ReactiveUI;
+global using ReactiveUI.Reactive;
global using ReactiveUI.SourceGenerators;
global using Stellar.Maui;
global using Stellar.Maui.Pages;
diff --git a/Stellar.MauiSample/GlobalUsings.cs b/Stellar.MauiSample/GlobalUsings.cs
index 97d8ff8..8ae01f2 100644
--- a/Stellar.MauiSample/GlobalUsings.cs
+++ b/Stellar.MauiSample/GlobalUsings.cs
@@ -3,6 +3,7 @@
global using System.Reactive.Linq;
global using CommunityToolkit.Maui.Markup;
global using ReactiveUI;
+global using ReactiveUI.Reactive;
global using ReactiveUI.SourceGenerators;
global using Stellar.Maui;
global using Stellar.Maui.Pages;
diff --git a/Stellar.MauiSample/Stellar.MauiSample.csproj b/Stellar.MauiSample/Stellar.MauiSample.csproj
index 7ec06a2..1170d58 100644
--- a/Stellar.MauiSample/Stellar.MauiSample.csproj
+++ b/Stellar.MauiSample/Stellar.MauiSample.csproj
@@ -72,7 +72,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Stellar.UnitTests/Usings.cs b/Stellar.UnitTests/Usings.cs
index 9df1d42..28a1f4e 100644
--- a/Stellar.UnitTests/Usings.cs
+++ b/Stellar.UnitTests/Usings.cs
@@ -1 +1,2 @@
-global using Xunit;
+global using ReactiveUI.Reactive;
+global using Xunit;
diff --git a/Stellar.Uno/Extensions/BuilderExtensions.cs b/Stellar.Uno/Extensions/BuilderExtensions.cs
new file mode 100644
index 0000000..2b58068
--- /dev/null
+++ b/Stellar.Uno/Extensions/BuilderExtensions.cs
@@ -0,0 +1,51 @@
+using System.Reflection;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.UI.Dispatching;
+using ReactiveUI.Reactive.Builder;
+
+namespace Stellar.Uno;
+
+public static class BuilderExtensions
+{
+ public static IServiceCollection UseStellarComponents(this IServiceCollection services)
+ {
+ UseStellarComponents(services);
+
+ services.ConfigureStellarComponents(typeof(TStellarAssembly).GetTypeInfo().Assembly);
+
+ return services;
+ }
+
+ ///
+ /// Registers Stellar's Uno Platform services. Must be called from the UI thread
+ /// (e.g. in Application.OnLaunched) so the UI thread's
+ /// can be captured for the main-thread scheduler.
+ ///
+ /// The service collection.
+ /// The same service collection, for chaining.
+ public static IServiceCollection UseStellarComponents(this IServiceCollection services)
+ {
+ var dispatcherQueue = DispatcherQueue.GetForCurrentThread()
+ ?? throw new InvalidOperationException(
+ "UseStellarComponents must be called from the UI thread so the DispatcherQueue can be captured.");
+
+ // There is no ReactiveUI 24-compatible Uno platform package, so the activation
+ // fetcher and schedulers are registered here directly, mirroring Stellar.Avalonia.
+ RxAppBuilder
+ .CreateReactiveUIBuilder()
+ .WithRegistration(
+ static resolver => resolver.RegisterConstant(new UnoActivationForViewFetcher()))
+ .WithMainThreadScheduler(new UnoScheduler(dispatcherQueue))
+ .WithTaskPoolScheduler(Schedulers.ShortTermThreadPoolScheduler)
+ .Build();
+
+ return services;
+ }
+
+ public static IServiceCollection EnableHotReload(this IServiceCollection services)
+ {
+ HotReloadService.HotReloadAware = true;
+
+ return services;
+ }
+}
diff --git a/Stellar.Uno/GlobalUsings.cs b/Stellar.Uno/GlobalUsings.cs
new file mode 100644
index 0000000..bb463e0
--- /dev/null
+++ b/Stellar.Uno/GlobalUsings.cs
@@ -0,0 +1,8 @@
+global using System;
+global using System.Reactive;
+global using System.Reactive.Concurrency;
+global using System.Reactive.Disposables;
+global using System.Reactive.Linq;
+global using System.Reactive.Subjects;
+global using ReactiveUI;
+global using ReactiveUI.Reactive;
diff --git a/Stellar.Uno/HotReloadService.cs b/Stellar.Uno/HotReloadService.cs
new file mode 100644
index 0000000..e7fc0a9
--- /dev/null
+++ b/Stellar.Uno/HotReloadService.cs
@@ -0,0 +1,21 @@
+#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
+[assembly: System.Reflection.Metadata.MetadataUpdateHandler(typeof(Stellar.Uno.HotReloadService))]
+
+namespace Stellar.Uno;
+
+public static class HotReloadService
+{
+ public static bool HotReloadAware { get; set; }
+
+ public static event Action? UpdateApplicationEvent;
+
+ internal static void ClearCache(Type[]? types)
+ {
+ }
+
+ internal static void UpdateApplication(Type[]? types)
+ {
+ UpdateApplicationEvent?.Invoke(types);
+ }
+}
+#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
diff --git a/Stellar.Uno/PageBase.cs b/Stellar.Uno/PageBase.cs
new file mode 100644
index 0000000..de27e92
--- /dev/null
+++ b/Stellar.Uno/PageBase.cs
@@ -0,0 +1,117 @@
+using System.ComponentModel;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Navigation;
+
+namespace Stellar.Uno;
+
+// Implements IViewFor directly: there is no ReactiveUI 24-compatible Uno platform
+// package to supply a ReactivePage base class, so Stellar provides the ViewModel
+// dependency property and DataContext syncing itself.
+public abstract class PageBase : Page, IStellarView
+ where TViewModel : class
+{
+ public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(
+ nameof(ViewModel),
+ typeof(TViewModel),
+ typeof(PageBase),
+ new PropertyMetadata(null));
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public ViewManager ViewManager { get; } = new UnoViewManager();
+
+ public TViewModel? ViewModel
+ {
+ get => (TViewModel?)GetValue(ViewModelProperty);
+ set => SetValue(ViewModelProperty, value);
+ }
+
+ object? IViewFor.ViewModel
+ {
+ get => ViewModel;
+ set => ViewModel = value as TViewModel;
+ }
+
+ public IObservable PageInitialized => ViewManager.Initialized;
+
+ public IObservable Activated => ViewManager.Activated;
+
+ public IObservable Deactivated => ViewManager.Deactivated;
+
+ public IObservable IsAppearing => ViewManager.IsAppearing;
+
+ public IObservable IsDisappearing => ViewManager.IsDisappearing;
+
+ public IObservable NavigatedTo => ViewManager.NavigatedTo;
+
+ public IObservable NavigatedFrom => ViewManager.NavigatedFrom;
+
+ public IObservable Disposed => ViewManager.Disposed;
+
+ public IObservable LifecycleEvents => ViewManager.LifecycleEvents;
+
+ protected PageBase()
+ : this(manuallyInitialize: true)
+ {
+ }
+
+ protected PageBase(
+ TViewModel? viewModel = null,
+ bool maintain = false,
+ bool delayBindingRegistrationUntilAttached = false,
+ bool manuallyInitialize = true)
+ {
+ Loaded += HandleLoaded;
+ Unloaded += HandleUnloaded;
+ DataContextChanged += HandleDataContextChanged;
+ RegisterPropertyChangedCallback(
+ ViewModelProperty,
+ (_, _) => ViewManager.PropertyChanged(this, nameof(ViewModel)));
+
+ if (!manuallyInitialize)
+ {
+ this.InitializeStellarComponent(viewModel, maintain, delayBindingRegistrationUntilAttached);
+ }
+ }
+
+ public virtual void Initialize()
+ {
+ }
+
+ public abstract void SetupUserInterface();
+
+ public abstract void Bind(WeakCompositeDisposable disposables);
+
+ protected override void OnNavigatedTo(NavigationEventArgs e)
+ {
+ base.OnNavigatedTo(e);
+
+ ViewManager.OnNavigating(this, NavigationEvent.NavigatedTo);
+ }
+
+ protected override void OnNavigatedFrom(NavigationEventArgs e)
+ {
+ ViewManager.OnNavigating(this, NavigationEvent.NavigatedFrom);
+
+ base.OnNavigatedFrom(e);
+ }
+
+ private void HandleLoaded(object sender, RoutedEventArgs e)
+ {
+ ViewManager.HandleActivated(this);
+
+ ViewManager.OnLifecycle(this, LifecycleEvent.IsAppearing);
+ }
+
+ private void HandleUnloaded(object sender, RoutedEventArgs e)
+ {
+ ViewManager.OnLifecycle(this, LifecycleEvent.IsDisappearing);
+
+ ViewManager.HandleDeactivated(this);
+ }
+
+ private void HandleDataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
+ {
+ ViewModel = args.NewValue as TViewModel;
+ }
+}
diff --git a/Stellar.Uno/Stellar.Uno.csproj b/Stellar.Uno/Stellar.Uno.csproj
new file mode 100644
index 0000000..9a79e24
--- /dev/null
+++ b/Stellar.Uno/Stellar.Uno.csproj
@@ -0,0 +1,27 @@
+
+
+ net10.0
+ true
+ enable
+ enable
+ StellarUI.Uno
+ Stellar UI - Uno Platform
+ Eight-Bot
+ Eight-Bot 2026
+ https://github.com/TheEightBot/StellarUI/blob/main/LICENSE
+ https://github.com/TheEightBot/StellarUI
+ Uno Platform;MVVM;Reactive UI;Reactive Extensions;Eight-Bot
+
+
+
+
+
+
+
+
+
+
diff --git a/Stellar.Uno/UnoActivationForViewFetcher.cs b/Stellar.Uno/UnoActivationForViewFetcher.cs
new file mode 100644
index 0000000..e8916e3
--- /dev/null
+++ b/Stellar.Uno/UnoActivationForViewFetcher.cs
@@ -0,0 +1,37 @@
+using Microsoft.UI.Xaml;
+
+namespace Stellar.Uno;
+
+///
+/// Determines when Uno Platform views are activated and deactivated so that
+/// WhenActivated works for them. There is no ReactiveUI 24-compatible Uno
+/// platform package, so Stellar provides this itself. Views follow
+/// / .
+///
+public sealed class UnoActivationForViewFetcher : IActivationForViewFetcher
+{
+ public int GetAffinityForView(Type view) =>
+ typeof(FrameworkElement).IsAssignableFrom(view) ? 10 : 0;
+
+ public IObservable GetActivationForView(IActivatableView view)
+ {
+ if (view is not FrameworkElement element)
+ {
+ return Observable.Return(false);
+ }
+
+ var loaded = Observable
+ .FromEventPattern(
+ handler => element.Loaded += handler,
+ handler => element.Loaded -= handler)
+ .Select(static _ => true);
+
+ var unloaded = Observable
+ .FromEventPattern(
+ handler => element.Unloaded += handler,
+ handler => element.Unloaded -= handler)
+ .Select(static _ => false);
+
+ return loaded.Merge(unloaded).DistinctUntilChanged();
+ }
+}
diff --git a/Stellar.Uno/UnoScheduler.cs b/Stellar.Uno/UnoScheduler.cs
new file mode 100644
index 0000000..72d362f
--- /dev/null
+++ b/Stellar.Uno/UnoScheduler.cs
@@ -0,0 +1,80 @@
+using Microsoft.UI.Dispatching;
+
+namespace Stellar.Uno;
+
+///
+/// An that schedules work onto an Uno Platform UI thread via its
+/// . There is no ReactiveUI 24-compatible Uno platform
+/// package, so Stellar provides this itself. Work scheduled with no due time from the UI
+/// thread runs inline.
+///
+public sealed class UnoScheduler : IScheduler
+{
+ private readonly DispatcherQueue _dispatcherQueue;
+
+ public UnoScheduler(DispatcherQueue dispatcherQueue)
+ {
+ ArgumentNullException.ThrowIfNull(dispatcherQueue);
+
+ _dispatcherQueue = dispatcherQueue;
+ }
+
+ public DateTimeOffset Now => DateTimeOffset.Now;
+
+ public IDisposable Schedule(TState state, Func action)
+ {
+ var innerDisp = new SingleAssignmentDisposable();
+
+ if (_dispatcherQueue.HasThreadAccess)
+ {
+ innerDisp.Disposable = action(this, state);
+
+ return innerDisp;
+ }
+
+ _dispatcherQueue.TryEnqueue(
+ () =>
+ {
+ if (!innerDisp.IsDisposed)
+ {
+ innerDisp.Disposable = action(this, state);
+ }
+ });
+
+ return innerDisp;
+ }
+
+ public IDisposable Schedule(TState state, TimeSpan dueTime, Func action)
+ {
+ if (dueTime <= TimeSpan.Zero)
+ {
+ return Schedule(state, action);
+ }
+
+ var innerDisp = new SingleAssignmentDisposable();
+ var timer = _dispatcherQueue.CreateTimer();
+ timer.Interval = dueTime;
+ timer.IsRepeating = false;
+ timer.Tick += (_, _) =>
+ {
+ if (!innerDisp.IsDisposed)
+ {
+ innerDisp.Disposable = action(this, state);
+ }
+ };
+ timer.Start();
+
+ return new CompositeDisposable(
+ Disposable.Create(timer, static t => t.Stop()),
+ innerDisp);
+ }
+
+ public IDisposable Schedule(TState state, DateTimeOffset dueTime, Func action)
+ {
+ var relative = dueTime - Now;
+
+ return relative <= TimeSpan.Zero
+ ? Schedule(state, action)
+ : Schedule(state, relative, action);
+ }
+}
diff --git a/Stellar.Uno/UnoViewManager.cs b/Stellar.Uno/UnoViewManager.cs
new file mode 100644
index 0000000..c182378
--- /dev/null
+++ b/Stellar.Uno/UnoViewManager.cs
@@ -0,0 +1,66 @@
+using Microsoft.UI.Xaml;
+
+namespace Stellar.Uno;
+
+public class UnoViewManager : ViewManager
+ where TViewModel : class
+{
+ // Null whenever no view is activated, which HandleDeactivated relies on.
+ private IStellarView? _view;
+
+ public override void HandleActivated(IStellarView view)
+ {
+ base.HandleActivated(view);
+
+ if (HotReloadService.HotReloadAware)
+ {
+ _view = view;
+ HotReloadService.UpdateApplicationEvent -= HandleHotReload;
+ HotReloadService.UpdateApplicationEvent += HandleHotReload;
+ }
+ }
+
+ public override void HandleDeactivated(IStellarView view)
+ {
+ if (HotReloadService.HotReloadAware)
+ {
+ _view = null;
+ HotReloadService.UpdateApplicationEvent -= HandleHotReload;
+ }
+
+ base.HandleDeactivated(view);
+ }
+
+#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
+ private void HandleHotReload(Type[]? updatedTypes)
+#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
+ {
+ if (_view is not FrameworkElement fe)
+ {
+ return;
+ }
+
+ fe.DispatcherQueue.TryEnqueue(
+ () =>
+ {
+ var view = _view;
+
+ if (view is null)
+ {
+ return;
+ }
+
+ var maintainStatus = view.ViewManager.Maintain;
+
+ view.ViewManager.Maintain = false;
+
+ view.ViewManager.UnregisterBindings(view);
+
+ view.ViewManager.Maintain = maintainStatus;
+
+ view.SetupUserInterface();
+
+ view.ViewManager.RegisterBindings(view);
+ });
+ }
+}
diff --git a/Stellar.Uno/UserControlBase.cs b/Stellar.Uno/UserControlBase.cs
new file mode 100644
index 0000000..0a02f52
--- /dev/null
+++ b/Stellar.Uno/UserControlBase.cs
@@ -0,0 +1,94 @@
+using System.ComponentModel;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+
+namespace Stellar.Uno;
+
+// Implements IViewFor directly: there is no ReactiveUI 24-compatible Uno platform
+// package to supply a ReactiveUserControl base class, so Stellar provides the ViewModel
+// dependency property and DataContext syncing itself.
+public abstract class UserControlBase : UserControl, IStellarView
+ where TViewModel : class
+{
+ public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(
+ nameof(ViewModel),
+ typeof(TViewModel),
+ typeof(UserControlBase),
+ new PropertyMetadata(null));
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public ViewManager ViewManager { get; } = new UnoViewManager();
+
+ public TViewModel? ViewModel
+ {
+ get => (TViewModel?)GetValue(ViewModelProperty);
+ set => SetValue(ViewModelProperty, value);
+ }
+
+ object? IViewFor.ViewModel
+ {
+ get => ViewModel;
+ set => ViewModel = value as TViewModel;
+ }
+
+ public IObservable UserControlInitialized => ViewManager.Initialized;
+
+ public IObservable Activated => ViewManager.Activated;
+
+ public IObservable Deactivated => ViewManager.Deactivated;
+
+ public IObservable Disposed => ViewManager.Disposed;
+
+ public IObservable LifecycleEvents => ViewManager.LifecycleEvents;
+
+ protected UserControlBase()
+ : this(manuallyInitialize: true)
+ {
+ }
+
+ protected UserControlBase(
+ TViewModel? viewModel = null,
+ bool maintain = false,
+ bool delayBindingRegistrationUntilAttached = false,
+ bool manuallyInitialize = true)
+ {
+ Loaded += HandleLoaded;
+ Unloaded += HandleUnloaded;
+ DataContextChanged += HandleDataContextChanged;
+ RegisterPropertyChangedCallback(
+ ViewModelProperty,
+ (_, _) => ViewManager.PropertyChanged(this, nameof(ViewModel)));
+
+ if (!manuallyInitialize)
+ {
+ this.InitializeStellarComponent(viewModel, maintain, delayBindingRegistrationUntilAttached);
+ }
+ }
+
+ public virtual void Initialize()
+ {
+ }
+
+ public abstract void SetupUserInterface();
+
+ public abstract void Bind(WeakCompositeDisposable disposables);
+
+ private void HandleLoaded(object sender, RoutedEventArgs e)
+ {
+ ViewManager.HandleActivated(this);
+
+ ViewManager.OnLifecycle(this, LifecycleEvent.IsAppearing);
+ }
+
+ private void HandleUnloaded(object sender, RoutedEventArgs e)
+ {
+ ViewManager.OnLifecycle(this, LifecycleEvent.IsDisappearing);
+
+ ViewManager.HandleDeactivated(this);
+ }
+
+ private void HandleDataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
+ {
+ ViewModel = args.NewValue as TViewModel;
+ }
+}
diff --git a/Stellar.WinUI/Extensions/BuilderExtensions.cs b/Stellar.WinUI/Extensions/BuilderExtensions.cs
new file mode 100644
index 0000000..2157fb8
--- /dev/null
+++ b/Stellar.WinUI/Extensions/BuilderExtensions.cs
@@ -0,0 +1,39 @@
+using System.Reflection;
+using Microsoft.Extensions.DependencyInjection;
+using ReactiveUI.Builder;
+using ReactiveUI.Reactive.Builder;
+
+namespace Stellar.WinUI;
+
+public static class BuilderExtensions
+{
+ public static IServiceCollection UseStellarComponents(this IServiceCollection services)
+ {
+ UseStellarComponents(services);
+
+ services.ConfigureStellarComponents(typeof(TStellarAssembly).GetTypeInfo().Assembly);
+
+ return services;
+ }
+
+ public static IServiceCollection UseStellarComponents(this IServiceCollection services)
+ {
+ // WithWinUI registers the WinUI platform services (activation fetcher,
+ // main-thread scheduler, binding hooks) that ReactiveUI's old
+ // InitializeReactiveUI selected.
+ RxAppBuilder
+ .CreateReactiveUIBuilder()
+ .WithWinUI()
+ .WithTaskPoolScheduler(Schedulers.ShortTermThreadPoolScheduler)
+ .Build();
+
+ return services;
+ }
+
+ public static IServiceCollection EnableHotReload(this IServiceCollection services)
+ {
+ HotReloadService.HotReloadAware = true;
+
+ return services;
+ }
+}
diff --git a/Stellar.WinUI/GlobalUsings.cs b/Stellar.WinUI/GlobalUsings.cs
new file mode 100644
index 0000000..bb463e0
--- /dev/null
+++ b/Stellar.WinUI/GlobalUsings.cs
@@ -0,0 +1,8 @@
+global using System;
+global using System.Reactive;
+global using System.Reactive.Concurrency;
+global using System.Reactive.Disposables;
+global using System.Reactive.Linq;
+global using System.Reactive.Subjects;
+global using ReactiveUI;
+global using ReactiveUI.Reactive;
diff --git a/Stellar.WinUI/HotReloadService.cs b/Stellar.WinUI/HotReloadService.cs
new file mode 100644
index 0000000..11c0a0c
--- /dev/null
+++ b/Stellar.WinUI/HotReloadService.cs
@@ -0,0 +1,21 @@
+#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
+[assembly: System.Reflection.Metadata.MetadataUpdateHandler(typeof(Stellar.WinUI.HotReloadService))]
+
+namespace Stellar.WinUI;
+
+public static class HotReloadService
+{
+ public static bool HotReloadAware { get; set; }
+
+ public static event Action? UpdateApplicationEvent;
+
+ internal static void ClearCache(Type[]? types)
+ {
+ }
+
+ internal static void UpdateApplication(Type[]? types)
+ {
+ UpdateApplicationEvent?.Invoke(types);
+ }
+}
+#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
diff --git a/Stellar.WinUI/PageBase.cs b/Stellar.WinUI/PageBase.cs
new file mode 100644
index 0000000..44616a9
--- /dev/null
+++ b/Stellar.WinUI/PageBase.cs
@@ -0,0 +1,92 @@
+using System.ComponentModel;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Navigation;
+
+namespace Stellar.WinUI;
+
+// ReactivePage supplies the ViewModel dependency property and IViewFor plumbing; Stellar
+// layers its ViewManager lifecycle on top. WinUI has no all-property change virtual, so
+// ViewModel changes are forwarded through RegisterPropertyChangedCallback instead.
+public abstract class PageBase : ReactivePage, IStellarView
+ where TViewModel : class
+{
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public ViewManager ViewManager { get; } = new WinUIViewManager();
+
+ public IObservable PageInitialized => ViewManager.Initialized;
+
+ public IObservable Activated => ViewManager.Activated;
+
+ public IObservable Deactivated => ViewManager.Deactivated;
+
+ public IObservable IsAppearing => ViewManager.IsAppearing;
+
+ public IObservable IsDisappearing => ViewManager.IsDisappearing;
+
+ public IObservable NavigatedTo => ViewManager.NavigatedTo;
+
+ public IObservable NavigatedFrom => ViewManager.NavigatedFrom;
+
+ public IObservable Disposed => ViewManager.Disposed;
+
+ public IObservable LifecycleEvents => ViewManager.LifecycleEvents;
+
+ protected PageBase()
+ : this(manuallyInitialize: true)
+ {
+ }
+
+ protected PageBase(
+ TViewModel? viewModel = null,
+ bool maintain = false,
+ bool delayBindingRegistrationUntilAttached = false,
+ bool manuallyInitialize = true)
+ {
+ Loaded += HandleLoaded;
+ Unloaded += HandleUnloaded;
+ RegisterPropertyChangedCallback(
+ ViewModelProperty,
+ (_, _) => ViewManager.PropertyChanged(this, nameof(ViewModel)));
+
+ if (!manuallyInitialize)
+ {
+ this.InitializeStellarComponent(viewModel, maintain, delayBindingRegistrationUntilAttached);
+ }
+ }
+
+ public virtual void Initialize()
+ {
+ }
+
+ public abstract void SetupUserInterface();
+
+ public abstract void Bind(WeakCompositeDisposable disposables);
+
+ protected override void OnNavigatedTo(NavigationEventArgs e)
+ {
+ base.OnNavigatedTo(e);
+
+ ViewManager.OnNavigating(this, NavigationEvent.NavigatedTo);
+ }
+
+ protected override void OnNavigatedFrom(NavigationEventArgs e)
+ {
+ ViewManager.OnNavigating(this, NavigationEvent.NavigatedFrom);
+
+ base.OnNavigatedFrom(e);
+ }
+
+ private void HandleLoaded(object sender, RoutedEventArgs e)
+ {
+ ViewManager.HandleActivated(this);
+
+ ViewManager.OnLifecycle(this, LifecycleEvent.IsAppearing);
+ }
+
+ private void HandleUnloaded(object sender, RoutedEventArgs e)
+ {
+ ViewManager.OnLifecycle(this, LifecycleEvent.IsDisappearing);
+
+ ViewManager.HandleDeactivated(this);
+ }
+}
diff --git a/Stellar.WinUI/Stellar.WinUI.csproj b/Stellar.WinUI/Stellar.WinUI.csproj
new file mode 100644
index 0000000..5635d79
--- /dev/null
+++ b/Stellar.WinUI/Stellar.WinUI.csproj
@@ -0,0 +1,32 @@
+
+
+ net10.0-windows10.0.19041.0
+ true
+
+ true
+ false
+ false
+ false
+ true
+ enable
+ enable
+ StellarUI.WinUI
+ Stellar UI - WinUI 3
+ Eight-Bot
+ Eight-Bot 2026
+ https://github.com/TheEightBot/StellarUI/blob/main/LICENSE
+ https://github.com/TheEightBot/StellarUI
+ WinUI;Windows App SDK;MVVM;Reactive UI;Reactive Extensions;Eight-Bot
+
+
+
+
+
+
+
+
+
diff --git a/Stellar.WinUI/UserControlBase.cs b/Stellar.WinUI/UserControlBase.cs
new file mode 100644
index 0000000..f9a4cb1
--- /dev/null
+++ b/Stellar.WinUI/UserControlBase.cs
@@ -0,0 +1,69 @@
+using System.ComponentModel;
+using Microsoft.UI.Xaml;
+
+namespace Stellar.WinUI;
+
+// ReactiveUserControl supplies the ViewModel dependency property and IViewFor plumbing;
+// Stellar layers its ViewManager lifecycle on top. WinUI has no all-property change
+// virtual, so ViewModel changes are forwarded through RegisterPropertyChangedCallback.
+public abstract class UserControlBase : ReactiveUserControl, IStellarView
+ where TViewModel : class
+{
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public ViewManager ViewManager { get; } = new WinUIViewManager();
+
+ public IObservable UserControlInitialized => ViewManager.Initialized;
+
+ public IObservable Activated => ViewManager.Activated;
+
+ public IObservable Deactivated => ViewManager.Deactivated;
+
+ public IObservable Disposed => ViewManager.Disposed;
+
+ public IObservable LifecycleEvents => ViewManager.LifecycleEvents;
+
+ protected UserControlBase()
+ : this(manuallyInitialize: true)
+ {
+ }
+
+ protected UserControlBase(
+ TViewModel? viewModel = null,
+ bool maintain = false,
+ bool delayBindingRegistrationUntilAttached = false,
+ bool manuallyInitialize = true)
+ {
+ Loaded += HandleLoaded;
+ Unloaded += HandleUnloaded;
+ RegisterPropertyChangedCallback(
+ ViewModelProperty,
+ (_, _) => ViewManager.PropertyChanged(this, nameof(ViewModel)));
+
+ if (!manuallyInitialize)
+ {
+ this.InitializeStellarComponent(viewModel, maintain, delayBindingRegistrationUntilAttached);
+ }
+ }
+
+ public virtual void Initialize()
+ {
+ }
+
+ public abstract void SetupUserInterface();
+
+ public abstract void Bind(WeakCompositeDisposable disposables);
+
+ private void HandleLoaded(object sender, RoutedEventArgs e)
+ {
+ ViewManager.HandleActivated(this);
+
+ ViewManager.OnLifecycle(this, LifecycleEvent.IsAppearing);
+ }
+
+ private void HandleUnloaded(object sender, RoutedEventArgs e)
+ {
+ ViewManager.OnLifecycle(this, LifecycleEvent.IsDisappearing);
+
+ ViewManager.HandleDeactivated(this);
+ }
+}
diff --git a/Stellar.WinUI/WinUIViewManager.cs b/Stellar.WinUI/WinUIViewManager.cs
new file mode 100644
index 0000000..863ee8e
--- /dev/null
+++ b/Stellar.WinUI/WinUIViewManager.cs
@@ -0,0 +1,66 @@
+using Microsoft.UI.Xaml;
+
+namespace Stellar.WinUI;
+
+public class WinUIViewManager : ViewManager
+ where TViewModel : class
+{
+ // Null whenever no view is activated, which HandleDeactivated relies on.
+ private IStellarView? _view;
+
+ public override void HandleActivated(IStellarView view)
+ {
+ base.HandleActivated(view);
+
+ if (HotReloadService.HotReloadAware)
+ {
+ _view = view;
+ HotReloadService.UpdateApplicationEvent -= HandleHotReload;
+ HotReloadService.UpdateApplicationEvent += HandleHotReload;
+ }
+ }
+
+ public override void HandleDeactivated(IStellarView view)
+ {
+ if (HotReloadService.HotReloadAware)
+ {
+ _view = null;
+ HotReloadService.UpdateApplicationEvent -= HandleHotReload;
+ }
+
+ base.HandleDeactivated(view);
+ }
+
+#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
+ private void HandleHotReload(Type[]? updatedTypes)
+#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
+ {
+ if (_view is not FrameworkElement fe)
+ {
+ return;
+ }
+
+ fe.DispatcherQueue.TryEnqueue(
+ () =>
+ {
+ var view = _view;
+
+ if (view is null)
+ {
+ return;
+ }
+
+ var maintainStatus = view.ViewManager.Maintain;
+
+ view.ViewManager.Maintain = false;
+
+ view.ViewManager.UnregisterBindings(view);
+
+ view.ViewManager.Maintain = maintainStatus;
+
+ view.SetupUserInterface();
+
+ view.ViewManager.RegisterBindings(view);
+ });
+ }
+}
diff --git a/Stellar.Wpf/Extensions/BuilderExtensions.cs b/Stellar.Wpf/Extensions/BuilderExtensions.cs
new file mode 100644
index 0000000..6ad227c
--- /dev/null
+++ b/Stellar.Wpf/Extensions/BuilderExtensions.cs
@@ -0,0 +1,37 @@
+using System.Reflection;
+using Microsoft.Extensions.DependencyInjection;
+using ReactiveUI.Reactive.Builder;
+
+namespace Stellar.Wpf;
+
+public static class BuilderExtensions
+{
+ public static IServiceCollection UseStellarComponents(this IServiceCollection services)
+ {
+ UseStellarComponents(services);
+
+ services.ConfigureStellarComponents(typeof(TStellarAssembly).GetTypeInfo().Assembly);
+
+ return services;
+ }
+
+ public static IServiceCollection UseStellarComponents(this IServiceCollection services)
+ {
+ // WithWpf registers the WPF platform services (activation fetcher, main-thread
+ // scheduler, binding hooks) that ReactiveUI's old InitializeReactiveUI selected.
+ RxAppBuilder
+ .CreateReactiveUIBuilder()
+ .WithWpf()
+ .WithTaskPoolScheduler(Schedulers.ShortTermThreadPoolScheduler)
+ .Build();
+
+ return services;
+ }
+
+ public static IServiceCollection EnableHotReload(this IServiceCollection services)
+ {
+ HotReloadService.HotReloadAware = true;
+
+ return services;
+ }
+}
diff --git a/Stellar.Wpf/GlobalUsings.cs b/Stellar.Wpf/GlobalUsings.cs
new file mode 100644
index 0000000..bb463e0
--- /dev/null
+++ b/Stellar.Wpf/GlobalUsings.cs
@@ -0,0 +1,8 @@
+global using System;
+global using System.Reactive;
+global using System.Reactive.Concurrency;
+global using System.Reactive.Disposables;
+global using System.Reactive.Linq;
+global using System.Reactive.Subjects;
+global using ReactiveUI;
+global using ReactiveUI.Reactive;
diff --git a/Stellar.Wpf/HotReloadService.cs b/Stellar.Wpf/HotReloadService.cs
new file mode 100644
index 0000000..40771f2
--- /dev/null
+++ b/Stellar.Wpf/HotReloadService.cs
@@ -0,0 +1,21 @@
+#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
+[assembly: System.Reflection.Metadata.MetadataUpdateHandler(typeof(Stellar.Wpf.HotReloadService))]
+
+namespace Stellar.Wpf;
+
+public static class HotReloadService
+{
+ public static bool HotReloadAware { get; set; }
+
+ public static event Action? UpdateApplicationEvent;
+
+ internal static void ClearCache(Type[]? types)
+ {
+ }
+
+ internal static void UpdateApplication(Type[]? types)
+ {
+ UpdateApplicationEvent?.Invoke(types);
+ }
+}
+#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
diff --git a/Stellar.Wpf/Stellar.Wpf.csproj b/Stellar.Wpf/Stellar.Wpf.csproj
new file mode 100644
index 0000000..19bfeb8
--- /dev/null
+++ b/Stellar.Wpf/Stellar.Wpf.csproj
@@ -0,0 +1,28 @@
+
+
+
+ net10.0-windows10.0.19041.0
+ true
+
+ true
+ true
+ enable
+ enable
+ StellarUI.Wpf
+ Stellar UI - WPF
+ Eight-Bot
+ Eight-Bot 2026
+ https://github.com/TheEightBot/StellarUI/blob/main/LICENSE
+ https://github.com/TheEightBot/StellarUI
+ WPF;MVVM;Reactive UI;Reactive Extensions;Eight-Bot
+
+
+
+
+
+
+
+
diff --git a/Stellar.Wpf/UserControlBase.cs b/Stellar.Wpf/UserControlBase.cs
new file mode 100644
index 0000000..17e825d
--- /dev/null
+++ b/Stellar.Wpf/UserControlBase.cs
@@ -0,0 +1,137 @@
+using System.ComponentModel;
+using System.Windows;
+using ReactiveUI.Reactive;
+
+namespace Stellar.Wpf;
+
+// ReactiveUserControl supplies the ViewModel dependency property and IViewFor plumbing;
+// Stellar layers its ViewManager lifecycle on top. WPF has no virtual for Loaded/Unloaded,
+// so the constructor subscribes to the routed events instead.
+public abstract class UserControlBase : ReactiveUserControl, IStellarView
+ where TViewModel : class
+{
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public ViewManager ViewManager { get; } = new WpfViewManager();
+
+ public IObservable UserControlInitialized => ViewManager.Initialized;
+
+ public IObservable Activated => ViewManager.Activated;
+
+ public IObservable Deactivated => ViewManager.Deactivated;
+
+ public IObservable Disposed => ViewManager.Disposed;
+
+ public IObservable LifecycleEvents => ViewManager.LifecycleEvents;
+
+ protected UserControlBase()
+ : this(manuallyInitialize: true)
+ {
+ }
+
+ protected UserControlBase(
+ TViewModel? viewModel = null,
+ bool maintain = false,
+ bool delayBindingRegistrationUntilAttached = false,
+ bool manuallyInitialize = true)
+ {
+ Loaded += HandleLoaded;
+ Unloaded += HandleUnloaded;
+
+ if (!manuallyInitialize)
+ {
+ this.InitializeStellarComponent(viewModel, maintain, delayBindingRegistrationUntilAttached);
+ }
+ }
+
+ public virtual void Initialize()
+ {
+ }
+
+ public abstract void SetupUserInterface();
+
+ public abstract void Bind(WeakCompositeDisposable disposables);
+
+ protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
+ {
+ ViewManager.PropertyChanged(this, e.Property.Name);
+
+ base.OnPropertyChanged(e);
+ }
+
+ private void HandleLoaded(object sender, RoutedEventArgs e)
+ {
+ ViewManager.HandleActivated(this);
+
+ ViewManager.OnLifecycle(this, LifecycleEvent.IsAppearing);
+ }
+
+ private void HandleUnloaded(object sender, RoutedEventArgs e)
+ {
+ ViewManager.OnLifecycle(this, LifecycleEvent.IsDisappearing);
+
+ ViewManager.HandleDeactivated(this);
+ }
+}
+
+public abstract class UserControlBase : ReactiveUserControl, IStellarView
+ where TViewModel : class
+{
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public ViewManager ViewManager { get; } = new WpfViewManager();
+
+ public IObservable UserControlInitialized => ViewManager.Initialized;
+
+ public IObservable Activated => ViewManager.Activated;
+
+ public IObservable Deactivated => ViewManager.Deactivated;
+
+ public IObservable Disposed => ViewManager.Disposed;
+
+ public IObservable LifecycleEvents => ViewManager.LifecycleEvents;
+
+ protected UserControlBase()
+ {
+ Loaded += HandleLoaded;
+ Unloaded += HandleUnloaded;
+ DataContextChanged += HandleDataContextChanged;
+ }
+
+ public virtual void Initialize()
+ {
+ }
+
+ public abstract void SetupUserInterface();
+
+ public abstract void Bind(WeakCompositeDisposable disposables);
+
+ protected abstract void MapDataModelToViewModel(TViewModel viewModel, TDataModel dataModel);
+
+ protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
+ {
+ ViewManager.PropertyChanged(this, e.Property.Name);
+
+ base.OnPropertyChanged(e);
+ }
+
+ private void HandleLoaded(object sender, RoutedEventArgs e)
+ {
+ ViewManager.HandleActivated(this);
+
+ ViewManager.OnLifecycle(this, LifecycleEvent.IsAppearing);
+ }
+
+ private void HandleUnloaded(object sender, RoutedEventArgs e)
+ {
+ ViewManager.OnLifecycle(this, LifecycleEvent.IsDisappearing);
+
+ ViewManager.HandleDeactivated(this);
+ }
+
+ private void HandleDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
+ {
+ if (ViewModel is not null && e.NewValue is TDataModel dataModel)
+ {
+ MapDataModelToViewModel(ViewModel, dataModel);
+ }
+ }
+}
diff --git a/Stellar.Wpf/WindowBase.cs b/Stellar.Wpf/WindowBase.cs
new file mode 100644
index 0000000..d830653
--- /dev/null
+++ b/Stellar.Wpf/WindowBase.cs
@@ -0,0 +1,83 @@
+using System.ComponentModel;
+using System.Windows;
+using ReactiveUI.Reactive;
+
+namespace Stellar.Wpf;
+
+// ReactiveWindow supplies the ViewModel dependency property and IViewFor plumbing;
+// Stellar layers its ViewManager lifecycle on top, mirroring Stellar.Avalonia's WindowBase.
+public abstract class WindowBase : ReactiveWindow, IStellarView
+ where TViewModel : class
+{
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public ViewManager ViewManager { get; } = new WpfViewManager();
+
+ public IObservable WindowInitialized => ViewManager.Initialized;
+
+ public IObservable WindowActivated => ViewManager.Activated;
+
+ public IObservable WindowDeactivated => ViewManager.Deactivated;
+
+ public IObservable IsAppearing => ViewManager.IsAppearing;
+
+ public IObservable IsDisappearing => ViewManager.IsDisappearing;
+
+ public IObservable Disposed => ViewManager.Disposed;
+
+ public IObservable LifecycleEvents => ViewManager.LifecycleEvents;
+
+ protected WindowBase()
+ : this(manuallyInitialize: true)
+ {
+ }
+
+ protected WindowBase(
+ TViewModel? viewModel = null,
+ bool maintain = false,
+ bool delayBindingRegistrationUntilAttached = false,
+ bool manuallyInitialize = true)
+ {
+ if (!manuallyInitialize)
+ {
+ this.InitializeStellarComponent(viewModel, maintain, delayBindingRegistrationUntilAttached);
+ }
+ }
+
+ public virtual void Initialize()
+ {
+ }
+
+ public abstract void SetupUserInterface();
+
+ public abstract void Bind(WeakCompositeDisposable disposables);
+
+ protected override void OnInitialized(EventArgs e)
+ {
+ base.OnInitialized(e);
+
+ ViewManager.HandleActivated(this);
+ }
+
+ protected override void OnContentRendered(EventArgs e)
+ {
+ base.OnContentRendered(e);
+
+ ViewManager.OnLifecycle(this, LifecycleEvent.IsAppearing);
+ }
+
+ protected override void OnClosing(CancelEventArgs e)
+ {
+ ViewManager.OnLifecycle(this, LifecycleEvent.IsDisappearing);
+
+ ViewManager.HandleDeactivated(this);
+
+ base.OnClosing(e);
+ }
+
+ protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
+ {
+ ViewManager.PropertyChanged(this, e.Property.Name);
+
+ base.OnPropertyChanged(e);
+ }
+}
diff --git a/Stellar.Wpf/WpfViewManager.cs b/Stellar.Wpf/WpfViewManager.cs
new file mode 100644
index 0000000..4748c39
--- /dev/null
+++ b/Stellar.Wpf/WpfViewManager.cs
@@ -0,0 +1,60 @@
+using System.Windows;
+
+namespace Stellar.Wpf;
+
+public class WpfViewManager : ViewManager
+ where TViewModel : class
+{
+ // Null whenever no view is activated, which HandleDeactivated relies on.
+ private IStellarView? _view;
+
+ public override void HandleActivated(IStellarView view)
+ {
+ base.HandleActivated(view);
+
+ if (HotReloadService.HotReloadAware)
+ {
+ _view = view;
+ HotReloadService.UpdateApplicationEvent -= HandleHotReload;
+ HotReloadService.UpdateApplicationEvent += HandleHotReload;
+ }
+ }
+
+ public override void HandleDeactivated(IStellarView view)
+ {
+ if (HotReloadService.HotReloadAware)
+ {
+ _view = null;
+ HotReloadService.UpdateApplicationEvent -= HandleHotReload;
+ }
+
+ base.HandleDeactivated(view);
+ }
+
+#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
+ private void HandleHotReload(Type[]? updatedTypes)
+#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
+ {
+ if (_view is null)
+ {
+ return;
+ }
+
+ Application.Current?.Dispatcher
+ .Invoke(
+ () =>
+ {
+ var maintainStatus = _view.ViewManager.Maintain;
+
+ _view.ViewManager.Maintain = false;
+
+ _view.ViewManager.UnregisterBindings(_view);
+
+ _view.ViewManager.Maintain = maintainStatus;
+
+ _view.SetupUserInterface();
+
+ _view.ViewManager.RegisterBindings(_view);
+ });
+ }
+}
diff --git a/Stellar.sln b/Stellar.sln
index 6ae7901..2035b2e 100644
--- a/Stellar.sln
+++ b/Stellar.sln
@@ -49,6 +49,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stellar.DiskDataCache", "St
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stellar.SourceGenerators", "Stellar.SourceGenerators\Stellar.SourceGenerators.csproj", "{037D57FD-AB7C-4F69-8841-1A655FC86145}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stellar.Wpf", "Stellar.Wpf\Stellar.Wpf.csproj", "{D3E861EB-BBD7-405F-AB85-04F928E39E38}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stellar.WinUI", "Stellar.WinUI\Stellar.WinUI.csproj", "{B51B97EC-F16B-4092-950D-260D71CF7A65}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stellar.Uno", "Stellar.Uno\Stellar.Uno.csproj", "{51D9C2E2-D338-45BF-B904-5C8B1DDF68FF}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -216,6 +222,42 @@ Global
{037D57FD-AB7C-4F69-8841-1A655FC86145}.Release|x64.Build.0 = Release|Any CPU
{037D57FD-AB7C-4F69-8841-1A655FC86145}.Release|x86.ActiveCfg = Release|Any CPU
{037D57FD-AB7C-4F69-8841-1A655FC86145}.Release|x86.Build.0 = Release|Any CPU
+ {D3E861EB-BBD7-405F-AB85-04F928E39E38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D3E861EB-BBD7-405F-AB85-04F928E39E38}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D3E861EB-BBD7-405F-AB85-04F928E39E38}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {D3E861EB-BBD7-405F-AB85-04F928E39E38}.Debug|x64.Build.0 = Debug|Any CPU
+ {D3E861EB-BBD7-405F-AB85-04F928E39E38}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D3E861EB-BBD7-405F-AB85-04F928E39E38}.Debug|x86.Build.0 = Debug|Any CPU
+ {D3E861EB-BBD7-405F-AB85-04F928E39E38}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D3E861EB-BBD7-405F-AB85-04F928E39E38}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D3E861EB-BBD7-405F-AB85-04F928E39E38}.Release|x64.ActiveCfg = Release|Any CPU
+ {D3E861EB-BBD7-405F-AB85-04F928E39E38}.Release|x64.Build.0 = Release|Any CPU
+ {D3E861EB-BBD7-405F-AB85-04F928E39E38}.Release|x86.ActiveCfg = Release|Any CPU
+ {D3E861EB-BBD7-405F-AB85-04F928E39E38}.Release|x86.Build.0 = Release|Any CPU
+ {B51B97EC-F16B-4092-950D-260D71CF7A65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B51B97EC-F16B-4092-950D-260D71CF7A65}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B51B97EC-F16B-4092-950D-260D71CF7A65}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {B51B97EC-F16B-4092-950D-260D71CF7A65}.Debug|x64.Build.0 = Debug|Any CPU
+ {B51B97EC-F16B-4092-950D-260D71CF7A65}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B51B97EC-F16B-4092-950D-260D71CF7A65}.Debug|x86.Build.0 = Debug|Any CPU
+ {B51B97EC-F16B-4092-950D-260D71CF7A65}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B51B97EC-F16B-4092-950D-260D71CF7A65}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B51B97EC-F16B-4092-950D-260D71CF7A65}.Release|x64.ActiveCfg = Release|Any CPU
+ {B51B97EC-F16B-4092-950D-260D71CF7A65}.Release|x64.Build.0 = Release|Any CPU
+ {B51B97EC-F16B-4092-950D-260D71CF7A65}.Release|x86.ActiveCfg = Release|Any CPU
+ {B51B97EC-F16B-4092-950D-260D71CF7A65}.Release|x86.Build.0 = Release|Any CPU
+ {51D9C2E2-D338-45BF-B904-5C8B1DDF68FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {51D9C2E2-D338-45BF-B904-5C8B1DDF68FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {51D9C2E2-D338-45BF-B904-5C8B1DDF68FF}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {51D9C2E2-D338-45BF-B904-5C8B1DDF68FF}.Debug|x64.Build.0 = Debug|Any CPU
+ {51D9C2E2-D338-45BF-B904-5C8B1DDF68FF}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {51D9C2E2-D338-45BF-B904-5C8B1DDF68FF}.Debug|x86.Build.0 = Debug|Any CPU
+ {51D9C2E2-D338-45BF-B904-5C8B1DDF68FF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {51D9C2E2-D338-45BF-B904-5C8B1DDF68FF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {51D9C2E2-D338-45BF-B904-5C8B1DDF68FF}.Release|x64.ActiveCfg = Release|Any CPU
+ {51D9C2E2-D338-45BF-B904-5C8B1DDF68FF}.Release|x64.Build.0 = Release|Any CPU
+ {51D9C2E2-D338-45BF-B904-5C8B1DDF68FF}.Release|x86.ActiveCfg = Release|Any CPU
+ {51D9C2E2-D338-45BF-B904-5C8B1DDF68FF}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Stellar.slnf b/Stellar.slnf
index 7ab2dc8..2417a5b 100644
--- a/Stellar.slnf
+++ b/Stellar.slnf
@@ -10,7 +10,10 @@
"Stellar.DiskDataCache\\Stellar.DiskDataCache.csproj",
"Stellar.FluentValidation\\Stellar.FluentValidation.csproj",
"Stellar.SourceGenerators\\Stellar.SourceGenerators.csproj",
- "Stellar.UnitTests\\Stellar.UnitTests.csproj"
+ "Stellar.UnitTests\\Stellar.UnitTests.csproj",
+ "Stellar.Uno\\Stellar.Uno.csproj",
+ "Stellar.WinUI\\Stellar.WinUI.csproj",
+ "Stellar.Wpf\\Stellar.Wpf.csproj"
]
}
}
diff --git a/Stellar/GlobalUsings.cs b/Stellar/GlobalUsings.cs
index b2683af..c41d875 100644
--- a/Stellar/GlobalUsings.cs
+++ b/Stellar/GlobalUsings.cs
@@ -6,3 +6,4 @@
global using System.Reactive.Linq;
global using ReactiveGenerator;
global using ReactiveUI;
+global using ReactiveUI.Reactive;
diff --git a/Stellar/Stellar.csproj b/Stellar/Stellar.csproj
index 7a47009..eda9faf 100644
--- a/Stellar/Stellar.csproj
+++ b/Stellar/Stellar.csproj
@@ -18,7 +18,8 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
+