ObservableEvents¶
ReactiveMarbles.ObservableEvents is a source generator that converts .NET events into observable sequences, making them easier to compose, filter, and combine with other reactive operations.
Overview¶
Instead of manually subscribing to events and managing event handlers, ObservableEvents automatically generates extension methods that convert events to IObservable<T> sequences. This allows you to use the full power of Rx operators with any .NET event.
Installation¶
Install the package via NuGet:
dotnet add package ReactiveMarbles.ObservableEvents.SourceGenerator
Or via Package Manager:
<PackageReference Include="ReactiveMarbles.ObservableEvents.SourceGenerator" Version="*" />
How It Works¶
ObservableEvents is a source generator that runs at compile-time. It analyzes your project's referenced assemblies and generates an Events() extension method that returns a strongly-typed aggregator object with one IObservable<T> property per public event on the source type.
Generated Code Pattern¶
For a type with an event like:
public event EventHandler<MouseEventArgs> MouseMove;
the generator produces an Events() aggregator roughly equivalent to:
public static class YourClassEventsExtensions
{
public static YourClassEvents Events(this YourClass @this) => new(@this);
}
public sealed class YourClassEvents
{
public YourClassEvents(YourClass source) => _source = source;
private readonly YourClass _source;
public IObservable<EventPattern<MouseEventArgs>> MouseMove =>
Observable.FromEventPattern<MouseEventArgs>(
h => _source.MouseMove += h,
h => _source.MouseMove -= h);
// ...one property per public event on YourClass
}
At the call site you therefore write obj.Events().MouseMove, not obj.MouseMoveEvent().
Basic Usage¶
Converting Events to Observables¶
using ReactiveMarbles.ObservableEvents;
public partial class MainViewModel : ReactiveObject
{
public MainViewModel()
{
// Subscribe to button click events
var button = new Button();
button.Events().Click
.Subscribe(_ => Console.WriteLine("Button clicked!"));
}
}
With Reactive Extensions Operators¶
The real power comes from composing with Rx operators:
// Throttle rapid clicks
button.Events().Click
.Throttle(TimeSpan.FromMilliseconds(500))
.Subscribe(_ => PerformAction());
// Take only the first 3 clicks
button.Events().Click
.Take(3)
.Subscribe(_ => Console.WriteLine("Click counted"));
// Filter based on event args
textBox.Events().TextChanged
.Select(e => e.Sender as TextBox)
.Where(tb => tb?.Text.Length > 3)
.Subscribe(tb => ValidateInput(tb.Text));
Platform-Specific Examples¶
WPF¶
using ReactiveMarbles.ObservableEvents;
public partial class MainWindow : ReactiveWindow<MainViewModel>
{
public MainWindow()
{
InitializeComponent();
this.WhenActivated(disposables =>
{
// Window events
this.Events().Loaded
.Subscribe(_ => Console.WriteLine("Window loaded"))
.DisposeWith(disposables);
this.Events().Closing
.Subscribe(e =>
{
var result = MessageBox.Show("Are you sure?", "Confirm", MessageBoxButton.YesNo);
e.EventArgs.Cancel = result == MessageBoxResult.No;
})
.DisposeWith(disposables);
// Mouse events with throttling
SearchTextBox.Events().KeyUp
.Throttle(TimeSpan.FromMilliseconds(300))
.Select(e => (e.Sender as TextBox)?.Text)
.Where(text => !string.IsNullOrWhiteSpace(text))
.Subscribe(text => ViewModel.Search(text))
.DisposeWith(disposables);
});
}
}
MAUI¶
using ReactiveMarbles.ObservableEvents;
public partial class MainPage : ReactiveContentPage<MainViewModel>
{
public MainPage()
{
InitializeComponent();
this.WhenActivated(disposables =>
{
// Entry text changes
SearchEntry.Events().TextChanged
.Throttle(TimeSpan.FromMilliseconds(500))
.Select(e => e.EventArgs.NewTextValue)
.Subscribe(text => ViewModel.SearchText = text)
.DisposeWith(disposables);
// Button taps with debounce
SubmitButton.Events().Clicked
.Throttle(TimeSpan.FromSeconds(1))
.Subscribe(_ => ViewModel.SubmitCommand.Execute().Subscribe())
.DisposeWith(disposables);
});
}
}
Avalonia¶
using ReactiveMarbles.ObservableEvents;
public partial class MainWindow : ReactiveWindow<MainViewModel>
{
public MainWindow()
{
InitializeComponent();
this.WhenActivated(disposables =>
{
// Pointer events
this.Events().PointerPressed
.Subscribe(e => Console.WriteLine($"Clicked at: {e.EventArgs.GetPosition(this)}"))
.DisposeWith(disposables);
// Text input with validation
UsernameTextBox.Events().TextChanged
.Select(e => e.EventArgs.Text)
.DistinctUntilChanged()
.Subscribe(text => ViewModel.Username = text)
.DisposeWith(disposables);
});
}
}
Blazor¶
using ReactiveMarbles.ObservableEvents;
public partial class CounterComponent : ReactiveComponentBase<CounterViewModel>
{
protected override void OnInitialized()
{
ViewModel = new CounterViewModel();
this.WhenActivated(disposables =>
{
// For Blazor, you typically use @onclick in markup
// But you can still use ObservableEvents for custom components
// Example with a custom component that has events
CustomComponent.Events().ValueChanged
.Subscribe(e => ViewModel.Value = e.EventArgs.NewValue)
.DisposeWith(disposables);
});
base.OnInitialized();
}
}
Advanced Patterns¶
Combining Multiple Events¶
// Wait for both events before proceeding
var loaded = window.Events().Loaded;
var dataReady = dataService.Events().DataLoaded;
loaded.Zip(dataReady, (l, d) => Unit.Default)
.Subscribe(_ => InitializeUI());
Event Sequences with State¶
// Track double-clicks
button.Events().Click
.Buffer(TimeSpan.FromMilliseconds(300))
.Where(clicks => clicks.Count == 2)
.Subscribe(_ => HandleDoubleClick());
Mouse Drag Pattern¶
var mouseDown = canvas.Events().MouseDown;
var mouseMove = canvas.Events().MouseMove;
var mouseUp = canvas.Events().MouseUp;
var drag = from down in mouseDown
from move in mouseMove.TakeUntil(mouseUp)
select move.EventArgs.GetPosition(canvas);
drag.Subscribe(pos => UpdateDragPosition(pos));
Keyboard Shortcuts¶
this.Events().KeyDown
.Where(e => e.EventArgs.Key == Key.S &&
(Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
.Subscribe(_ => SaveDocument());
Integration with ReactiveUI Bindings¶
ObservableEvents works seamlessly with ReactiveUI's binding system:
public partial class MainWindow : ReactiveWindow<MainViewModel>
{
public MainWindow()
{
InitializeComponent();
this.WhenActivated(disposables =>
{
// Convert event to property
SearchBox.Events().TextChanged
.Select(e => (e.Sender as TextBox)?.Text)
.BindTo(this, x => x.ViewModel.SearchText)
.DisposeWith(disposables);
// Use event with command
SaveButton.Events().Click
.InvokeCommand(ViewModel.SaveCommand)
.DisposeWith(disposables);
});
}
}
Performance Considerations¶
- 1. Memory Management: Always use
DisposeWith(disposables)insideWhenActivatedto prevent memory leaks - 2. Throttling: Use
ThrottleorDebouncefor high-frequency events like mouse moves or text changes - 3. Unsubscription: The generator handles proper event unsubscription when you dispose the observable
Common Patterns¶
Form Validation¶
var usernameValid = UsernameTextBox.Events().TextChanged
.Select(e => e.EventArgs.Text)
.Select(text => !string.IsNullOrWhiteSpace(text) && text.Length >= 3);
var passwordValid = PasswordTextBox.Events().TextChanged
.Select(e => e.EventArgs.Text)
.Select(text => text?.Length >= 6);
usernameValid.CombineLatest(passwordValid, (u, p) => u && p)
.Subscribe(valid => SubmitButton.IsEnabled = valid);
Search with Cancellation¶
SearchBox.Events().TextChanged
.Throttle(TimeSpan.FromMilliseconds(500))
.Select(e => (e.Sender as TextBox)?.Text)
.DistinctUntilChanged()
.SelectMany(async text => await SearchAsync(text))
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(results => DisplayResults(results));
Focus Management¶
// Auto-focus next field
FirstNameTextBox.Events().KeyDown
.Where(e => e.EventArgs.Key == Key.Enter)
.Subscribe(_ => LastNameTextBox.Focus());
Configuration¶
ObservableEvents can be configured via MSBuild properties in your project file:
<PropertyGroup>
<!-- Generate events for specific assemblies only -->
<ObservableEventsAssemblies>MyCustomControls</ObservableEventsAssemblies>
<!-- Exclude specific types -->
<ObservableEventsExclude>ObsoleteControl</ObservableEventsExclude>
</PropertyGroup>
Troubleshooting¶
Events Not Generated¶
If you don't see generated events:
- 1. Rebuild the project - Source generators run during compilation
- 2. Check the assembly is referenced - ObservableEvents only generates for referenced assemblies
- 3. Verify event visibility - Only public events are generated
- 4. Update packages - Ensure you have the latest version
IDE Support¶
- Visual Studio 2022+: Full support with IntelliSense
- Visual Studio Code: Install C# Dev Kit for full support
- Rider: Supported in recent versions
Viewing Generated Code¶
To see generated code:
- 1. In Solution Explorer, expand Dependencies ? Analyzers ? ReactiveMarbles.ObservableEvents.SourceGenerator
- 2. Look for generated files under the generator node
Comparison with Manual Event Handling¶
Traditional Approach¶
public MainWindow()
{
InitializeComponent();
button.Click += Button_Click;
textBox.TextChanged += TextBox_TextChanged;
}
private void Button_Click(object sender, EventArgs e)
{
// Handle click
}
private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
// Handle text change
}
protected override void OnClosed(EventArgs e)
{
// Must manually unsubscribe
button.Click -= Button_Click;
textBox.TextChanged -= TextBox_TextChanged;
base.OnClosed(e);
}
ObservableEvents Approach¶
public MainWindow()
{
InitializeComponent();
this.WhenActivated(disposables =>
{
button.Events().Click
.Subscribe(_ => HandleClick())
.DisposeWith(disposables);
textBox.Events().TextChanged
.Throttle(TimeSpan.FromMilliseconds(300))
.Subscribe(e => HandleTextChange(e.EventArgs))
.DisposeWith(disposables);
// Automatic cleanup when disposed
});
}
Best Practices¶
- 1. Always Dispose: Use
WhenActivatedandDisposeWithto ensure proper cleanup - 2. Throttle High-Frequency Events: Use
ThrottleorDebouncefor events that fire frequently - 3. Use Strong Typing: Leverage the typed
EventPattern<T>for better IntelliSense - 4. Combine with Commands: Use
InvokeCommandto integrate with ReactiveCommand - 5. Handle Errors: Use
CatchorOnErrorResumeNextfor resilient event handling
Additional Resources¶
- ObservableEvents GitHub Repository
- ReactiveUI Documentation
- Reactive Extensions Documentation
- Sample Applications