Skip to content

Refactor View.Command, CommandContext, and InputBinding to simplify #4595

@tig

Description

@tig

This is a sub-issue / prerequisite for fixing:

Type Hierarchy (Before)

IInputBinding (interface)
├── Commands: Command[]
├── Data: object?
└── Source: View?  ← Added in Phase 1

KeyBinding : IInputBinding (record struct)
├── Commands, Data, Source
├── Key: Key?
└── Target: View?  ← Used for app-level hotkeys

MouseBinding : IInputBinding (record struct)
├── Commands, Data, Source
└── MouseEvent: Mouse?  ← Renamed from MouseEventArgs in Phase 1

InputBinding : IInputBinding (record struct)  ← Added in Phase 2
├── Commands, Data, Source

ICommandContext (interface)
├── Command: Command
├── Source: View?
└── Binding: IInputBinding?  ← Added in Phase 2

CommandContext : ICommandContext (record struct)  ← Non-generic in Phase 3
├── Command, Source
└── Binding: IInputBinding?

Problem 1: Generic Variance Blocks Pattern Matching

// This fails - generics are invariant
if (args.Context is CommandContext<IInputBinding> ctx)  // FALSE for MouseBinding!

Solution: Non-generic CommandContext with IInputBinding Binding property.

Problem 2: Can't Access Binding Polymorphically

// ICommandContext doesn't have Binding
if (args.Context is ICommandContext ctx)
{
    var binding = ctx.Binding;  // ERROR: no such property
}

Solution: Add IInputBinding Binding { get; } to ICommandContext.

Problem 3: Inconsistent Source Tracking

Binding Type Where Source Is Tracked
KeyBinding Target property
MouseBinding MouseEventArgs.View (nested in Mouse)
Programmatic ICommandContext.Source (no binding)

Solution: Add Source to IInputBinding interface. Keep KeyBinding.Target for backward compatibility (it serves a specific purpose for app-level hotkeys).

Problem 4: Programmatic Invocations Have No Binding

view.InvokeCommand(Command.Accept);  // No binding, Source comes from context

Solution: InputBinding type for programmatic invocations. All invocations have a binding.

Proposed Type System

IInputBinding (Updated)

public interface IInputBinding
{
    /// <summary>The commands this binding will invoke.</summary>
    Command[] Commands { get; set; }

    /// <summary>Arbitrary context data.</summary>
    object? Data { get; set; }

    /// <summary>
    ///     The View that is the origin of this binding.
    ///     For key bindings, this is where the binding was added.
    ///     For mouse bindings, this is the view that received the mouse event.
    ///     For programmatic invocations, this is the view that called InvokeCommand.
    /// </summary>
    View? Source { get; set; }
}

InputBinding (New)

/// <summary>
///     A generic input binding used for programmatic command invocations
///     or when a specific binding type is not needed.
/// </summary>
public record struct InputBinding : IInputBinding
{
    public InputBinding(Command[] commands, View? source = null, object? data = null)
    {
        Commands = commands;
        Source = source;
        Data = data;
    }

    public Command[] Commands { get; set; }
    public object? Data { get; set; }
    public View? Source { get; set; }
}

KeyBinding (Updated)

public record struct KeyBinding : IInputBinding
{
    public Command[] Commands { get; set; }
    public object? Data { get; set; }
    public View? Source { get; set; }  // NEW: from interface
    public Key? Key { get; set; }
    public View? Target { get; set; }  // KEEP: app-level hotkey target
}

MouseBinding (Updated)

public record struct MouseBinding : IInputBinding
{
    public Command[] Commands { get; set; }
    public object? Data { get; set; }
    public View? Source { get; set; }  // NEW: from interface (replaces MouseEvent.View usage)
    public Mouse? MouseEvent { get; set; }  // RENAMED from MouseEventArgs
}

ICommandContext (Updated)

public interface ICommandContext
{
    /// <summary>The command being invoked.</summary>
    Command Command { get; }

    /// <summary>
    ///     The View that first invoked this command.
    ///     This remains constant during command propagation.
    /// </summary>
    View? Source { get; set; }

    /// <summary>
    ///     The binding that triggered the command.
    ///     Use pattern matching to access specific binding types.
    /// </summary>
    IInputBinding Binding { get; }
}

CommandContext (Non-Generic)

public record struct CommandContext : ICommandContext
{
    public CommandContext(Command command, View? source, IInputBinding binding)
    {
        Command = command;
        Source = source;
        Binding = binding;
    }

    public Command Command { get; init; }
    public View? Source { get; set; }
    public IInputBinding Binding { get; init; }
}

Simplified Usage

Before:

        if (args.Context is CommandContext<MouseBinding> { } && checkbox.CheckedState == CheckState.Checked)
        {
            // If user clicks with mouse and item is already checked, do nothing
            args.Handled = true;

            return;
        }

        if (args.Context is CommandContext<KeyBinding> binding && binding.Command == Command.HotKey && checkbox.CheckedState == CheckState.Checked)

After:

        if (args.Context?.Binding is MouseBinding && checkbox.CheckedState == CheckState.Checked)
        {
            // If user clicks with mouse and item is already checked, do nothing
            args.Handled = true;

            return;
        }

        if (args.Context?.Binding is KeyBinding && args.Context.Command == Command.HotKey && checkbox.CheckedState == CheckState.Checked)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    Status

    No status

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions