ReactiveUI MAUI Popups: Reactive Views Made Easy
Welcome to the official documentation for ReactiveUI.Maui.Plugins.Popup! This package is your key to unlocking a more streamlined and reactive way to handle popups in your .NET MAUI applications. By integrating seamlessly with the powerful Mopups library, it allows you to leverage the Model-View-ViewModel (MVVM) pattern and the principles of Reactive Programming to build sophisticated, composable, and maintainable UI components. If you're looking to enhance your MAUI app with elegant popup experiences that are easy to manage and test, you've come to the right place. We'll guide you through the setup, provide practical examples, and dive into the core concepts that make this plugin a game-changer for your development workflow.
Packages
Getting started is as simple as installing the package into your MAUI project. This single dependency brings the power of ReactiveUI's observable patterns to your popup management.
Recommended Setup
To ensure that ReactiveUI.Maui.Plugins.Popup works seamlessly within your .NET MAUI application, you must initialize it during your application's startup. This is typically done in your MauiProgram.cs file. The ConfigureReactiveUIPopup() extension method does the heavy lifting, registering all the necessary services and wiring up the ReactiveUI integrations with the Mopups library. This crucial step ensures that all reactive behaviors and navigation extensions are available throughout your application.
Here’s how you integrate it into your MauiProgram.cs:
using Microsoft.Maui.Hosting;
using ReactiveUI.Maui.Plugins.Popup;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
})
// Initialize ReactiveUI Popups
.ConfigureReactiveUIPopup();
return builder.Build();
}
}
By adding .ConfigureReactiveUIPopup() to your builder pipeline, you're setting the stage for a more robust and reactive approach to managing dialogs and popups in your MAUI application. This initialization is fundamental to utilizing the ReactivePopupPage and its associated navigation helpers effectively.
Quick Example: Your First Reactive Popup
Let's walk through creating a simple confirmation popup. This example demonstrates how to define a ViewModel, create a corresponding popup view, and bind them together using ReactiveUI's powerful features. This process highlights the core MVVM pattern enhanced with reactivity, making your UI logic cleaner and more manageable.
1. Create a ViewModel
First, we define a ViewModel that will manage the state and actions for our confirmation popup. By inheriting from ReactiveObject, we gain access to properties that automatically notify observers of changes and ReactiveCommand for handling user actions in a reactive manner. This ViewModel will expose commands for confirming and canceling the action, which will be bound to buttons in our popup view.
using System.Reactive;
using ReactiveUI;
public class ConfirmViewModel : ReactiveObject
{
public ReactiveCommand<Unit, Unit> ConfirmCommand { get; }
public ReactiveCommand<Unit, Unit> CancelCommand { get; }
public ConfirmViewModel()
{
// Create a command that can be executed when the user confirms.
// It returns Unit, indicating no specific result is passed back.
ConfirmCommand = ReactiveCommand.Create(() =>
{
// TODO: Implement your confirmation logic here.
// This could involve saving data, proceeding with an action, etc.
System.Diagnostics.Debug.WriteLine("Confirmation action executed.");
});
// Create a command to handle the cancellation action.
// Similar to ConfirmCommand, it returns Unit.
CancelCommand = ReactiveCommand.Create(() =>
{
// TODO: Implement your cancellation logic here.
// This might involve dismissing a form or returning to a previous state.
System.Diagnostics.Debug.WriteLine("Cancel action executed.");
});
}
}
In this ViewModel, ConfirmCommand and CancelCommand are the primary interaction points. They are created using ReactiveCommand.Create, which is the standard way to define commands that can be executed. The Unit type parameter signifies that these commands don't take any input and don't return any specific output value beyond the completion of the command execution. This setup is ideal for simple actions like confirming or canceling a dialog.
2. Create the Popup View
Next, we create the visual representation of our popup. In MAUI, this is typically done using XAML. For our reactive popup, the root element must be ReactivePopupPage or its generic counterpart, ReactivePopupPage<TViewModel>. This allows the popup to participate in ReactiveUI's view-model binding system. We'll define the UI elements and then use the WhenActivated block in the code-behind to establish the bindings between the UI controls and the ViewModel's commands.
XAML (ConfirmPopup.xaml)
<?xml version="1.0" encoding="utf-8" ?>
<rxui:ReactivePopupPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:rxui="clr-namespace:ReactiveUI.Maui.Plugins.Popup;assembly=ReactiveUI.Maui.Plugins.Popup"
xmlns:vm="clr-namespace:MyApp.ViewModels;assembly=MyApp"
x:TypeArguments="vm:ConfirmViewModel"
x:Class="MyApp.Views.ConfirmPopup">
<VerticalStackLayout Spacing="10" VerticalOptions="Center" HorizontalOptions="Center" BackgroundColor="White" Padding="20">
<Label Text="Are you sure?" FontSize="18" HorizontalOptions="Center" />
<HorizontalStackLayout Spacing="10" HorizontalOptions="Center">
<Button x:Name="YesButton" Text="Yes" />
<Button x:Name="NoButton" Text="No" />
</HorizontalStackLayout>
</VerticalStackLayout>
</rxui:ReactivePopupPage>
Code-behind (ConfirmPopup.xaml.cs)
using ReactiveUI;
using ReactiveUI.Maui.Plugins.Popup;
using System.Reactive.Disposables;
// Specify the ViewModel type for strong typing
public partial class ConfirmPopup : ReactivePopupPage<ConfirmViewModel>
{
public ConfirmPopup()
{
InitializeComponent();
// Assign the ViewModel instance. In a real app, you might use a DI container.
ViewModel = new ConfirmViewModel();
// The WhenActivated block is where you set up your bindings.
// It ensures bindings are active only when the page is active.
this.WhenActivated(disposables =>
{
// Bind the 'ConfirmCommand' from the ViewModel to the 'YesButton'.
// When YesButton is clicked, ConfirmCommand will be executed.
this.BindCommand(ViewModel, vm => vm.ConfirmCommand, v => v.YesButton)
.DisposeWith(disposables);
// Bind the 'CancelCommand' from the ViewModel to the 'NoButton'.
// When NoButton is clicked, CancelCommand will be executed.
this.BindCommand(ViewModel, vm => vm.CancelCommand, v => v.NoButton)
.DisposeWith(disposables);
// Subscribe to the BackgroundClick observable.
// When the background is tapped, we close the popup.
this.BackgroundClick
.Subscribe(_ => Navigation.PopPopup())
.DisposeWith(disposables);
});
}
}
In the XAML, we declare ConfirmPopup as a ReactivePopupPage and specify its x:TypeArguments as vm:ConfirmViewModel. This establishes the link between the view and its ViewModel. In the code-behind, ViewModel = new ConfirmViewModel(); instantiates our ViewModel. The WhenActivated block is the heart of our reactive setup. this.BindCommand is a convenient ReactiveUI extension that links a UI control's default action (like a button click) to a ReactiveCommand in the ViewModel. this.BackgroundClick is an observable provided by ReactiveUI.Maui.Plugins.Popup that emits a value when the user taps the semi-transparent overlay behind the popup, allowing us to easily dismiss it. Each binding and subscription is added to disposables to ensure proper cleanup when the view is disposed.
3. Navigate to the Popup
Finally, you need a way to present this popup to the user. You can trigger the popup from anywhere within your application's navigation stack, such as a standard MAUI ContentPage or directly from another ViewModel. The ReactiveUI.Maui.Plugins.Popup package provides convenient extension methods on Microsoft.Maui.Controls.INavigation for this purpose.
From a View (code-behind):
If you need to show the popup directly from your page's code-behind (though often done from a ViewModel), you can use the Navigation.PushPopup method:
// Assuming 'this' is a MAUI Page with access to Navigation
await Navigation.PushPopup(new ConfirmPopup());
This is a straightforward way to present the popup modally. The await keyword indicates that execution will pause until the popup is dismissed.
Using an Observable pipeline in a ViewModel:
A more idiomatic ReactiveUI approach is to trigger the popup navigation from a command within your ViewModel. This keeps your UI logic declarative and testable. For instance, if you had a ShowConfirmationDialogCommand in another ViewModel, you could chain it to the popup navigation:
// In your ViewModel
public ReactiveCommand<Unit, Unit> ShowConfirmationDialogCommand { get; }
// ... constructor ...
ShowConfirmationDialogCommand = ReactiveCommand.CreateFromObservable(() =>
Navigation.PushPopup(new ConfirmPopup())
);
// Or, if you need to react to the result of the popup (e.g., if ConfirmCommand returned a value)
// ConfirmCommand
// .SelectMany(_ => Navigation.PushPopup(new ConfirmPopup()))
// .Subscribe(popupResult => { /* Handle popup result */ });
This pattern leverages ReactiveUI's CreateFromObservable factory method for ReactiveCommand. It allows the command's execution to be driven by an observable sequence, in this case, the result of Navigation.PushPopup. This makes the flow reactive and easy to compose with other observable operations. The Navigation.PushPopup method itself returns an IObservable<Unit> that completes when the popup is pushed, making it perfect for use within SelectMany or CreateFromObservable.
Understanding Hot and Cold Observables in ReactiveUI
ReactiveUI heavily relies on the concept of Observables, which are fundamental building blocks for asynchronous and event-driven programming. Understanding the difference between hot and cold observables is crucial for writing efficient, predictable, and bug-free reactive code, especially when dealing with UI interactions and data streams.
Cold Observables
Think of a cold observable like a DVD or a song you download. It doesn't start doing anything until you explicitly subscribe to it. Each subscriber gets its own, independent execution of the observable's sequence. If multiple subscribers subscribe, the work is duplicated for each one. This is ideal for operations that should start fresh for every consumer, like making a network request or reading a file.
Characteristics of Cold Observables:
- Lazy Execution: Work begins only upon subscription.
- Independent Instances: Each subscription triggers a new, separate execution.
- Resource Duplication: Can be inefficient if the same operation needs to be performed for multiple subscribers (e.g., fetching the same data).
Example: Consider an observable that fetches data from a web API:
IObservable<MyData> FetchDataObservable()
{
// This code only runs when someone subscribes.
Console.WriteLine("Starting data fetch...");
return Observable.Create<MyData>(observer =>
{
// Simulate network request
// ... fetch data ...
var data = new MyData { /* ... */ };
observer.OnNext(data);
observer.OnCompleted();
// Return a disposable for cleanup if needed
return Disposable.Empty;
});
}
// Subscriber 1
var subscription1 = FetchDataObservable().Subscribe(data => Console.WriteLine("Subscriber 1 got data."));
// Subscriber 2
var subscription2 = FetchDataObservable().Subscribe(data => Console.WriteLine("Subscriber 2 got data."));
When Subscriber 1 subscribes, the FetchDataObservable executes, prints "Starting data fetch...", and provides data. When Subscriber 2 subscribes later, the entire process repeats – it prints "Starting data fetch..." again and fetches the data independently. This is the essence of a cold observable.
Hot Observables
A hot observable is like a live TV broadcast or a radio station. It's already running, and anyone who tunes in (subscribes) starts receiving the events from that point forward. Hot observables represent events that are happening independently of any specific subscriber. They are useful for representing user interface events (like button clicks, mouse movements), sensor readings, or any stream of data that's generated continuously.
Characteristics of Hot Observables:
- Eager Execution: The observable starts producing values as soon as it's created, regardless of subscribers.
- Shared Execution: All subscribers receive the same sequence of values produced by the single, shared execution.
- Event Broadcasting: Acts like a publisher of events to multiple interested parties.
Example: UI events like button clicks are naturally hot observables:
// Assuming 'MyButton' is a MAUI Button control
// WhenMyButtonIsClicked is a hot observable - it emits whenever the button is clicked,
// regardless of how many observers are listening.
var clicks = MyButton.Events().Clicked
.Select(_ => "Button was clicked!");
// Subscriber 1
var subscription1 = clicks.Subscribe(message => Console.WriteLine({{content}}quot;Hot stream 1: {message}"));
// Subscriber 2 (subscribes later)
await Task.Delay(1000); // Wait a bit
var subscription2 = clicks.Subscribe(message => Console.WriteLine({{content}}quot;Hot stream 2: {message}"));
// When the button is clicked (e.g., by the user):
// Both Subscriber 1 and Subscriber 2 will receive the message.
In this scenario, MyButton.Events().Clicked is a hot observable. The underlying event mechanism fires whenever the button is clicked. Both subscription1 and subscription2 will receive the message if they are subscribed when the click occurs. If Subscriber 2 subscribes after a click has already happened, they will only start receiving events from the moment they subscribed onwards; they won't receive the past click event.
Converting Between Hot and Cold
Sometimes, you might have a cold observable that you want to share among multiple subscribers without duplicating work, or you might want to treat a hot observable as cold for testing purposes. Operators like Share() and Publish() are used to convert cold observables into hot ones, allowing multiple subscribers to share a single underlying subscription.
Publish().Connect(): This is the most fundamental way to share a cold observable.Publish()turns a cold observable into a connectable observable, andConnect()starts the underlying subscription. All subsequent subscribers will share this single subscription.Share(): A simpler operator that combinesPublish().Connect(). It shares the source observable and automatically connects when the first subscriber appears and disconnects when the last subscriber leaves.
var coldObservable = FetchDataObservable();
// Convert to a hot observable using Share()
var sharedObservable = coldObservable.Share();
// Now, both subscribers will share the *same* execution of FetchDataObservable()
var subscription1 = sharedObservable.Subscribe(...);
var subscription2 = sharedObservable.Subscribe(...);
Understanding this distinction is vital for managing resources, preventing race conditions, and ensuring your reactive streams behave as expected. ReactiveUI.Maui.Plugins.Popup leverages these patterns, particularly for events like BackgroundClick (a hot observable) and navigation actions (which can be represented as observables).
API Reference
This section provides a detailed overview of the components and extensions offered by ReactiveUI.Maui.Plugins.Popup.
Setup
ConfigureReactiveUIPopup(): This is an extension method forIMauiAppBuilder. Call it in yourMauiProgram.csto register the necessary services for Mopups and ReactiveUI integration. It ensures that popup navigation and reactive bindings function correctly.
Controls
ReactivePopupPage: The base class for creating popup views. It implementsIViewFor, making it compatible with ReactiveUI's binding system. Use this for non-generic view models or when type safety isn't strictly required at the base level.ReactivePopupPage<TViewModel>: The generic base class for popup views. By specifyingTViewModel, you get a strongly-typedViewModelproperty, enhancing compile-time safety and improving developer experience. This is the recommended base class for most popup implementations.
Navigation Extensions
These extension methods are added to Microsoft.Maui.Controls.INavigation to provide a reactive and observable interface for managing popups. They simplify the process of showing and hiding popups within your navigation flow.
PushPopup<T>(T page, bool animate = true): Pushes a specified popuppageonto the navigation stack. Theanimateparameter controls whether the popup appears with an animation. This method returns anIObservable<Unit>that completes when the popup has been successfully pushed.PopPopup(bool animate = true): Removes the top-most popup from the navigation stack. Theanimateparameter controls the closing animation. This returns anIObservable<Unit>that completes once the popup is removed.PopAllPopup(bool animate = true): Removes all popups currently on the navigation stack. Useful for clearing all overlays or dialogs at once. Supports animation control and returns anIObservable<Unit>upon completion.RemovePopupPage(PopupPage page, bool animate = true): Removes a specificPopupPageinstance from the stack. This is useful if you need to remove a popup that isn't necessarily the topmost one. Animation is optional.
Observables
ReactivePopupPage exposes several observable properties and methods that allow you to react to various lifecycle events and user interactions related to popups:
BackgroundClick: AnIObservable<Unit>exposed onReactivePopupPage. It emits a value whenever the user taps the semi-transparent background overlay that typically surrounds a popup. This is perfect for implementing dismissible popups.PoppingObservable(): Returns anIObservable<Unit>that emits just before a popup is about to be closed (popped). You can use this for cleanup logic specific to the closing action.PoppedObservable(): Returns anIObservable<Unit>that emits immediately after a popup has been successfully closed. Useful for actions that should occur after the popup is gone.PushingObservable(): Returns anIObservable<Unit>that emits just before a popup is about to be displayed (pushed). Useful for pre-show setup.PushedObservable(): Returns anIObservable<Unit>that emits immediately after a popup has been successfully displayed. Useful for actions that need to happen once the popup is visible.
Sponsorship
The development and maintenance of ReactiveUI.Maui.Plugins.Popup, along with the broader ReactiveUI ecosystem, are driven by dedicated core team members and a passionate community of contributors. This work is undertaken during their personal time. If your professional work benefits from ReactiveUI and its associated libraries, especially in critical applications, please consider supporting our efforts through sponsorship. Your contributions help us dedicate more time to developing, maintaining, and improving these powerful tools, ultimately increasing your own development speed and application quality.
[Become a sponsor](https://github.com/sponsors/reactivemarbles).
Donations are utilized in several key ways:
- Enabling Core Team Development: Funding allows core team members to allocate more focused time to working on ReactiveUI and its associated projects.
- Recognizing Significant Contributions: We can offer support or recognition to contributors who invest substantial effort in improving the libraries.
- Supporting Ecosystem Projects: Contributions also help sustain the health and growth of projects within the broader ReactiveUI ecosystem.
Maintainers
This project thrives thanks to the collective efforts of the ReactiveUI community and its contributors. We are a group of developers passionate about building robust, reactive applications, and we welcome contributions from anyone interested in improving the libraries.
For more information on contributing or to see who is involved, please check out the main ReactiveUI repositories and community channels.
For further details on ReactiveUI and its related projects, consider exploring the ReactiveUI official website. To learn more about the underlying Mopups library, you can visit its GitHub repository.