This repository was archived by the owner on Nov 3, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 3, 2026. It is now read-only.
[Bug]: BindCommandUnsafe does not dispose the displaced binding when it rebinds #163
Copy link
Copy link
Closed
Copy link
Labels
bugSomething isn't workingSomething isn't working
Description
Prerequisites
- I have searched this repository's open and closed issues for duplicates.
- I am using the latest stable release.
What happens
RuntimeCommandBindingFallback.BindCommand rebinds on every emission of the control chain and assigns the new binding to a MutableDisposable, which does not dispose the previous one. A link that raises a change notification re-emits the same control, so each notification adds a binding: the command executes once per binding, and the extra bindings stay attached after the returned disposable is disposed.
What you expected instead
One active binding per BindCommandUnsafe call. Disposing it detaches the command.
Minimal reproduction
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
using ReactiveUI.Binding.Reactive;
using ReactiveUI.Binding.Reactive.Builder;
ClickBinder binder = new();
RxBindingBuilder.CreateReactiveUIBindingBuilder().WithCommandBinder(binder).WithCoreServices().BuildApp();
ViewModel vm = new();
View view = new(vm);
IDisposable binding = view.BindCommandUnsafe(vm, x => x.Command, x => x.Editor.Properties.Button, "Click");
// The editor reports that its Properties changed (the same instance, e.g. one of its settings changed).
view.Editor.RaisePropertiesChanged();
view.Editor.RaisePropertiesChanged();
view.Editor.Properties.Button.PerformClick();
Console.WriteLine($"executions per click: {vm.Executions}, active bindings: {binder.Active}"); // 3, 3 - expected 1, 1
binding.Dispose();
vm.Executions = 0;
view.Editor.Properties.Button.PerformClick();
Console.WriteLine($"after dispose: executions per click: {vm.Executions}, active bindings: {binder.Active}"); // 2, 2 - expected 0, 0
class Npc : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected void Raise([CallerMemberName] string? name = null) => this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
sealed class Button
{
public event EventHandler? Click;
public void PerformClick() => this.Click?.Invoke(this, EventArgs.Empty);
}
sealed class Properties { public Button Button { get; } = new(); } // no notifications
sealed class Editor : Npc
{
public Properties Properties { get; } = new();
public void RaisePropertiesChanged() => this.Raise(nameof(this.Properties));
}
sealed class ViewModel : Npc
{
public int Executions { get; set; }
public ICommand Command { get; }
public ViewModel() => this.Command = new Command(() => this.Executions++);
}
sealed class Command(Action execute) : ICommand
{
public event EventHandler? CanExecuteChanged { add { } remove { } }
public bool CanExecute(object? parameter) => true;
public void Execute(object? parameter) => execute();
}
sealed class View(ViewModel viewModel) : IViewFor<ViewModel>
{
public ViewModel? ViewModel { get; set; } = viewModel;
object? IViewFor.ViewModel { get => this.ViewModel; set => this.ViewModel = (ViewModel?)value; }
public Editor Editor { get; } = new();
}
// Binds the command to the given event of a Button.
sealed class ClickBinder : ICreatesCommandBinding
{
public int Active { get; private set; }
public int GetAffinityForObject<T>(bool hasEventTarget) => typeof(T) == typeof(Button) ? 100 : 0;
public IDisposable? BindCommandToObject<T>(ICommand? command, T? target, IObservable<object?> commandParameter)
where T : class
=> this.BindCommandToObject<T, EventArgs>(command, target, commandParameter, nameof(Button.Click));
public IDisposable? BindCommandToObject<T, TEventArgs>(ICommand? command, T? target, IObservable<object?> commandParameter, string eventName)
where T : class
{
Button button = (Button)(object)target!;
EventHandler handler = (_, _) => command!.Execute(null);
button.Click += handler;
this.Active++;
return new Unbind(() => { button.Click -= handler; this.Active--; });
}
public IDisposable? BindCommandToObject<T, TEventArgs>(
ICommand? command, T? target, IObservable<object?> commandParameter, Action<EventHandler<TEventArgs>> addHandler, Action<EventHandler<TEventArgs>> removeHandler)
where T : class
where TEventArgs : EventArgs
=> null;
private sealed class Unbind(Action action) : IDisposable
{
public void Dispose() => action();
}
}Steps to reproduce
Running is enough.
Exception and stack trace
Package and version
ReactiveUI.Binding.Reactive 8.8.1
UI framework
Windows Forms
Operating system
Windows
Target framework
net10.0-windows10.0.19041
Device and OS version
No response
IDE
No response
Did this work in an earlier version?
No response
Workaround
No response
Additional information
No response
Metadata
Metadata
Assignees
Labels
bugSomething isn't workingSomething isn't working
Activity
fix: dispose the displaced binding when BindCommandUnsafe rebinds
fix: dispose the displaced binding when BindCommandUnsafe rebinds
fix: make view WhenActivated trim and AOT safe; build without runtime…
fix: dispose the displaced binding when BindCommandUnsafe rebinds (#165)