This is the core MVVM Framework components used by Eight-Bot for building apps. A highly opinionated ReactiveUI on Rails.
$ dotnet add package StellarUI.DiskDataCacheA comprehensive cross-platform .NET application framework built on ReactiveUI for creating reactive MVVM applications.
StellarUI is a reactive UI framework that provides a structured and consistent approach to building applications across different platforms including MAUI, Blazor, and Avalonia. It implements the MVVM (Model-View-ViewModel) pattern and enhances it with reactive programming paradigms, making it easier to manage application state, handle UI events, and implement complex workflows.
The framework is designed to support multiple platforms while maintaining a consistent programming model, allowing developers to share business logic and UI patterns across different .NET UI frameworks.
ServiceRegistrationAttributeStellarUI is composed of several projects, each serving a specific purpose in the framework:
To use StellarUI in your project, add the relevant package references:
<!-- For MAUI applications -->
<PackageReference Include="Stellar" Version="latest" />
<PackageReference Include="Stellar.Maui" Version="latest" />
<!-- For Blazor applications -->
<PackageReference Include="Stellar" Version="latest" />
<PackageReference Include="Stellar.Blazor" Version="latest" />
<!-- For Avalonia applications -->
<PackageReference Include="Stellar" Version="latest" />
<PackageReference Include="Stellar.Avalonia" Version="latest" />
StellarUI simplifies application setup across all platforms with a focus on minimizing boilerplate code. Just add these two key method calls to your existing app setup:
// In your MauiProgram.cs
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
// Just add these two lines to enable StellarUI:
.UseStellarComponents<App>() // Register all StellarUI components automatically
.EnableHotReload(); // Enable hot reload capability (optional but recommended)
return builder.Build();
}
// In your Program.cs
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
// Just add this line to enable StellarUI:
builder.Services.UseStellarComponents<App>(); // Register all StellarUI components automatically
await builder.Build().RunAsync();
// In your Program.cs
public static AppBuilder BuildAvaloniaApp() =>
AppBuilder.Configure<App>()
.UsePlatformDetect()
// Just add these two lines to enable StellarUI:
.UseStellarComponents<App>() // Register all StellarUI components automatically
.EnableHotReload() // Enable hot reload capability (optional but recommended)
.UseReactiveUI();
That's it! The UseStellarComponents<App>() method handles all the necessary registration of services, views, and view models. The framework auto-discovers and registers all classes in your application that have the [ServiceRegistration] attribute.
StellarUI provides base classes for each type of view component across platforms:
ContentPageBase<TViewModel>: Base class for MAUI ContentPageGridBase<TViewModel>: Base class for MAUI GridStackLayoutBase<TViewModel>: Base class for MAUI StackLayoutContentViewBase<TViewModel>: Base class for MAUI ContentViewShellBase<TViewModel>: Base class for MAUI ShellPopupPageBase<TViewModel>: Base class for popup pagesComponentBase<TViewModel>: Base class for Blazor componentsLayoutComponentBase<TViewModel>: Base class for Blazor layoutsInjectableComponentBase<TViewModel>: Base class for components with DIWindowBase<TViewModel>: Base class for Avalonia windowsUserControlBase<TViewModel>: Base class for Avalonia user controlsAll views follow a consistent pattern for setup and binding:
[ServiceRegistration] // Optional: Register for dependency injection
public class SampleView : ContentViewBase<SampleViewModel>
{
private Label _label;
// Constructor - initialize with the view model
public SampleView(SampleViewModel viewModel)
{
this.InitializeStellarComponent(viewModel);
}
// Setup the UI elements
public override void SetupUserInterface()
{
Content = new Label()
.Assign(out _label);
}
// Bind the UI elements to the view model
public override void Bind(WeakCompositeDisposable disposables)
{
this.OneWayBind(ViewModel, vm => vm.Text, v => v._label.Text)
.DisposeWith(disposables);
// Add other bindings as needed
}
}
ViewModels in StellarUI inherit from ViewModelBase and use the [Reactive] attribute from ReactiveUI for reactive properties:
[ServiceRegistration] // Register for dependency injection
public partial class SampleViewModel : ViewModelBase
{
[Reactive]
public string Text { get; set; } = "Hello, World!";
// Initialize method for setup that happens at creation time
protected override void Initialize()
{
// Setup initial values, etc.
}
// Bind method for setting up reactive bindings
protected override void Bind(WeakCompositeDisposable disposables)
{
// Setup observables, commands, etc.
this.WhenAnyValue(x => x.Text)
.Subscribe(text => Console.WriteLine($"Text changed: {text}"))
.DisposeWith(disposables);
}
}
StellarUI provides a ServiceRegistrationAttribute to automatically register classes with the dependency injection system:
// Register as transient (default)
[ServiceRegistration]
public class TransientService : IService { }
// Register as singleton
[ServiceRegistration(Lifetime.Singleton)]
public class SingletonService : IService { }
// Register as scoped
[ServiceRegistration(Lifetime.Scoped)]
public class ScopedService : IService { }
Services can be injected into ViewModels through constructor injection:
[ServiceRegistration]
public partial class SampleViewModel : ViewModelBase
{
private readonly IService _service;
// Constructor injection
public SampleViewModel(IService service)
{
_service = service;
}
// Rest of the ViewModel...
}
StellarUI includes a Roslyn source generator that emits a strongly-typed registration extension based on your assembly, eliminating runtime reflection.
Add the generator package to your project via NuGet:
dotnet add package StellarUI.SourceGenerators --version <latest-version>
Rebuild your project to produce AddRegisteredServicesFor{YourAssemblyName}.g.cs in obj/<TFM>/generated.
In your startup code (e.g., MauiProgram.cs or Program.cs), call the generated extension:
using Stellar; // namespace of generated code
builder.Services.AddRegisteredServicesForMyApp(); // replace MyApp with your project's AssemblyName
All [ServiceRegistration] attributed types will be registered at compile time.
StellarUI provides a consistent set of lifecycle events across all platforms:
These events are exposed as IObservable<Unit> properties on all view base classes, allowing you to react to them with reactive extensions:
public class SampleView : ContentViewBase<SampleViewModel>
{
public SampleView(SampleViewModel viewModel)
{
this.InitializeStellarComponent(viewModel);
// Subscribe to lifecycle events
this.IsAppearing
.Subscribe(_ => Console.WriteLine("View is appearing"))
.DisposeWith(disposables);
this.IsDisappearing
.Subscribe(_ => Console.WriteLine("View is disappearing"))
.DisposeWith(disposables);
}
// Rest of the view...
}
ViewModels can implement the ILifecycleEventAware interface to be notified of lifecycle events:
[ServiceRegistration]
public partial class SampleViewModel : ViewModelBase, ILifecycleEventAware
{
public void OnLifecycleEvent(LifecycleEvent lifecycleEvent)
{
switch (lifecycleEvent)
{
case LifecycleEvent.IsAppearing:
// Handle appearing
break;
case LifecycleEvent.IsDisappearing:
// Handle disappearing
break;
// Handle other lifecycle events...
}
}
// Rest of the ViewModel...
}
StellarUI provides platform-agnostic navigation abstractions to simplify navigation across different platforms:
// Navigate to a page
Observable.Return(Unit.Default)
.NavigateToPage<SimpleSamplePage>(this)
.DisposeWith(disposables);
// Navigate to a page with parameters
Observable.Return(42) // The parameter value
.NavigateToPage<int, ParameterizedPage>(
this,
queryParameters: (value, dict) =>
{
dict.Add("ParameterValue", value);
})
.DisposeWith(disposables);
// Show popup
this.BindCommand(ViewModel, vm => vm.ShowPopupCommand, v => v._popupButton)
.DisposeWith(disposables);
// In ViewModel
_showPopupCommand = ReactiveCommand.CreateFromTask(async () =>
{
await PopupNavigation.Instance.PushAsync(
new SamplePopupPage(new SampleViewModel()));
});
// Navigate in a Blazor component
Navigation.NavigateTo("/details/42");
// In the target page class, receive the parameter
[Parameter]
public string Id { get; set; }
// Or with query parameters
[QueryParameter]
public int ParameterValue { get; set; }
// Navigate in an Avalonia application
var window = new DetailWindow(new DetailViewModel());
window.Show();
StellarUI integrates with FluentValidation for model validation across all platforms:
// Define a validator
public class UserValidator : AbstractValidator<UserViewModel>
{
public UserValidator()
{
RuleFor(x => x.Username).NotEmpty().MinimumLength(4);
RuleFor(x => x.Email).NotEmpty().EmailAddress();
RuleFor(x => x.Age).GreaterThan(0).LessThan(120);
}
}
// Use the validator in a view model
[ServiceRegistration]
public partial class UserViewModel : ViewModelBase, IProvideValidation
{
[Reactive]
public string Username { get; set; }
[Reactive]
public string Email { get; set; }
[Reactive]
public int Age { get; set; }
// Implementation of IProvideValidation
public IEnumerable<ValidationResult> Validate()
{
var validator = new UserValidator();
var results = validator.Validate(this);
return results.Errors
.Select(x => new ValidationResult(x.PropertyName, x.ErrorMessage));
}
// Track validity in the view model
[Reactive]
public bool IsValid { get; private set; }
protected override void Bind(CompositeDisposable disposables)
{
this.WhenAnyValue(x => x.Username, x => x.Email, x => x.Age)
.Throttle(TimeSpan.FromMilliseconds(300))
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(_ =>
{
IsValid = !Validate().Any();
})
.DisposeWith(disposables);
}
}
The Stellar.DiskDataCache package provides disk-based caching capabilities:
// Register the cache in your DI setup
services.AddSingleton<IDataCache, DiskCache>();
// Use the cache in a view model
[ServiceRegistration]
public partial class CachedDataViewModel : ViewModelBase
{
private readonly IDataCache _cache;
public CachedDataViewModel(IDataCache cache)
{
_cache = cache;
}
[Reactive]
public ObservableCollection<string> Items { get; private set; }
protected override async void Initialize()
{
// Try to load from cache first
var cachedItems = await _cache.GetAsync<List<string>>("items");
if (cachedItems != null)
{
Items = new ObservableCollection<string>(cachedItems);
}
else
{
Items = new ObservableCollection<string>();
}
}
public async Task SaveItems()
{
await _cache.SetAsync("items", Items.ToList());
}
}
StellarUI has built-in support for hot reload during development:
// Enable hot reload in a MAUI app
builder.EnableHotReload();
// Enable hot reload in an Avalonia app
AppBuilder.Configure<App>()
// ... other configuration ...
.EnableHotReload()
.UseReactiveUI();
Contributions to StellarUI are welcome! Here's how you can contribute:
git checkout -b feature/my-new-featuregit commit -am 'Add some feature'git push origin feature/my-new-featureStellar.sln in Visual Studio or your preferred IDEStellarUI is licensed under the MIT License. See the LICENSE file for details.