diff --git a/src/ReactiveUI/Bindings/Command/CommandBinder.cs b/src/ReactiveUI/Bindings/Command/CommandBinder.cs index 36c1fffe5b..0c5a8cefa8 100644 --- a/src/ReactiveUI/Bindings/Command/CommandBinder.cs +++ b/src/ReactiveUI/Bindings/Command/CommandBinder.cs @@ -32,26 +32,30 @@ static CommandBinder() => _binderImplementation = AppLocator.Current.GetService< /// /// 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. /// - /// 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. - /// The type of the view implementing the IViewFor interface. - /// The type of the view model containing the command property. - /// The type of the command property, which must implement ICommand. + /// + /// 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. + /// This method uses reflection to observe events and properties on the control, which may be + /// affected by trimming in some deployment scenarios. + /// + /// The type of the view that implements the IViewFor interface. + /// The type of the view model containing the command to bind. + /// The type of the command property on the view model. Must implement ICommand. /// The type of the control on the view to which the command will be bound. - /// The type of the parameter passed to the command when it is executed. - /// The view instance to which the command will be bound. Cannot be null. - /// The view model instance containing the command property. May be null if the view is not currently bound to a - /// view model. + /// The type of the parameter passed to the command when the event is raised. + /// The view instance containing the control to which the command will be bound. Cannot be null. + /// 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. /// An expression identifying the command property on the view model to bind. Cannot be null. /// An expression identifying the control on the view to which the command will be bound. Cannot be null. /// An observable that provides the parameter to pass to the command when it is executed. Cannot be null. - /// The name of the event on the control that triggers the command. If null, a default event is used based on the - /// control type. + /// 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. - /// An IReactiveBinding instance representing the active binding between the command and the control. + /// An IReactiveBinding{TView, TProp} representing the established binding between the command and the control. + /// It will remain active until disposed. [RequiresUnreferencedCode("Dynamic observation uses reflection over members that may be trimmed.")] public static IReactiveBinding BindCommand< TView, @@ -79,25 +83,29 @@ public static IReactiveBinding BindCommand< } /// - /// 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. /// - /// 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. - /// The type of the view implementing the IViewFor interface. - /// The type of the view model containing the command property. - /// The type of the command property to bind, implementing ICommand. + /// + /// 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. + /// This method uses reflection to observe events and properties on the control, which may be + /// affected by trimming in some deployment scenarios. + /// + /// The type of the view that implements the IViewFor interface. + /// The type of the view model containing the command to bind. + /// The type of the command property on the view model. Must implement ICommand. /// The type of the control on the view to which the command will be bound. - /// The view instance to which the control belongs. Cannot be null. - /// The view model instance containing the command property. Can be null if the view's ViewModel property is used. + /// The view instance containing the control to which the command will be bound. Cannot be null. + /// 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. /// An expression identifying the command property on the view model to bind. Cannot be null. - /// An expression identifying the control on the view to bind the command to. Cannot be null. - /// The name of the event on the control that triggers the command. If null, a default event is used based on the - /// control type. + /// An expression identifying the control on the view to which the command will be bound. Cannot be null. + /// 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. - /// An object representing the binding between the command and the control, which can be disposed to unbind. + /// An IReactiveBinding{TView, TProp} representing the established binding between the command and the control. + /// It will remain active until disposed. [RequiresUnreferencedCode("Dynamic observation uses reflection over members that may be trimmed.")] public static IReactiveBinding BindCommand< TView, @@ -123,26 +131,30 @@ public static IReactiveBinding BindCommand< /// /// 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. /// - /// 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. - /// The type of the view implementing the IViewFor interface. - /// The type of the view model containing the command property. - /// The type of the command property, typically implementing ICommand. + /// + /// 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. + /// This method uses reflection to observe events and properties on the control, which may be + /// affected by trimming in some deployment scenarios. + /// + /// The type of the view that implements the IViewFor interface. + /// The type of the view model containing the command to bind. + /// The type of the command property on the view model. Must implement ICommand. /// The type of the control on the view to which the command will be bound. - /// The type of the parameter passed to the command when it is executed. - /// The view instance containing the control to bind the command to. Cannot be null. - /// The view model instance containing the command property. May be null if the view is not currently bound to a - /// view model. + /// The type of the parameter passed to the command when the event is raised. + /// The view instance containing the control to which the command will be bound. Cannot be null. + /// 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. /// An expression identifying the command property on the view model to bind. Cannot be null. /// An expression identifying the control on the view to which the command will be bound. Cannot be null. /// An expression specifying the parameter to pass to the command when it is executed. Cannot be null. /// 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. - /// An IReactiveBinding{TView, TProp} representing the established binding between the command and the control. + /// An IReactiveBinding{TView, TProp} representing the established binding between the command and the control. + /// It will remain active until disposed. [RequiresUnreferencedCode("Dynamic observation uses reflection over members that may be trimmed.")] public static IReactiveBinding BindCommand< TView, diff --git a/src/ReactiveUI/Bindings/Command/CommandBinderImplementation.cs b/src/ReactiveUI/Bindings/Command/CommandBinderImplementation.cs index 5c973baadf..9b19cb5195 100644 --- a/src/ReactiveUI/Bindings/Command/CommandBinderImplementation.cs +++ b/src/ReactiveUI/Bindings/Command/CommandBinderImplementation.cs @@ -25,29 +25,7 @@ namespace ReactiveUI; /// public class CommandBinderImplementation : ICommandBinderImplementation { - /// - /// 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. - /// - /// 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. - /// The type of the view implementing the IViewFor interface. - /// The type of the view model containing the command property. - /// The type of the command property to bind, implementing ICommand. - /// The type of the control on the view to which the command will be bound. - /// The type of the parameter passed to the command when it is executed. - /// The view model instance containing the command to bind. Can be null if the binding should be established without - /// an initial view model. - /// The view instance containing the control to which the command will be bound. Cannot be null. - /// An expression specifying the command property on the view model to bind. Cannot be null. - /// An expression specifying the control on the view to which the command will be bound. Cannot be null. - /// 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. - /// 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. - /// An IReactiveBinding{TView, TProp} representing the established command binding. Disposing the returned object - /// will remove the binding. + /// [RequiresUnreferencedCode("Dynamic observation uses reflection over members that may be trimmed.")] public IReactiveBinding BindCommand< TView, @@ -68,17 +46,20 @@ public IReactiveBinding BindCommand< { ArgumentExceptionHelper.ThrowIfNull(vmProperty); ArgumentExceptionHelper.ThrowIfNull(controlProperty); + ArgumentExceptionHelper.ThrowIfNull(withParameter); 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(); + var parameterObservable = Reflection.ViewModelWhenAnyValue(viewModel, view, parameterExpression).Cast(); var bindingDisposable = BindCommandInternal( source, view, controlExpression, - withParameter.ToObservable(viewModel), + parameterObservable, toEvent); return new ReactiveBinding( @@ -90,31 +71,7 @@ public IReactiveBinding BindCommand< bindingDisposable); } - /// - /// 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. - /// - /// 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. - /// The type of the view implementing the IViewFor interface. - /// The type of the view model containing the command property. - /// The type of the command property, which must implement ICommand. - /// The type of the control on the view to which the command will be bound. - /// The type of the parameter passed to the command when it is executed. - /// The view model instance containing the command to bind. Can be null if the view model is not available at - /// binding time. - /// The view instance containing the control to which the command will be bound. - /// An expression specifying the command property on the view model to bind. - /// An expression specifying the control on the view that will trigger the command. - /// An observable sequence providing the parameter to pass to the command when it is executed. The latest value is - /// used for each command invocation. - /// The name of the event on the control that triggers the command. If null, a default event is used based on the - /// control type. - /// An IReactiveBinding{TView, TProp} representing the established binding between the command and the control. - /// Disposing the binding will remove the command association. + /// [RequiresUnreferencedCode("Dynamic observation uses reflection over members that may be trimmed.")] public IReactiveBinding BindCommand< TView, @@ -197,7 +154,9 @@ private static IDisposable BindCommandInternal< var isInitialBind = true; // Check for optional platform-specific command rebinding customization - var rebindingCustomizer = AppLocator.Current.GetService(); + var rebindingCustomizer = string.IsNullOrEmpty(toEvent) + ? AppLocator.Current.GetService() + : null; // Cache boxing of parameter values once to avoid rebuilding the Select pipeline on every rebind. var boxedParameter = withParameter.Select(static p => (object?)p); diff --git a/src/ReactiveUI/Bindings/Command/CommandBinderImplementationMixins.cs b/src/ReactiveUI/Bindings/Command/CommandBinderImplementationMixins.cs index 73e95d7c53..23ece67f1c 100644 --- a/src/ReactiveUI/Bindings/Command/CommandBinderImplementationMixins.cs +++ b/src/ReactiveUI/Bindings/Command/CommandBinderImplementationMixins.cs @@ -13,26 +13,30 @@ namespace ReactiveUI; internal static class CommandBinderImplementationMixins { /// - /// Binds an ICommand property on the view model to a control on the view, wiring the command to the specified event - /// on the control. + /// 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. /// - /// This method uses reflection to observe events and properties on the control and may not be - /// compatible with all trimming scenarios. The binding will automatically enable or disable the control based on - /// the command's CanExecute state. - /// The type of the view, which must implement IViewFor. - /// The type of the view model. - /// The type of the ICommand property on the view model. + /// + /// 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. + /// This method uses reflection to observe events and properties on the control, which may be + /// affected by trimming in some deployment scenarios. + /// + /// The type of the view that implements the IViewFor interface. + /// The type of the view model containing the command to bind. + /// The type of the command property on the view model. Must implement ICommand. /// The type of the control on the view to which the command will be bound. /// The command binder implementation used to perform the binding. - /// The view model instance containing the ICommand property to bind. Can be null if the view is not currently - /// associated with a view model. - /// The view instance containing the control to which the command will be bound. - /// An expression identifying the ICommand property on the view model to bind. - /// An expression identifying the control on the view to which the command will be bound. - /// The name of the event on the control that will trigger the command. If null, a default event is used based on - /// the control type. - /// An IReactiveBinding{TView, TProp} representing the established binding between the command and the control - /// event. + /// 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. + /// The view instance containing the control to which the command will be bound. Cannot be null. + /// An expression identifying the command property on the view model to bind. Cannot be null. + /// An expression identifying the control on the view to which the command will be bound. Cannot be null. + /// 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. + /// An IReactiveBinding{TView, TProp} representing the established binding between the command and the control. + /// It will remain active until disposed. [RequiresUnreferencedCode("Dynamic observation uses reflection over members that may be trimmed.")] public static IReactiveBinding BindCommand( this ICommandBinderImplementation @this, @@ -46,49 +50,4 @@ internal static class CommandBinderImplementationMixins where TProp : ICommand where TControl : class => @this.BindCommand(viewModel, view, propertyName, controlName, Observable.Empty, toEvent); - - /// - /// Binds a command from the view model to a control on the view, using a parameter expression to supply the command - /// parameter dynamically. - /// - /// This overload allows the command parameter to be supplied dynamically by observing changes in - /// the view model. The binding will automatically update the command parameter as the observed value changes. The - /// method uses reflection and may be affected by trimming in environments that perform code trimming. - /// The type of the view implementing the IViewFor interface. - /// The type of the view model containing the command. - /// The type of the command property on the view model. - /// The type of the control on the view to which the command will be bound. - /// The type of the parameter passed to the command when it is executed. - /// The command binder implementation used to perform the binding. - /// The view model instance containing the command to bind. Can be null if the view is not currently bound to a view - /// model. - /// The view instance containing the control to which the command will be bound. - /// An expression identifying the command property on the view model to bind. - /// An expression identifying the control on the view to which the command will be bound. - /// An expression that specifies how to obtain the parameter value to pass to the command when it is executed. - /// Cannot be null. - /// 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. - /// An object representing the binding between the command and the control, which can be disposed to unbind. - [RequiresUnreferencedCode("Dynamic observation uses reflection over members that may be trimmed.")] - public static IReactiveBinding BindCommand( - this ICommandBinderImplementation @this, - TViewModel? viewModel, - TView view, - Expression> propertyName, - Expression> controlName, - Expression> withParameter, - string? toEvent = null) - where TView : class, IViewFor - where TViewModel : class - where TProp : ICommand - where TControl : class - { - ArgumentExceptionHelper.ThrowIfNull(withParameter); - - var paramExpression = Reflection.Rewrite(withParameter.Body); - var param = Reflection.ViewModelWhenAnyValue(viewModel, view, paramExpression); - - return @this.BindCommand(viewModel, view, propertyName, controlName, param, toEvent); - } } diff --git a/src/ReactiveUI/Bindings/Command/ICommandBinderImplementation.cs b/src/ReactiveUI/Bindings/Command/ICommandBinderImplementation.cs index 350dab1cfd..a44db9af3d 100644 --- a/src/ReactiveUI/Bindings/Command/ICommandBinderImplementation.cs +++ b/src/ReactiveUI/Bindings/Command/ICommandBinderImplementation.cs @@ -13,26 +13,31 @@ namespace ReactiveUI; internal interface ICommandBinderImplementation : IEnableLogger { /// - /// 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 a specified event is raised. + /// 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. /// - /// This method uses reflection to observe events and properties on the control, which may be - /// affected by trimming in some deployment scenarios. The binding remains active until it is disposed. If the - /// specified event is not found on the control, an exception may be thrown at runtime. - /// The type of the view implementing the IViewFor interface. + /// + /// 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. + /// This method uses reflection to observe events and properties on the control, which may be + /// affected by trimming in some deployment scenarios. + /// + /// The type of the view that implements the IViewFor interface. /// The type of the view model containing the command to bind. - /// The type of the command property on the view model, typically implementing ICommand. + /// The type of the command property on the view model. Must implement ICommand. /// The type of the control on the view to which the command will be bound. - /// The type of the parameter passed to the command when it is executed. - /// The view model instance containing the command to bind. Can be null if the binding should be established without - /// an initial view model. + /// The type of the parameter passed to the command when the event is raised. + /// The view model instance containing the command to bind. Necessary for type inference. + /// Can be null if the binding should be established without an initial view model. /// The view instance containing the control to which the command will be bound. Cannot be null. - /// An expression identifying the command property on the view model to bind. - /// An expression identifying the control on the view that will trigger the command. - /// An expression specifying the parameter to pass to the command when it is executed. + /// An expression identifying the command property on the view model to bind. Cannot be null. + /// An expression identifying the control on the view to which the command will be bound. Cannot be null. + /// An expression specifying the parameter to pass to the command when it is executed. Cannot be null. /// 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. - /// An IReactiveBinding{TView, TProp} representing the established binding between the command and the control. + /// 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. + /// An IReactiveBinding{TView, TProp} representing the established binding between the command and the control. + /// It will remain active until disposed. [RequiresUnreferencedCode("Dynamic observation uses reflection over members that may be trimmed.")] IReactiveBinding BindCommand( TViewModel? viewModel, @@ -50,26 +55,28 @@ internal interface ICommandBinderImplementation : IEnableLogger /// 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. /// - /// This method uses reflection to 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. If the specified event does not exist on the control, an exception may be thrown - /// at runtime. + /// + /// 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. + /// This method uses reflection to observe events and properties on the control, which may be + /// affected by trimming in some deployment scenarios. + /// /// The type of the view that implements the IViewFor interface. /// The type of the view model containing the command to bind. /// The type of the command property on the view model. Must implement ICommand. /// The type of the control on the view to which the command will be bound. /// The type of the parameter passed to the command when the event is raised. - /// The view model instance containing the command to bind. Can be null if the binding should be established without - /// an initial view model. + /// 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. /// The view instance containing the control to which the command will be bound. Cannot be null. - /// An expression identifying the command property on the view model to bind. - /// An expression identifying the control on the view to which the command will be bound. - /// An observable that provides the parameter value to pass to the command when the event is raised. Can emit null - /// values. + /// An expression identifying the command property on the view model to bind. Cannot be null. + /// An expression identifying the control on the view to which the command will be bound. Cannot be null. + /// An observable that provides the parameter to pass to the command when it is executed. Cannot be null. /// 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. - /// An IReactiveBinding{TView, TProp} instance representing the established binding between the command and the - /// control. + /// 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. + /// An IReactiveBinding{TView, TProp} representing the established binding between the command and the control. + /// It will remain active until disposed. [RequiresUnreferencedCode("Dynamic observation uses reflection over members that may be trimmed.")] IReactiveBinding BindCommand( TViewModel? viewModel, diff --git a/src/tests/ReactiveUI.WinForms.Tests/winforms/CommandBindingImplementationTests.cs b/src/tests/ReactiveUI.WinForms.Tests/winforms/CommandBindingImplementationTests.cs index 7a14d61f14..8ffa862402 100644 --- a/src/tests/ReactiveUI.WinForms.Tests/winforms/CommandBindingImplementationTests.cs +++ b/src/tests/ReactiveUI.WinForms.Tests/winforms/CommandBindingImplementationTests.cs @@ -54,7 +54,7 @@ public async Task CommandBindByNameWireupWithParameter() var invokeCount = 0; vm.Command3.Subscribe(_ => ++invokeCount); - var disp = CommandBinderImplementationMixins.BindCommand(fixture, vm, view, vm => vm.Command3, v => v.Command1, vm => vm.Parameter); + var disp = fixture.BindCommand(vm, view, vm => vm.Command3, v => v.Command1, vm => vm.Parameter); view.Command1.PerformClick(); using (Assert.Multiple()) @@ -96,7 +96,7 @@ public async Task CommandBindToExplicitEventWireupWithParameter() var invokeCount = 0; vm.Command3.Subscribe(_ => ++invokeCount); - var disp = CommandBinderImplementationMixins.BindCommand(fixture, vm, view, x => x.Command3, x => x.Command2, vm => vm.Parameter, "MouseUp"); + var disp = fixture.BindCommand(vm, view, x => x.Command3, x => x.Command2, vm => vm.Parameter, "MouseUp"); view.Command2.RaiseMouseUpEvent(new MouseEventArgs(MouseButtons.Left, 1, 0, 0, 0)); using (Assert.Multiple()) diff --git a/src/tests/ReactiveUI.Wpf.Tests/Wpf/WpfCommandBindingImplementationTests.cs b/src/tests/ReactiveUI.Wpf.Tests/Wpf/WpfCommandBindingImplementationTests.cs index 0684edcf53..bd3323b362 100644 --- a/src/tests/ReactiveUI.Wpf.Tests/Wpf/WpfCommandBindingImplementationTests.cs +++ b/src/tests/ReactiveUI.Wpf.Tests/Wpf/WpfCommandBindingImplementationTests.cs @@ -9,6 +9,7 @@ using ReactiveUI.Tests.Utilities.Logging; using ReactiveUI.Tests.Wpf.Mocks; +using ReactiveUI.Tests.Xaml.Mocks; namespace ReactiveUI.Tests.Wpf; @@ -272,4 +273,29 @@ public async Task ViewModelShouldBeGarbageCollectedWhenOverwritten() await Assert.That(weakRef.IsAlive).IsFalse(); } + + [Test] + public async Task CommandAndParameterRebindToNewViewModelInstance() + { + var vm = new CommandBindingViewModel { Value = 1 }; + var view = new CommandBindingView { ViewModel = vm }; + + var received1 = 0; + view.ViewModel.Command1.Subscribe(i => received1 = i); + + var binding = new CommandBinderImplementation().BindCommand(vm, view, vm => vm.Command1, v => v.Command1, vm => vm.Value, nameof(CustomClickButton.CustomClick)); + + view.ViewModel = new CommandBindingViewModel { Value = 2 }; + + var received2 = 0; + view.ViewModel.Command1.Subscribe(i => received2 = i); + + view.Command1.RaiseCustomClick(); + + using (Assert.Multiple()) + { + await Assert.That(received1).IsEqualTo(0); + await Assert.That(received2).IsEqualTo(2); + } + } }