11using System ;
2+ using System . Collections . Generic ;
3+ using System . Threading . Tasks ;
4+ using System . Windows . Input ;
5+ using Microsoft . Maui . Controls . Internals ;
26using Xunit ;
37
48namespace Microsoft . Maui . Controls . Core . UnitTests
@@ -251,5 +255,110 @@ public void ExecuteDoesNotRunIfValueTypeAndSetToNull()
251255 command . Execute ( null ) ; // "null is not a valid value for int"
252256 Assert . True ( executions == 0 , "the command should not have executed" ) ;
253257 }
258+
259+ [ Theory ]
260+ [ InlineData ( typeof ( Button ) , true ) ]
261+ [ InlineData ( typeof ( Button ) , false ) ]
262+ [ InlineData ( typeof ( RefreshView ) , true ) ]
263+ [ InlineData ( typeof ( RefreshView ) , false ) ]
264+ [ InlineData ( typeof ( TextCell ) , true ) ]
265+ [ InlineData ( typeof ( TextCell ) , false ) ]
266+ [ InlineData ( typeof ( ImageButton ) , true ) ]
267+ [ InlineData ( typeof ( ImageButton ) , false ) ]
268+ [ InlineData ( typeof ( MenuItem ) , true ) ]
269+ [ InlineData ( typeof ( MenuItem ) , false ) ]
270+ [ InlineData ( typeof ( SearchBar ) , true ) ]
271+ [ InlineData ( typeof ( SearchBar ) , false ) ]
272+ [ InlineData ( typeof ( SearchHandler ) , true ) ]
273+ [ InlineData ( typeof ( SearchHandler ) , false ) ]
274+ public async Task CommandsSubscribedToCanExecuteCollect ( Type controlType , bool useWeakEventHandler )
275+ {
276+ // Create a view model with a Command
277+ ICommand command ;
278+
279+ if ( ! useWeakEventHandler )
280+ command = new CommandWithoutWeakEventHandler ( ) ;
281+ else
282+ command = new Command ( ( ) => { } ) ;
283+
284+ List < WeakReference > weakReferences = new List < WeakReference > ( ) ;
285+
286+ // Create a button in a separate scope to ensure no references remain
287+ {
288+ var control = ( BindableObject ) Activator . CreateInstance ( controlType ) ;
289+ switch ( control )
290+ {
291+ case Button b :
292+ b . Command = command ;
293+ break ;
294+ case RefreshView r :
295+ r . Command = command ;
296+ break ;
297+ case TextCell t :
298+ t . Command = command ;
299+ break ;
300+ case ImageButton i :
301+ i . Command = command ;
302+ break ;
303+ case MenuItem m :
304+ m . Command = command ;
305+ break ;
306+ case SearchBar s :
307+ s . SearchCommand = command ;
308+ break ;
309+ case SearchHandler sh :
310+ sh . Command = command ;
311+ sh . ClearPlaceholderCommand = command ;
312+ break ;
313+ }
314+
315+ // Create a weak reference to the button
316+ weakReferences . Add ( new WeakReference ( control ) ) ;
317+
318+ if ( control is ICommandElement commandElement )
319+ {
320+ // Add weak references to the command and its cleanup tracker
321+ weakReferences . Add ( new WeakReference ( commandElement . CleanupTracker ) ) ;
322+ weakReferences . Add ( new WeakReference ( commandElement . CleanupTracker . Proxy ) ) ;
323+ }
324+ else if ( control is SearchHandler searchHandler )
325+ {
326+ // Add weak references to the command and its cleanup tracker
327+ weakReferences . Add ( new WeakReference ( searchHandler . CommandSubscription ) ) ;
328+ weakReferences . Add ( new WeakReference ( searchHandler . CommandSubscription . Proxy ) ) ;
329+ weakReferences . Add ( new WeakReference ( searchHandler . ClearPlaceholderCommandSubscription ) ) ;
330+ weakReferences . Add ( new WeakReference ( searchHandler . ClearPlaceholderCommandSubscription . Proxy ) ) ;
331+ }
332+
333+ await TestHelpers . Collect ( ) ;
334+ await TestHelpers . Collect ( ) ;
335+
336+ // Make sure everything is still alive if the button is still in scope
337+ // We need to reference the button here again to keep it alive
338+ // awaiting a Task appears to move us to a new scope and causes the button to be collected
339+ Assert . NotNull ( control ) ;
340+
341+ foreach ( var weakRef in weakReferences )
342+ {
343+ Assert . True ( weakRef . IsAlive ) ;
344+ }
345+ }
346+
347+ foreach ( var weakRef in weakReferences )
348+ {
349+ Assert . False ( await weakRef . WaitForCollect ( ) ) ;
350+ }
351+ }
352+
353+ class CommandWithoutWeakEventHandler : ICommand
354+ {
355+ public event EventHandler CanExecuteChanged ;
356+
357+ public bool CanExecute ( object parameter ) => true ;
358+
359+ public void Execute ( object parameter ) { }
360+
361+ public void ChangeCanExecute ( ) => CanExecuteChanged ? . Invoke ( this , EventArgs . Empty ) ;
362+ }
254363 }
255- }
364+ }
0 commit comments