Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 54 additions & 42 deletions src/ReactiveUI/Bindings/Command/CommandBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,26 +32,30 @@ static CommandBinder() => _binderImplementation = AppLocator.Current.GetService<

/// <summary>
/// Binds a command from the view model to a control on the view, enabling the control to execute the command with a
/// parameter when triggered.
/// specified parameter when an event is raised.
/// </summary>
/// <remarks>This method uses reflection to dynamically observe events and properties on the control,
/// which may be affected by trimming in some deployment scenarios. The binding remains active until the returned
/// IReactiveBinding is disposed.</remarks>
/// <typeparam name="TView">The type of the view implementing the IViewFor interface.</typeparam>
/// <typeparam name="TViewModel">The type of the view model containing the command property.</typeparam>
/// <typeparam name="TProp">The type of the command property, which must implement ICommand.</typeparam>
/// <remarks>
/// <para>The binding enables the control to execute the command when the specified event is raised,
/// and automatically manages the enabled state of the control based on the command's CanExecute state.</para>
/// <para>This method uses reflection to observe events and properties on the control, which may be
/// affected by trimming in some deployment scenarios.</para>
/// </remarks>
/// <typeparam name="TView">The type of the view that implements the IViewFor interface.</typeparam>
/// <typeparam name="TViewModel">The type of the view model containing the command to bind.</typeparam>
/// <typeparam name="TProp">The type of the command property on the view model. Must implement ICommand.</typeparam>
/// <typeparam name="TControl">The type of the control on the view to which the command will be bound.</typeparam>
/// <typeparam name="TParam">The type of the parameter passed to the command when it is executed.</typeparam>
/// <param name="view">The view instance to which the command will be bound. Cannot be null.</param>
/// <param name="viewModel">The view model instance containing the command property. May be null if the view is not currently bound to a
/// view model.</param>
/// <typeparam name="TParam">The type of the parameter passed to the command when the event is raised.</typeparam>
/// <param name="view">The view instance containing the control to which the command will be bound. Cannot be null.</param>
/// <param name="viewModel">The view model instance containing the command to bind. Used for type inference.
/// Can be null if the binding should be established without an initial view model.</param>
/// <param name="propertyName">An expression identifying the command property on the view model to bind. Cannot be null.</param>
/// <param name="controlName">An expression identifying the control on the view to which the command will be bound. Cannot be null.</param>
/// <param name="withParameter">An observable that provides the parameter to pass to the command when it is executed. Cannot be null.</param>
/// <param name="toEvent">The name of the event on the control that triggers the command. If null, a default event is used based on the
/// control type.
/// <param name="toEvent">The name of the event on the control that triggers the command execution. If null, a default event is used based
/// on the control type. If the specified event does not exist on the control, an exception may be thrown at runtime.
/// NOTE: If this parameter is used inside WhenActivated, it's important to dispose the binding when the view is deactivated.</param>
/// <returns>An IReactiveBinding instance representing the active binding between the command and the control.</returns>
/// <returns>An IReactiveBinding{TView, TProp} representing the established binding between the command and the control.
/// It will remain active until disposed.</returns>
[RequiresUnreferencedCode("Dynamic observation uses reflection over members that may be trimmed.")]
public static IReactiveBinding<TView, TProp> BindCommand<
TView,
Expand Down Expand Up @@ -79,25 +83,29 @@ public static IReactiveBinding<TView, TProp> BindCommand<
}

