Skip to content
This repository was archived by the owner on Nov 3, 2026. It is now read-only.
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

Closed

Description

@volkmnv

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

Activity

  1. added theissue type on Oct 1, 2026
  2. added 2 commits that reference this issue on Oct 3, 2026
    6a8b03b
    95fbd1c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      ⌂ 换网址