/// <summary>
/// Binds a command from the view model to a control on the view, enabling the control to execute the specified
/// command when triggered.
/// Binds a command from the view model to a control on the view, enabling the control to execute the command with a
/// specified parameter when an event is raised.
/// </summary>
/// <remarks>This method uses reflection to observe events and properties on the control and may be
/// affected by trimming in environments that remove unused members. The binding enables the control to execute the
/// command when the specified event is raised, and automatically manages the enabled state of the control based on
/// the command's CanExecute state.</remarks>
/// <typeparam name="TView">The type of the view implementing the IViewFor interface.</typeparam>
/// <typeparam name="TViewModel">The type of the view model containing the command property.</typeparam>
/// <typeparam name="TProp">The type of the command property to bind, implementing ICommand.</typeparam>
/// <remarks>
/// <para>The binding enables the control to execute the command when the specified event is raised,
/// and automatically manages the enabled state of the control based on the command's CanExecute state.</para>
/// <para>This method uses reflection to observe events and properties on the control, which may be
/// affected by trimming in some deployment scenarios.</para>
/// </remarks>
/// <typeparam name="TView">The type of the view that implements the IViewFor interface.</typeparam>
/// <typeparam name="TViewModel">The type of the view model containing the command to bind.</typeparam>
/// <typeparam name="TProp">The type of the command property on the view model. Must implement ICommand.</typeparam>
/// <typeparam name="TControl">The type of the control on the view to which the command will be bound.</typeparam>
/// <param name="view">The view instance to which the control belongs. Cannot be null.</param>
/// <param name="viewModel">The view model instance containing the command property. Can be null if the view's ViewModel property is used.</param>
/// <param name="view">The view instance containing the control to which the command will be bound. Cannot be null.</param>
/// <param name="viewModel">The view model instance containing the command to bind. Used for type inference.
/// Can be null if the binding should be established without an initial view model.</param>
/// <param name="propertyName">An expression identifying the command property on the view model to bind. Cannot be null.</param>
/// <param name="controlName">An expression identifying the control on the view to bind the command to. Cannot be null.</param>
/// <param name="toEvent">The name of the event on the control that triggers the command. If null, a default event is used based on the
/// control type.
/// <param name="controlName">An expression identifying the control on the view to which the command will be bound. Cannot be null.</param>
/// <param name="toEvent">The name of the event on the control that triggers the command execution. If null, a default event is used based
/// on the control type. If the specified event does not exist on the control, an exception may be thrown at runtime.
/// NOTE: If this parameter is used inside WhenActivated, it's important to dispose the binding when the view is deactivated.</param>
/// <returns>An object representing the binding between the command and the control, which can be disposed to unbind.</returns>
/// <returns>An IReactiveBinding{TView, TProp} representing the established binding between the command and the control.
/// It will remain active until disposed.</returns>
[RequiresUnreferencedCode("Dynamic observation uses reflection over members that may be trimmed.")]
public static IReactiveBinding<TView, TProp> BindCommand<
TView,
Expand All @@ -123,26 +131,30 @@ public static IReactiveBinding<TView, TProp> BindCommand<

/// <summary>
/// Binds a command from the view model to a control on the view, enabling the control to execute the command with a
/// specified parameter when triggered.
/// specified parameter when an event is raised.
/// </summary>
/// <remarks>This method uses reflection to observe events and properties on the control and view model,
/// which may be affected by trimming in some deployment scenarios. The binding remains active until the returned
/// IReactiveBinding is disposed.</remarks>
/// <typeparam name="TView">The type of the view implementing the IViewFor interface.</typeparam>
/// <typeparam name="TViewModel">The type of the view model containing the command property.</typeparam>
/// <typeparam name="TProp">The type of the command property, typically implementing ICommand.</typeparam>
/// <remarks>
/// <para>The binding enables the control to execute the command when the specified event is raised,
/// and automatically manages the enabled state of the control based on the command's CanExecute state.</para>
/// <para>This method uses reflection to observe events and properties on the control, which may be
/// affected by trimming in some deployment scenarios.</para>
/// </remarks>
/// <typeparam name="TView">The type of the view that implements the IViewFor interface.</typeparam>
/// <typeparam name="TViewModel">The type of the view model containing the command to bind.</typeparam>
/// <typeparam name="TProp">The type of the command property on the view model. Must implement ICommand.</typeparam>
/// <typeparam name="TControl">The type of the control on the view to which the command will be bound.</typeparam>
/// <typeparam name="TParam">The type of the parameter passed to the command when it is executed.</typeparam>
/// <param name="view">The view instance containing the control to bind the command to. Cannot be null.</param>
/// <param name="viewModel">The view model instance containing the command property. May be null if the view is not currently bound to a
/// view model.</param>
/// <typeparam name="TParam">The type of the parameter passed to the command when the event is raised.</typeparam>
/// <param name="view">The view instance containing the control to which the command will be bound. Cannot be null.</param>
/// <param name="viewModel">The view model instance containing the command to bind. Used for type inference.
/// Can be null if the binding should be established without an initial view model.</param>
/// <param name="propertyName">An expression identifying the command property on the view model to bind. Cannot be null.</param>
/// <param name="controlName">An expression identifying the control on the view to which the command will be bound. Cannot be null.</param>
/// <param name="withParameter">An expression specifying the parameter to pass to the command when it is executed. Cannot be null.</param>
/// <param name="toEvent">The name of the event on the control that triggers the command execution. If null, a default event is used based
/// on the control type.
/// on the control type. If the specified event does not exist on the control, an exception may be thrown at runtime.
/// NOTE: If this parameter is used inside WhenActivated, it's important to dispose the binding when the view is deactivated.</param>
/// <returns>An IReactiveBinding{TView, TProp} representing the established binding between the command and the control.</returns>
/// <returns>An IReactiveBinding{TView, TProp} representing the established binding between the command and the control.
/// It will remain active until disposed.</returns>
[RequiresUnreferencedCode("Dynamic observation uses reflection over members that may be trimmed.")]
public static IReactiveBinding<TView, TProp> BindCommand<
TView,
Expand Down
59 changes: 9 additions & 50 deletions src/ReactiveUI/Bindings/Command/CommandBinderImplementation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,29 +25,7 @@ namespace ReactiveUI;
/// </remarks>
public class CommandBinderImplementation : ICommandBinderImplementation
{
/// <summary>
/// Binds a command from the view model to a control on the view, enabling the control to execute the command with
/// an optional parameter when triggered by a specified event.
/// </summary>
/// <remarks>This method uses reflection to observe properties and events, which may be affected by
/// trimming in some deployment scenarios. The binding is one-way, from the view model command to the view control.
/// If the specified event is not found on the control, an exception may be thrown at runtime.</remarks>
/// <typeparam name="TView">The type of the view implementing the IViewFor interface.</typeparam>
/// <typeparam name="TViewModel">The type of the view model containing the command property.</typeparam>
/// <typeparam name="TProp">The type of the command property to bind, implementing ICommand.</typeparam>
/// <typeparam name="TControl">The type of the control on the view to which the command will be bound.</typeparam>
/// <typeparam name="TParam">The type of the parameter passed to the command when it is executed.</typeparam>
/// <param name="viewModel">The view model instance containing the command to bind. Can be null if the binding should be established without
/// an initial view model.</param>
/// <param name="view">The view instance containing the control to which the command will be bound. Cannot be null.</param>
/// <param name="vmProperty">An expression specifying the command property on the view model to bind. Cannot be null.</param>
/// <param name="controlProperty">An expression specifying the control on the view to which the command will be bound. Cannot be null.</param>
/// <param name="withParameter">An expression specifying the parameter to pass to the command when it is executed. Can be null if the command
/// does not require a parameter.</param>
/// <param name="toEvent">The name of the event on the control that triggers the command execution. If null, a default event is used based
/// on the control type.</param>
/// <returns>An IReactiveBinding{TView, TProp} representing the established command binding. Disposing the returned object
/// will remove the binding.</returns>
/// <inheritdoc />
[RequiresUnreferencedCode("Dynamic observation uses reflection over members that may be trimmed.")]
public IReactiveBinding<TView, TProp> BindCommand<
TView,
Expand All @@ -68,17 +46,20 @@ public IReactiveBinding<TView, TProp> BindCommand<
{
ArgumentExceptionHelper.ThrowIfNull(vmProperty);
ArgumentExceptionHelper.ThrowIfNull(controlProperty);
ArgumentExceptionHelper.ThrowIfNull(withParameter);
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before this change null was rejected by withParameter.ToObservable(viewModel) anyway.


var vmExpression = Reflection.Rewrite(vmProperty.Body);
var controlExpression = Reflection.Rewrite(controlProperty.Body);
var parameterExpression = Reflection.Rewrite(withParameter.Body);

var source = Reflection.ViewModelWhenAnyValue(viewModel, view, vmExpression).Cast<TProp>();
var parameterObservable = Reflection.ViewModelWhenAnyValue(viewModel, view, parameterExpression).Cast<TParam>();

var bindingDisposable = BindCommandInternal<TView, TProp, TParam, TControl>(
source,
view,
controlExpression,
withParameter.ToObservable(viewModel),
parameterObservable,
toEvent);

return new ReactiveBinding<TView, TProp>(
Expand All @@ -90,31 +71,7 @@ public IReactiveBinding<TView, TProp> BindCommand<
bindingDisposable);
}

/// <summary>
/// Binds a command from the view model to a control on the view, enabling the control to execute the command with
/// an optional parameter stream and event trigger.
/// </summary>
/// <remarks>This method uses reflection to observe and bind to members, which may be affected by trimming
/// in some environments. The binding is one-way, from the view model command to the view control. If the control or
/// command property is not found, the binding will not be established. The method is suitable for scenarios where
/// commands need to be dynamically bound to controls with support for parameter streams and custom event
/// triggers.</remarks>
/// <typeparam name="TView">The type of the view implementing the IViewFor interface.</typeparam>
/// <typeparam name="TViewModel">The type of the view model containing the command property.</typeparam>
/// <typeparam name="TProp">The type of the command property, which must implement ICommand.</typeparam>
/// <typeparam name="TControl">The type of the control on the view to which the command will be bound.</typeparam>
/// <typeparam name="TParam">The type of the parameter passed to the command when it is executed.</typeparam>
/// <param name="viewModel">The view model instance containing the command to bind. Can be null if the view model is not available at
/// binding time.</param>
/// <param name="view">The view instance containing the control to which the command will be bound.</param>
/// <param name="vmProperty">An expression specifying the command property on the view model to bind.</param>
/// <param name="controlProperty">An expression specifying the control on the view that will trigger the command.</param>
/// <param name="withParameter">An observable sequence providing the parameter to pass to the command when it is executed. The latest value is
/// used for each command invocation.</param>
/// <param name="toEvent">The name of the event on the control that triggers the command. If null, a default event is used based on the
/// control type.</param>
/// <returns>An IReactiveBinding{TView, TProp} representing the established binding between the command and the control.
/// Disposing the binding will remove the command association.</returns>
/// <inheritdoc />
[RequiresUnreferencedCode("Dynamic observation uses reflection over members that may be trimmed.")]
public IReactiveBinding<TView, TProp> BindCommand<
TView,
Expand Down Expand Up @@ -197,7 +154,9 @@ private static IDisposable BindCommandInternal<
var isInitialBind = true;

// Check for optional platform-specific command rebinding customization
var rebindingCustomizer = AppLocator.Current.GetService<ICreatesCustomizedCommandRebinding>();
var rebindingCustomizer = string.IsNullOrEmpty(toEvent)
? AppLocator.Current.GetService<ICreatesCustomizedCommandRebinding>()
: null;

// Cache boxing of parameter values once to avoid rebuilding the Select pipeline on every rebind.
var boxedParameter = withParameter.Select(static p => (object?)p);
Expand Down
Loading