Skip to content

Fix: MauiView.DidUpdateFocus only clears IsFocused if it was the one that set it#34673

Open
Copilot wants to merge 3 commits intomainfrom
copilot/fix-isfocused-reset-behavior
Open

Fix: MauiView.DidUpdateFocus only clears IsFocused if it was the one that set it#34673
Copilot wants to merge 3 commits intomainfrom
copilot/fix-isfocused-reset-behavior

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 26, 2026

Description

MauiView.DidUpdateFocus was unconditionally setting IsFocused = false whenever native iOS focus moved away from the view. This was too aggressive — it would overwrite any IsFocused = true value that had been set by a third-party vendor directly toggling the property.

Changes

Added a _isFocusedSetByUs boolean field to MauiView. In DidUpdateFocus:

  • When this view gains focus (context.NextFocusedView == this), the flag is set to true alongside setting IsFocused = true.
  • When focus moves away, IsFocused is only set to false if _isFocusedSetByUs is true (i.e. this code was the one that set it). The flag is then reset to false.

This ensures we don't obliterate a third-party vendor that is independently toggling IsFocused on the cross-platform view.

Related

Fixes the behavior described at:

if (context.NextFocusedView == this)
{
if (CrossPlatformLayout is IView view)
{
view.IsFocused = true;
}
}
else
{
if (CrossPlatformLayout is IView view)
{
view.IsFocused = false;
}
}

…that set it

Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com>
Agent-Logs-Url: https://github.com/dotnet/maui/sessions/04c4b1bf-7b4f-4069-b9d0-b6dd5094f6b7
Copilot AI requested a review from PureWeen March 26, 2026 14:17
@PureWeen PureWeen added this to the .NET 10 SR6 milestone Mar 26, 2026
@PureWeen PureWeen moved this from Todo to Ready To Review in MAUI SDK Ongoing Mar 26, 2026
@PureWeen PureWeen marked this pull request as ready for review March 26, 2026 15:48
Copilot AI review requested due to automatic review settings March 26, 2026 15:48
@PureWeen
Copy link
Copy Markdown
Member

/azp run maui-pr-uitests, maui-pr-devicetests

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 1 pipeline(s), but failed to run 1 pipeline(s).

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Updates iOS focus propagation in MauiView to avoid clearing the cross-platform IView.IsFocused state when this code path didn’t set it, improving compatibility with third-party code that toggles IsFocused directly.

Changes:

  • Add _isFocusedSetByUs field to track whether MauiView.DidUpdateFocus set IsFocused.
  • Set IsFocused = false on focus loss only when _isFocusedSetByUs indicates MAUI set focus previously.

Comment on lines 812 to 815
_isFocusedSetByUs = false;
if (CrossPlatformLayout is IView view)
{
view.IsFocused = false;
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

In the focus-lost path, _isFocusedSetByUs is reset to false before confirming we can actually update the cross-platform IView. If CrossPlatformLayout is temporarily null/non-IView at this moment (it’s a WeakReference), we won’t set IsFocused = false but we’ll still clear the flag, which can leave IsFocused stuck true with no later opportunity to clear it. Consider only resetting _isFocusedSetByUs after successfully setting view.IsFocused = false (i.e., gate both operations on the same CrossPlatformLayout is IView view check).

Suggested change
_isFocusedSetByUs = false;
if (CrossPlatformLayout is IView view)
{
view.IsFocused = false;
if (CrossPlatformLayout is IView view)
{
view.IsFocused = false;
_isFocusedSetByUs = false;

Copilot uses AI. Check for mistakes.
@PureWeen
Copy link
Copy Markdown
Member

/azp run maui-pr-uitests

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 1 pipeline(s).

@MauiBot
Copy link
Copy Markdown
Collaborator

MauiBot commented Apr 5, 2026

🚦 Gate - Test Before and After Fix

📊 Expand Full Gateb6d44eb · Refine MauiView focus ownership and add regression test

Gate Result: ❌ FAILED

Platform: IOS · Base: main · Merge base: b43bdad1

Test Without Fix (expect FAIL) With Fix (expect PASS)
📱 ContentViewTests (DidUpdateFocusDoesNotResetCustomParentFocusState) Category=ContentView ✅ FAIL — 446s ❌ FAIL — 178s
🔴 Without fix — 📱 ContentViewTests (DidUpdateFocusDoesNotResetCustomParentFocusState): FAIL ✅ · 446s

(truncated to last 15,000 chars)

ific + 572
�[40m�[37mdbug�[39m�[22m�[49m: frame #7: 0x000000018ef6cc78 Foundation`-[NSRunLoop(NSRunLoop) runMode:beforeDate:] + 212
�[40m�[37mdbug�[39m�[22m�[49m: frame #8: 0x000000018efe03a4 Foundation`-[NSRunLoop(NSRunLoop) runUntilDate:] + 100
�[40m�[37mdbug�[39m�[22m�[49m: frame #9: 0x0000000100902f28 mlaunch`xamarin_dyn_objc_msgSend + 160
�[40m�[37mdbug�[39m�[22m�[49m: frame #10: 0x0000000105f7de14
�[40m�[37mdbug�[39m�[22m�[49m: frame #11: 0x00000001062e19e8
�[40m�[37mdbug�[39m�[22m�[49m: frame #12: 0x0000000105f77654
�[40m�[37mdbug�[39m�[22m�[49m: frame #13: 0x0000000105f110b4
�[40m�[37mdbug�[39m�[22m�[49m: frame #14: 0x000000010572cd54
�[40m�[37mdbug�[39m�[22m�[49m: frame #15: 0x0000000102604c04 libcoreclr.dylib`CallDescrWorkerInternal + 132
�[40m�[37mdbug�[39m�[22m�[49m: frame #16: 0x0000000102482d30 libcoreclr.dylib`MethodDescCallSite::CallTargetWorker(unsigned long long const*, unsigned long long*, int) + 836
�[40m�[37mdbug�[39m�[22m�[49m: frame #17: 0x0000000102389350 libcoreclr.dylib`RunMain(MethodDesc*, short, int*, PtrArray**) + 648
�[40m�[37mdbug�[39m�[22m�[49m: frame #18: 0x0000000102389688 libcoreclr.dylib`Assembly::ExecuteMainMethod(PtrArray**, int) + 264
�[40m�[37mdbug�[39m�[22m�[49m: frame #19: 0x00000001023b129c libcoreclr.dylib`CorHost2::ExecuteAssembly(unsigned int, char16_t const*, int, char16_t const**, unsigned int*) + 640
�[40m�[37mdbug�[39m�[22m�[49m: frame #20: 0x0000000102377650 libcoreclr.dylib`coreclr_execute_assembly + 232
�[40m�[37mdbug�[39m�[22m�[49m: frame #21: 0x00000001008fe140 mlaunch`mono_jit_exec + 204
�[40m�[37mdbug�[39m�[22m�[49m: frame #22: 0x0000000100901ecc mlaunch`xamarin_main + 884
�[40m�[37mdbug�[39m�[22m�[49m: frame #23: 0x00000001009031f4 mlaunch`main + 64
�[40m�[37mdbug�[39m�[22m�[49m: frame #24: 0x000000018d512b98 dyld`start + 6076
�[40m�[37mdbug�[39m�[22m�[49m: thread #2
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x000000018d871c34 libsystem_kernel.dylib`mach_msg2_trap + 8
�[40m�[37mdbug�[39m�[22m�[49m: frame #1: 0x000000018d8843a0 libsystem_kernel.dylib`mach_msg2_internal + 76
�[40m�[37mdbug�[39m�[22m�[49m: frame #2: 0x000000018d87a764 libsystem_kernel.dylib`mach_msg_overwrite + 484
�[40m�[37mdbug�[39m�[22m�[49m: frame #3: 0x000000018d871fa8 libsystem_kernel.dylib`mach_msg + 24
�[40m�[37mdbug�[39m�[22m�[49m: frame #4: 0x00000001023752f4 libcoreclr.dylib`MachMessage::Receive(unsigned int) + 80
�[40m�[37mdbug�[39m�[22m�[49m: frame #5: 0x000000010237461c libcoreclr.dylib`SEHExceptionThread(void*) + 164
�[40m�[37mdbug�[39m�[22m�[49m: frame #6: 0x000000018d8b3bc8 libsystem_pthread.dylib`_pthread_start + 136
�[40m�[37mdbug�[39m�[22m�[49m: thread #3, name = '.NET SynchManager'
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x000000018d877d04 libsystem_kernel.dylib`kevent + 8
�[40m�[37mdbug�[39m�[22m�[49m: frame #1: 0x0000000102369304 libcoreclr.dylib`CorUnix::CPalSynchronizationManager::ReadBytesFromProcessPipe(int, unsigned char*, int) + 484
�[40m�[37mdbug�[39m�[22m�[49m: frame #2: 0x00000001023689f0 libcoreclr.dylib`CorUnix::CPalSynchronizationManager::WorkerThread(void*) + 164
�[40m�[37mdbug�[39m�[22m�[49m: frame #3: 0x00000001023720fc libcoreclr.dylib`CorUnix::CPalThread::ThreadEntry(void*) + 364
�[40m�[37mdbug�[39m�[22m�[49m: frame #4: 0x000000018d8b3bc8 libsystem_pthread.dylib`_pthread_start + 136
�[40m�[37mdbug�[39m�[22m�[49m: thread #4, name = '.NET EventPipe'
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x000000018d87a498 libsystem_kernel.dylib`poll + 8
�[40m�[37mdbug�[39m�[22m�[49m: frame #1: 0x0000000102664e90 libcoreclr.dylib`ds_ipc_poll(_DiagnosticsIpcPollHandle*, unsigned long, unsigned int, void (*)(char const*, unsigned int)) + 172
�[40m�[37mdbug�[39m�[22m�[49m: frame #2: 0x0000000102712bb0 libcoreclr.dylib`ds_ipc_stream_factory_get_next_available_stream(void (*)(char const*, unsigned int)) + 756
�[40m�[37mdbug�[39m�[22m�[49m: frame #3: 0x0000000102710a68 libcoreclr.dylib`server_thread(void*) + 372
�[40m�[37mdbug�[39m�[22m�[49m: frame #4: 0x00000001023720fc libcoreclr.dylib`CorUnix::CPalThread::ThreadEntry(void*) + 364
�[40m�[37mdbug�[39m�[22m�[49m: frame #5: 0x000000018d8b3bc8 libsystem_pthread.dylib`_pthread_start + 136
�[40m�[37mdbug�[39m�[22m�[49m: thread #5, name = '.NET DebugPipe'
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x000000018d872678 libsystem_kernel.dylib`__open + 8
�[40m�[37mdbug�[39m�[22m�[49m: frame #1: 0x000000018d87d6a4 libsystem_kernel.dylib`open + 64
�[40m�[37mdbug�[39m�[22m�[49m: frame #2: 0x0000000102665a84 libcoreclr.dylib`TwoWayPipe::WaitForConnection() + 40
�[40m�[37mdbug�[39m�[22m�[49m: frame #3: 0x0000000102660578 libcoreclr.dylib`DbgTransportSession::TransportWorker() + 232
�[40m�[37mdbug�[39m�[22m�[49m: frame #4: 0x000000010265f5c8 libcoreclr.dylib`DbgTransportSession::TransportWorkerStatic(void*) + 40
�[40m�[37mdbug�[39m�[22m�[49m: frame #5: 0x00000001023720fc libcoreclr.dylib`CorUnix::CPalThread::ThreadEntry(void*) + 364
�[40m�[37mdbug�[39m�[22m�[49m: frame #6: 0x000000018d8b3bc8 libsystem_pthread.dylib`_pthread_start + 136
�[40m�[37mdbug�[39m�[22m�[49m: thread #6, name = '.NET Debugger'
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x000000018d8753cc libsystem_kernel.dylib`__psynch_cvwait + 8
�[40m�[37mdbug�[39m�[22m�[49m: frame #1: 0x000000018d8b409c libsystem_pthread.dylib`_pthread_cond_wait + 984
�[40m�[37mdbug�[39m�[22m�[49m: frame #2: 0x0000000102366f6c libcoreclr.dylib`CorUnix::CPalSynchronizationManager::ThreadNativeWait(CorUnix::_ThreadNativeWaitData*, unsigned int, CorUnix::ThreadWakeupReason*, unsigned int*) + 320
�[40m�[37mdbug�[39m�[22m�[49m: frame #3: 0x0000000102366bec libcoreclr.dylib`CorUnix::CPalSynchronizationManager::BlockThread(CorUnix::CPalThread*, unsigned int, bool, bool, CorUnix::ThreadWakeupReason*, unsigned int*) + 380
�[40m�[37mdbug�[39m�[22m�[49m: frame #4: 0x000000010236b0cc libcoreclr.dylib`CorUnix::InternalWaitForMultipleObjectsEx(CorUnix::CPalThread*, unsigned int, void* const*, int, unsigned int, int, int) + 1600
�[40m�[37mdbug�[39m�[22m�[49m: frame #5: 0x000000010265dda8 libcoreclr.dylib`DebuggerRCThread::MainLoop() + 228
�[40m�[37mdbug�[39m�[22m�[49m: frame #6: 0x000000010265dc70 libcoreclr.dylib`DebuggerRCThread::ThreadProc() + 256
�[40m�[37mdbug�[39m�[22m�[49m: frame #7: 0x000000010265da24 libcoreclr.dylib`DebuggerRCThread::ThreadProcStatic(void*) + 56
�[40m�[37mdbug�[39m�[22m�[49m: frame #8: 0x00000001023720fc libcoreclr.dylib`CorUnix::CPalThread::ThreadEntry(void*) + 364
�[40m�[37mdbug�[39m�[22m�[49m: frame #9: 0x000000018d8b3bc8 libsystem_pthread.dylib`_pthread_start + 136
�[40m�[37mdbug�[39m�[22m�[49m: thread #7
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x000000018d8753cc libsystem_kernel.dylib`__psynch_cvwait + 8
�[40m�[37mdbug�[39m�[22m�[49m: frame #1: 0x000000018d8b409c libsystem_pthread.dylib`_pthread_cond_wait + 984
�[40m�[37mdbug�[39m�[22m�[49m: frame #2: 0x0000000102366f6c libcoreclr.dylib`CorUnix::CPalSynchronizationManager::ThreadNativeWait(CorUnix::_ThreadNativeWaitData*, unsigned int, CorUnix::ThreadWakeupReason*, unsigned int*) + 320
�[40m�[37mdbug�[39m�[22m�[49m: frame #3: 0x0000000102366bec libcoreclr.dylib`CorUnix::CPalSynchronizationManager::BlockThread(CorUnix::CPalThread*, unsigned int, bool, bool, CorUnix::ThreadWakeupReason*, unsigned int*) + 380
�[40m�[37mdbug�[39m�[22m�[49m: frame #4: 0x000000010236b0cc libcoreclr.dylib`CorUnix::InternalWaitForMultipleObjectsEx(CorUnix::CPalThread*, unsigned int, void* const*, int, unsigned int, int, int) + 1600
�[40m�[37mdbug�[39m�[22m�[49m: frame #5: 0x00000001024b8078 libcoreclr.dylib`FinalizerThread::WaitForFinalizerEvent(CLREvent*) + 240
�[40m�[37mdbug�[39m�[22m�[49m: frame #6: 0x00000001024b81d8 libcoreclr.dylib`FinalizerThread::FinalizerThreadWorker(void*) + 264
�[40m�[37mdbug�[39m�[22m�[49m: frame #7: 0x0000000102455fa8 libcoreclr.dylib`ManagedThreadBase_DispatchOuter(ManagedThreadCallState*) + 248
�[40m�[37mdbug�[39m�[22m�[49m: frame #8: 0x000000010245648c libcoreclr.dylib`ManagedThreadBase::FinalizerBase(void (*)(void*)) + 36
�[40m�[37mdbug�[39m�[22m�[49m: frame #9: 0x00000001024b8350 libcoreclr.dylib`FinalizerThread::FinalizerThreadStart(void*) + 88
�[40m�[37mdbug�[39m�[22m�[49m: frame #10: 0x00000001023720fc libcoreclr.dylib`CorUnix::CPalThread::ThreadEntry(void*) + 364
�[40m�[37mdbug�[39m�[22m�[49m: frame #11: 0x000000018d8b3bc8 libsystem_pthread.dylib`_pthread_start + 136
�[40m�[37mdbug�[39m�[22m�[49m: thread #8, name = '.NET SigHandler'
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x000000018d8727dc libsystem_kernel.dylib`read + 8
�[40m�[37mdbug�[39m�[22m�[49m: frame #1: 0x0000000100a90e98 libSystem.Native.dylib`SignalHandlerLoop + 96
�[40m�[37mdbug�[39m�[22m�[49m: frame #2: 0x000000018d8b3bc8 libsystem_pthread.dylib`_pthread_start + 136
�[40m�[37mdbug�[39m�[22m�[49m: thread #9
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x000000018d877d04 libsystem_kernel.dylib`kevent + 8
�[40m�[37mdbug�[39m�[22m�[49m: frame #1: 0x0000000100a8f4a4 libSystem.Native.dylib`SystemNative_WaitForSocketEvents + 80
�[40m�[37mdbug�[39m�[22m�[49m: frame #2: 0x000000010608ca9c
�[40m�[37mdbug�[39m�[22m�[49m: frame #3: 0x000000010608c6f4
�[40m�[37mdbug�[39m�[22m�[49m: frame #4: 0x000000010608c43c
�[40m�[37mdbug�[39m�[22m�[49m: frame #5: 0x0000000105f71388
�[40m�[37mdbug�[39m�[22m�[49m: frame #6: 0x0000000105f71218
�[40m�[37mdbug�[39m�[22m�[49m: frame #7: 0x0000000105f71140
�[40m�[37mdbug�[39m�[22m�[49m: frame #8: 0x0000000102604c04 libcoreclr.dylib`CallDescrWorkerInternal + 132
�[40m�[37mdbug�[39m�[22m�[49m: frame #9: 0x0000000102482988 libcoreclr.dylib`DispatchCallSimple(unsigned long*, unsigned int, unsigned long long, unsigned int) + 268
�[40m�[37mdbug�[39m�[22m�[49m: frame #10: 0x0000000102494c6c libcoreclr.dylib`ThreadNative::KickOffThread_Worker(void*) + 148
�[40m�[37mdbug�[39m�[22m�[49m: frame #11: 0x0000000102455fa8 libcoreclr.dylib`ManagedThreadBase_DispatchOuter(ManagedThreadCallState*) + 248
�[40m�[37mdbug�[39m�[22m�[49m: frame #12: 0x000000010245645c libcoreclr.dylib`ManagedThreadBase::KickOff(void (*)(void*), void*) + 32
�[40m�[37mdbug�[39m�[22m�[49m: frame #13: 0x0000000102494d44 libcoreclr.dylib`ThreadNative::KickOffThread(void*) + 172
�[40m�[37mdbug�[39m�[22m�[49m: frame #14: 0x00000001023720fc libcoreclr.dylib`CorUnix::CPalThread::ThreadEntry(void*) + 364
�[40m�[37mdbug�[39m�[22m�[49m: frame #15: 0x000000018d8b3bc8 libsystem_pthread.dylib`_pthread_start + 136
�[40m�[37mdbug�[39m�[22m�[49m: thread #10
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x000000018d871c34 libsystem_kernel.dylib`mach_msg2_trap + 8
�[40m�[37mdbug�[39m�[22m�[49m: frame #1: 0x000000018d8843a0 libsystem_kernel.dylib`mach_msg2_internal + 76
�[40m�[37mdbug�[39m�[22m�[49m: frame #2: 0x000000018d87a764 libsystem_kernel.dylib`mach_msg_overwrite + 484
�[40m�[37mdbug�[39m�[22m�[49m: frame #3: 0x000000018d871fa8 libsystem_kernel.dylib`mach_msg + 24
�[40m�[37mdbug�[39m�[22m�[49m: frame #4: 0x000000018d99ec0c CoreFoundation`__CFRunLoopServiceMachPort + 160
�[40m�[37mdbug�[39m�[22m�[49m: frame #5: 0x000000018d99d528 CoreFoundation`__CFRunLoopRun + 1208
�[40m�[37mdbug�[39m�[22m�[49m: frame #6: 0x000000018d99c9e8 CoreFoundation`CFRunLoopRunSpecific + 572
�[40m�[37mdbug�[39m�[22m�[49m: frame #7: 0x000000018ef6cc78 Foundation`-[NSRunLoop(NSRunLoop) runMode:beforeDate:] + 212
�[40m�[37mdbug�[39m�[22m�[49m: frame #8: 0x0000000100902f28 mlaunch`xamarin_dyn_objc_msgSend + 160
�[40m�[37mdbug�[39m�[22m�[49m: frame #9: 0x00000001062d743c
�[40m�[37mdbug�[39m�[22m�[49m: frame #10: 0x00000001062d7300
�[40m�[37mdbug�[39m�[22m�[49m: frame #11: 0x00000001062d7134
�[40m�[37mdbug�[39m�[22m�[49m: frame #12: 0x00000001062d4138
�[40m�[37mdbug�[39m�[22m�[49m: frame #13: 0x0000000105f71330
�[40m�[37mdbug�[39m�[22m�[49m: frame #14: 0x0000000105f71218
�[40m�[37mdbug�[39m�[22m�[49m: frame #15: 0x0000000105f71140
�[40m�[37mdbug�[39m�[22m�[49m: frame #16: 0x0000000102604c04 libcoreclr.dylib`CallDescrWorkerInternal + 132
�[40m�[37mdbug�[39m�[22m�[49m: frame #17: 0x0000000102482988 libcoreclr.dylib`DispatchCallSimple(unsigned long*, unsigned int, unsigned long long, unsigned int) + 268
�[40m�[37mdbug�[39m�[22m�[49m: frame #18: 0x0000000102494c6c libcoreclr.dylib`ThreadNative::KickOffThread_Worker(void*) + 148
�[40m�[37mdbug�[39m�[22m�[49m: frame #19: 0x0000000102455fa8 libcoreclr.dylib`ManagedThreadBase_DispatchOuter(ManagedThreadCallState*) + 248
�[40m�[37mdbug�[39m�[22m�[49m: frame #20: 0x000000010245645c libcoreclr.dylib`ManagedThreadBase::KickOff(void (*)(void*), void*) + 32
�[40m�[37mdbug�[39m�[22m�[49m: frame #21: 0x0000000102494d44 libcoreclr.dylib`ThreadNative::KickOffThread(void*) + 172
�[40m�[37mdbug�[39m�[22m�[49m: frame #22: 0x00000001023720fc libcoreclr.dylib`CorUnix::CPalThread::ThreadEntry(void*) + 364
�[40m�[37mdbug�[39m�[22m�[49m: frame #23: 0x000000018d8b3bc8 libsystem_pthread.dylib`_pthread_start + 136
�[40m�[37mdbug�[39m�[22m�[49m: thread #11
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x000000018d8738b0 libsystem_kernel.dylib`__workq_kernreturn + 8
�[40m�[37mdbug�[39m�[22m�[49m: thread #12, name = 'com.apple.CFSocket.private'
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x000000018d87cc2c libsystem_kernel.dylib`__select + 8
�[40m�[37mdbug�[39m�[22m�[49m: frame #1: 0x000000018d9c4a80 CoreFoundation`__CFSocketManager + 704
�[40m�[37mdbug�[39m�[22m�[49m: frame #2: 0x000000018d8b3bc8 libsystem_pthread.dylib`_pthread_start + 136
�[40m�[37mdbug�[39m�[22m�[49m: thread #13
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x000000018d8738b0 libsystem_kernel.dylib`__workq_kernreturn + 8
�[40m�[37mdbug�[39m�[22m�[49m: thread #14
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x0000000000000000
�[40m�[37mdbug�[39m�[22m�[49m: (lldb) detach
�[40m�[37mdbug�[39m�[22m�[49m: Process 9323 detached
�[40m�[37mdbug�[39m�[22m�[49m: (lldb) quit
�[40m�[37mdbug�[39m�[22m�[49m: 9323 Execution timed out after 60 seconds and the process was killed.
�[40m�[37mdbug�[39m�[22m�[49m: Process mlaunch exited with 137
�[40m�[37mdbug�[39m�[22m�[49m: Failed to list crash reports from device.
�[40m�[37mdbug�[39m�[22m�[49m: Test run started but crashed and no test results were reported
�[40m�[37mdbug�[39m�[22m�[49m: No crash reports, waiting 30 seconds for the crash report service...
�[41m�[30mfail�[39m�[22m�[49m: Application test run crashed
      Failed to launch the application, please try again. If the problem persists, try rebooting MacOS
�[40m�[32minfo�[39m�[22m�[49m: Uninstalling the application 'com.microsoft.maui.controls.devicetests' from 'iPhone 11 Pro'
�[40m�[37mdbug�[39m�[22m�[49m: 
�[40m�[37mdbug�[39m�[22m�[49m: Running /Applications/Xcode_26.1.1.app/Contents/Developer/usr/bin/simctl
�[40m�[37mdbug�[39m�[22m�[49m: An error was encountered processing the command (domain=com.apple.CoreSimulator.SimError, code=405):
�[40m�[37mdbug�[39m�[22m�[49m: Unable to lookup in current state: Shutdown
�[40m�[37mdbug�[39m�[22m�[49m: Process simctl exited with 149
�[41m�[30mfail�[39m�[22m�[49m: Failed to uninstall the app bundle! Check logs for more details!
XHarness exit code: 83 (APP_LAUNCH_FAILURE)
  Passed: 0
  Failed: 0
  Tests completed with exit code: 83

🟢 With fix — 📱 ContentViewTests (DidUpdateFocusDoesNotResetCustomParentFocusState): FAIL ❌ · 178s
  Determining projects to restore...
  All projects are up-to-date for restore.
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13801949
  Graphics -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Graphics/Release/net10.0-ios26.0/Microsoft.Maui.Graphics.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13801949
  Essentials -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Essentials/Release/net10.0-ios26.0/Microsoft.Maui.Essentials.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13801949
  Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Core/Release/net10.0-ios26.0/Microsoft.Maui.dll
  Controls.BindingSourceGen -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.BindingSourceGen/Release/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13801949
  TestUtils.DeviceTests -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/TestUtils.DeviceTests/Release/net10.0-ios/Microsoft.Maui.TestUtils.DeviceTests.dll
  Controls.Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Core/Release/net10.0-ios26.0/Microsoft.Maui.Controls.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13801949
  Controls.Xaml -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Xaml/Release/net10.0-ios26.0/Microsoft.Maui.Controls.Xaml.dll
  TestUtils.DeviceTests.Runners -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/TestUtils.DeviceTests.Runners/Release/net10.0-ios/Microsoft.Maui.TestUtils.DeviceTests.Runners.dll
  Core.DeviceTests.Shared -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Core.DeviceTests.Shared/Release/net10.0-ios/Microsoft.Maui.DeviceTests.Shared.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13801949
  Maps -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Maps/Release/net10.0-ios26.0/Microsoft.Maui.Maps.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13801949
  Controls.Maps -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Maps/Release/net10.0-ios26.0/Microsoft.Maui.Controls.Maps.dll
  TestUtils.DeviceTests.Runners.SourceGen -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/TestUtils.DeviceTests.Runners.SourceGen/Release/netstandard2.0/Microsoft.Maui.TestUtils.DeviceTests.Runners.SourceGen.dll
  Detected signing identity:
    Code Signing Key: "" (-)
    Provisioning Profile: "" () - no entitlements
    Bundle Id: com.microsoft.maui.controls.devicetests
    App Id: com.microsoft.maui.controls.devicetests
  Controls.DeviceTests -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.DeviceTests/Release/net10.0-ios/iossimulator-arm64/Microsoft.Maui.Controls.DeviceTests.dll
  Optimizing assemblies for size may change the behavior of the app. Be sure to test after publishing. See: https://aka.ms/dotnet-illink
  Optimizing assemblies for size. This process might take a while.
  IL stripping assemblies

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:02:39.90
[11.0.0-prerelease.26107.1+bfbac237157e59cdbd19334325b2af80bd6e9828] XHarness command issued: apple test --app artifacts/bin/Controls.DeviceTests/Release/net10.0-ios/iossimulator-arm64/Microsoft.Maui.Controls.DeviceTests.app --target ios-simulator-64_18.6 --device B1BB622D-EC7D-43A7-B089-BE4BDFBA7C35 -o artifacts/log --timeout 01:00:00 -v --set-env=TestFilter=Category=ContentView
�[40m�[32minfo�[39m�[22m�[49m: Preparing run for ios-simulator-64_18.6 targeting B1BB622D-EC7D-43A7-B089-BE4BDFBA7C35
�[40m�[32minfo�[39m�[22m�[49m: Looking for available ios-simulator-64_18.6 simulators..
�[40m�[37mdbug�[39m�[22m�[49m: Looking for available ios-simulator-64_18.6 simulators. Storing logs into list-ios-simulator-64_18.6-20260410_104237.log
�[40m�[32minfo�[39m�[22m�[49m: Found simulator device 'iPhone 11 Pro'
�[40m�[32minfo�[39m�[22m�[49m: Getting app bundle information from '/Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.DeviceTests/Release/net10.0-ios/iossimulator-arm64/Microsoft.Maui.Controls.DeviceTests.app'..
�[40m�[37mdbug�[39m�[22m�[49m: 
�[40m�[37mdbug�[39m�[22m�[49m: Running /usr/libexec/PlistBuddy
�[40m�[37mdbug�[39m�[22m�[49m: Process PlistBuddy exited with 0
�[40m�[37mdbug�[39m�[22m�[49m: 
�[40m�[37mdbug�[39m�[22m�[49m: Running /usr/libexec/PlistBuddy
�[40m�[37mdbug�[39m�[22m�[49m: Process PlistBuddy exited with 0
�[40m�[37mdbug�[39m�[22m�[49m: 
�[40m�[37mdbug�[39m�[22m�[49m: Running /usr/libexec/PlistBuddy
�[40m�[37mdbug�[39m�[22m�[49m: Process PlistBuddy exited with 0
�[40m�[37mdbug�[39m�[22m�[49m: 
�[40m�[37mdbug�[39m�[22m�[49m: Running /usr/libexec/PlistBuddy
�[40m�[37mdbug�[39m�[22m�[49m: Process PlistBuddy exited with 0
�[40m�[32minfo�[39m�[22m�[49m: Uninstalling any previous instance of 'com.microsoft.maui.controls.devicetests' from 'iPhone 11 Pro'
�[40m�[37mdbug�[39m�[22m�[49m: 
�[40m�[37mdbug�[39m�[22m�[49m: Running /Applications/Xcode_26.1.1.app/Contents/Developer/usr/bin/simctl
�[40m�[37mdbug�[39m�[22m�[49m: Process simctl exited with 0
�[40m�[32minfo�[39m�[22m�[49m: Application 'com.microsoft.maui.controls.devicetests' was uninstalled successfully
�[40m�[32minfo�[39m�[22m�[49m: Installing application 'Microsoft.Maui.Controls.DeviceTests' on 'iPhone 11 Pro'
�[40m�[37mdbug�[39m�[22m�[49m: Installing '/Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.DeviceTests/Release/net10.0-ios/iossimulator-arm64/Microsoft.Maui.Controls.DeviceTests.app' to 'iPhone 11 Pro' (143.69 MB)
�[40m�[37mdbug�[39m�[22m�[49m: 
�[40m�[37mdbug�[39m�[22m�[49m: Running /Users/cloudtest/.nuget/packages/microsoft.dotnet.xharness.cli/11.0.0-prerelease.26107.1/tools/net10.0/any/../../../runtimes/any/native/mlaunch/bin/mlaunch
�[40m�[37mdbug�[39m�[22m�[49m: Using Xcode 26.1.1 found in /Applications/Xcode_26.1.1.app
�[40m�[37mdbug�[39m�[22m�[49m: xcrun simctl list --json --json-output /tmp/tmpLDgvv1.tmp
�[40m�[37mdbug�[39m�[22m�[49m: Xamarin.Hosting: No need to boot (already booted): iPhone 11 Pro
�[40m�[37mdbug�[39m�[22m�[49m: Xamarin.Hosting: Installing on iPhone 11 Pro (B1BB622D-EC7D-43A7-B089-BE4BDFBA7C35) by executing 'xcrun simctl install B1BB622D-EC7D-43A7-B089-BE4BDFBA7C35 /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.DeviceTests/Release/net10.0-ios/iossimulator-arm64/Microsoft.Maui.Controls.DeviceTests.app'
�[40m�[37mdbug�[39m�[22m�[49m: Xamarin.Hosting: The bundle id com.microsoft.maui.controls.devicetests was successfully installed.
�[40m�[37mdbug�[39m�[22m�[49m: Process mlaunch exited with 0
�[40m�[32minfo�[39m�[22m�[49m: Application 'Microsoft.Maui.Controls.DeviceTests' was installed successfully on 'iPhone 11 Pro'
�[40m�[32minfo�[39m�[22m�[49m: Starting test run for com.microsoft.maui.controls.devicetests..
�[40m�[37mdbug�[39m�[22m�[49m: *** Executing 'Microsoft.Maui.Controls.DeviceTests' on ios-simulator-64_18.6 'iPhone 11 Pro' ***
�[40m�[37mdbug�[39m�[22m�[49m: Test log server listening on: 0.0.0.0:54520
�[40m�[37mdbug�[39m�[22m�[49m: System log for the 'iPhone 11 Pro' simulator is: /Users/cloudtest/Library/Logs/CoreSimulator/B1BB622D-EC7D-43A7-B089-BE4BDFBA7C35/system.log
�[40m�[37mdbug�[39m�[22m�[49m: Simulator 'iPhone 11 Pro' is already booted
�[40m�[37mdbug�[39m�[22m�[49m: Scanning log stream for Microsoft.Maui.Controls.DeviceTests into '/Users/cloudtest/vss/_work/1/s/artifacts/log/Microsoft.Maui.Controls.DeviceTests.log'..
�[40m�[37mdbug�[39m�[22m�[49m: 
�[40m�[37mdbug�[39m�[22m�[49m: Running /Applications/Xcode_26.1.1.app/Contents/Developer/usr/bin/simctl
�[40m�[37mdbug�[39m�[22m�[49m: Launching the app
�[40m�[37mdbug�[39m�[22m�[49m: 
�[40m�[37mdbug�[39m�[22m�[49m: Running /Users/cloudtest/.nuget/packages/microsoft.dotnet.xharness.cli/11.0.0-prerelease.26107.1/tools/net10.0/any/../../../runtimes/any/native/mlaunch/bin/mlaunch
�[40m�[37mdbug�[39m�[22m�[49m: Connection from 127.0.0.1:54528 saving logs to /Users/cloudtest/vss/_work/1/s/artifacts/log/test-ios-simulator-64_18.6-20260410_104242.log
�[40m�[37mdbug�[39m�[22m�[49m: Tests have finished executing
�[40m�[37mdbug�[39m�[22m�[49m: Process mlaunch exited with 0
�[40m�[37mdbug�[39m�[22m�[49m: Test run completed
�[40m�[37mdbug�[39m�[22m�[49m: 
�[40m�[37mdbug�[39m�[22m�[49m: Running /bin/bash
�[40m�[37mdbug�[39m�[22m�[49m: Process simctl exited with 137
�[40m�[37mdbug�[39m�[22m�[49m: cp: /Users/cloudtest/Library/Developer/CoreSimulator/Devices/B1BB622D-EC7D-43A7-B089-BE4BDFBA7C35/data/Containers/Data/Application/95D125AC-D065-455F-8310-0309AC4A993C/Documents/test-results.xml: No such file or directory
�[40m�[37mdbug�[39m�[22m�[49m: Process bash exited with 1
�[40m�[37mdbug�[39m�[22m�[49m: Test run succeeded
�[40m�[37mdbug�[39m�[22m�[49m: No crash reports, waiting 0 seconds for the crash report service...
�[40m�[32minfo�[39m�[22m�[49m: Application finished the test run successfully
�[40m�[32minfo�[39m�[22m�[49m: Tests run: 3 Passed: 3 Inconclusive: 0 Failed: 0 Ignored: 0
�[40m�[32minfo�[39m�[22m�[49m: Uninstalling the application 'com.microsoft.maui.controls.devicetests' from 'iPhone 11 Pro'
�[40m�[37mdbug�[39m�[22m�[49m: 
�[40m�[37mdbug�[39m�[22m�[49m: Running /Applications/Xcode_26.1.1.app/Contents/Developer/usr/bin/simctl
�[40m�[37mdbug�[39m�[22m�[49m: Process simctl exited with 0
�[40m�[32minfo�[39m�[22m�[49m: Application 'com.microsoft.maui.controls.devicetests' was uninstalled successfully
XHarness exit code: 0
  Passed: 2
  Failed: 3
  Tests completed successfully

⚠️ Issues found
  • ContentViewTests (DidUpdateFocusDoesNotResetCustomParentFocusState) FAILED with fix (should pass)
    • Device tests: 3 of 5 failed
📁 Fix files reverted (2 files)
  • eng/pipelines/ci-copilot.yml
  • src/Core/src/Platform/iOS/MauiView.cs

@MauiBot
Copy link
Copy Markdown
Collaborator

MauiBot commented Apr 5, 2026

🤖 AI Summary

📊 Expand Full Reviewb6d44eb · Refine MauiView focus ownership and add regression test
🔍 Pre-Flight — Context & Validation

Issue: No linked GitHub issue — fix described inline in PR body (references MauiView.cs:799-812)
PR: #34673 - Fix: MauiView.DidUpdateFocus only clears IsFocused if it was the one that set it
Platforms Affected: iOS / macCatalyst
Files Changed: 1 implementation (src/Core/src/Platform/iOS/MauiView.cs), 2 test files (ContentViewTests.cs, ContentViewTests.iOS.cs)

Key Findings

  • MauiView.DidUpdateFocus was unconditionally clearing IsFocused = false when native iOS focus moved away, even if a third-party vendor had set IsFocused = true directly.
  • PR fix introduces _isFocusedSetByUs bool field: set true when THIS code path sets IsFocused = true, only clear if _isFocusedSetByUs is true on focus loss.
  • Gate FAILED: Test fails in BOTH without-fix (446s — expected) AND with-fix (178s — unexpected) conditions. The with-fix failure suggests the test itself is broken.
  • Test issue (github-actions evaluator): TestFocusUpdateContext uses NSObjectFlag.Empty and new UIFocusAnimationCoordinator() — both may crash on iOS device. Also, PreviouslyFocusedView = new UIView() instead of platformView means the test doesn't actually validate the right path.
  • Fundamental logic concern (kubaflo code review): If a vendor sets IsFocused = true and then focus moves to an ENTIRELY different control outside the ContentView, _isFocusedSetByUs is still false → MAUI never clears IsFocused. It would be permanently true.
  • Open (outdated) review thread from copilot-pull-request-reviewer: _isFocusedSetByUs = false should be inside the null-check — but current code already has this fixed in latest commit.
  • No linked GitHub issue. PR milestone: .NET 10 SR6. PR author: Copilot (3 commits, +118/-4 lines).

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #34673 _isFocusedSetByUs boolean flag — only clears focus that this code set ❌ FAILED (Gate) MauiView.cs IsFocused can get stuck true when vendor sets it; test itself may be broken

🔧 Fix — Analysis & Comparison

Fix Candidates

# Source Approach Test Result Files Changed Notes
1 try-fix (claude-opus-4.6) Stateless descendant-aware check: next.IsDescendantOfView(this) — no new field; also fixes test timing (wait for Handler) ✅ PASS MauiView.cs, ContentViewTests.iOS.cs Fixes "stuck IsFocused" bug in PR's approach
PR PR #34673 _isFocusedSetByUs boolean flag — only clears IsFocused set by this code ❌ FAILED (Gate) MauiView.cs IsFocused can get stuck true; test broken

(Attempts 2-4 in progress...)


@MauiBot MauiBot added s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) labels Apr 5, 2026
github-actions bot and others added 2 commits April 9, 2026 12:51
Replace the _isFocusedSetByUs boolean with a stateless check using
context.PreviouslyFocusedView == this. This eliminates the ordering
bug where the flag could be cleared before the IView null-check,
leaving IsFocused stuck at true. The iOS focus context already
provides PreviouslyFocusedView, making extra state unnecessary.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Only claim focus ownership when MauiView actually toggles IsFocused
from false to true, and document why we do not rely on
PreviouslyFocusedView for composite controls. Add an iOS/MacCatalyst
ContentView device test that reproduces the vendor regression and
fails without the fix.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 34673

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 34673"

@github-actions
Copy link
Copy Markdown
Contributor

🧪 PR Test Evaluation

Overall Verdict: ⚠️ Tests need improvement

The device test covers the right scenario conceptually, but has a critical gap: as written, it would pass even without the fix because PreviouslyFocusedView is set to new UIView() rather than platformView. Additionally, the complementary "normal focus" path is not tested.

👍 / 👎 — Was this evaluation helpful? React to let us know!

📊 Expand Full Evaluation

PR Test Evaluation Report

PR: #34673 — Fix: MauiView.DidUpdateFocus only clears IsFocused if it was the one that set it
Test files evaluated: 2 (ContentViewTests.iOS.cs, ContentViewTests.cs)
Fix files: 1 (src/Core/src/Platform/iOS/MauiView.cs)


Overall Verdict

⚠️ Tests need improvement

The added test DidUpdateFocusDoesNotResetCustomParentFocusState exercises the right scenario conceptually, but uses new UIView() (not platformView) as PreviouslyFocusedView, meaning the pre-fix code path (PreviouslyFocusedView == this) would also be false — the test passes with and without the fix and is therefore not a true regression guard.


1. Fix Coverage — ⚠️ Concern

The test sets IsFocused = true externally, then calls DidUpdateFocus, and asserts IsFocused remains true. However, the TestFocusUpdateContext is constructed as:

var context = new TestFocusUpdateContext(templatedEntry.ToPlatform(), new UIView());
//                                       ↑ NextFocusedView              ↑ PreviouslyFocusedView

PreviouslyFocusedView is new UIView()not platformView. The old (pre-fix) guard that would incorrectly clear focus was:

else if (context.PreviouslyFocusedView == this)   // old code

Since new UIView() != platformView, this condition is false with the old code too. The test passes both before and after the fix, making it insufficient as a regression guard.

To actually reproduce the bug, the context should use platformView as the previous focused view:

// Should be:
var context = new TestFocusUpdateContext(templatedEntry.ToPlatform(), platformView);

With that change, the old code would incorrectly clear IsFocused (test fails → bug reproduced), while the new code would correctly leave it alone (test passes → fix verified).


2. Edge Cases & Gaps — ⚠️ Concern

Covered:

  • Vendor-set IsFocused = true is not cleared by DidUpdateFocus (conceptually, once §1 is fixed)

Missing:

  • Correct regression test: PreviouslyFocusedView must be platformView to trigger the pre-fix bug path (see §1)
  • Positive path for _isFocusedSetByUs: No test verifies that when MauiView itself sets focus via DidUpdateFocus (context.NextFocusedView == this_isFocusedSetByUs = true), focus is properly cleared when focus then moves away. This validates the flag-based ownership mechanism end-to-end.
  • Flag reset consistency: No test confirms _isFocusedSetByUs is properly reset to false after clearing, preventing bugs across multiple focus cycles on the same view.

3. Test Type Appropriateness — ✅ Pass

Current: Device Test
Recommendation: Same — Device test is the right choice.

DidUpdateFocus is a UIKit API (UIFocusEnvironment) requiring a real platform context, native UIView hierarchy, and UIFocusAnimationCoordinator. Unit testing this would require impractical mocking of UIKit internals. CreateHandlerAndAddToWindow correctly sets up the necessary window context.


4. Convention Compliance — ✅ Pass

  • [Fact] attribute used ✅
  • [Category(TestCategory.ContentView)] on class ✅
  • CreateHandlerAndAddToWindow pattern used ✅
  • AssertEventually used for async waiting ✅
  • No obsolete APIs, no Task.Delay/Thread.Sleep
  • Helper types properly co-located in iOS partial class ✅

5. Flakiness Risk — ✅ Low

  • AssertEventually(() => templatedEntry is not null) correctly waits for async template inflation ✅
  • No timing-sensitive assertions ✅
  • No arbitrary delays ✅

6. Duplicate Coverage — ✅ No duplicates

The existing Issue28945.cs tests focus propagation to MauiView (Tab key → Focused event fired). This PR's test covers focus ownership tracking in DidUpdateFocus — a distinct concern. No redundancy.


7. Platform Scope — ⚠️ Concern

The fix is in MauiView.cs (under Platform/iOS/) which compiles for both iOS and MacCatalyst. The test in ContentViewTests.iOS.cs also compiles for both platforms implicitly. However, TestFocusUpdateContext inherits UIFocusUpdateContext using NSObjectFlag.Empty — a low-level construct that may behave differently on MacCatalyst. An explicit note or annotation would clarify the MacCatalyst coverage intent.


8. Assertion Quality — ✅ Pass

Assert.True(contentView.IsFocused);        // directly checks the affected property
Assert.Equal(0, parentUnfocusedCount);     // checks the Unfocused event was not raised

Both assertions target the precise observable symptoms of the bug. The quality is good — the issue is whether the test triggers the bug at all (§1), not whether the assertions are meaningful.


9. Fix-Test Alignment — ✅ Pass

Test file (ContentViewTests.iOS.cs) matches the fix file (MauiView.cs, iOS platform). The FocusableContentView-with-templated-Entry scenario directly reflects the "composite controls that mirror a focused child Entry to the parent IsFocused state" described in the PR and code comments.


Recommendations

  1. 🔴 Fix the regression test's PreviouslyFocusedView — Change new UIView() to platformView in the TestFocusUpdateContext call. Without this, the test does not distinguish old code from new:

    // Current (does NOT catch the bug — passes with old and new code):
    var context = new TestFocusUpdateContext(templatedEntry.ToPlatform(), new UIView());
    
    // Fixed (correctly catches the bug with old code, passes with fix):
    var context = new TestFocusUpdateContext(templatedEntry.ToPlatform(), platformView);
  2. 🟡 Add a test for the positive _isFocusedSetByUs path — Verify that when MauiView gains focus via DidUpdateFocus (NextFocusedView == this), and focus then moves away, IsFocused is correctly cleared. This exercises the flag's other branch and validates round-trip ownership.

  3. 🟢 (Optional) Confirm MacCatalyst CI coverage — Add a comment or annotation confirming the test is expected to run on MacCatalyst, since MauiView.cs compiles for both platforms.

Warning

⚠️ Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • dc.services.visualstudio.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "dc.services.visualstudio.com"

See Network Configuration for more information.

🧪 Test evaluation by Evaluate PR Tests

@PureWeen
Copy link
Copy Markdown
Member

/azp run maui-pr-uitests, maui-pr-devicetests

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 2 pipeline(s).

@kubaflo
Copy link
Copy Markdown
Contributor

kubaflo commented Apr 10, 2026

Code Review — PR #34673

Independent Assessment

What this changes: Adds a _isFocusedSetByUs boolean field to MauiView (iOS/macCatalyst) that tracks whether DidUpdateFocus was the code path that set IsFocused = true. When focus moves away, IsFocused is only cleared if _isFocusedSetByUs is true. This prevents overwriting IsFocused values set externally (e.g., by a third-party vendor control).

Inferred motivation: Composite vendor controls (e.g., Syncfusion) mirror an inner Entry's focus state to the parent ContentView's IsFocused. UIKit's DidUpdateFocus fires on all ancestors, and since NextFocusedView points to the Entry (not the ContentView), the original code's unconditional else branch cleared the vendor-set IsFocused = false.

Reconciliation with PR Narrative

Agreement: My analysis matches the PR description. The fix correctly identifies that the original code was too aggressive in clearing IsFocused.

Findings

🐛 Bug risk — IsFocused can get stuck at true when vendor sets it

Scenario:

  1. Vendor sets contentView.IsFocused = true when inner Entry gains focus
  2. DidUpdateFocus fires — NextFocusedView is the Entry, not ContentView → else branch. _isFocusedSetByUs is false → skip clearing. ✅ correct so far.
  3. User taps a completely different control outside the ContentView
  4. DidUpdateFocus fires on ContentView — NextFocusedView is the other control → else branch. _isFocusedSetByUs is still falseskip clearing again
  5. ContentView.IsFocused stays true permanentlyUnfocused event never fires, VisualState stays Focused

This puts the burden on the vendor to also clear IsFocused = false when their inner control loses focus. If the vendor only handles the "gained" path, IsFocused will never be cleared by MAUI.

Possible mitigation: Instead of a simple boolean, consider checking whether the NextFocusedView is a descendant of this view. If focus moves to a descendant, keep IsFocused. If focus moves completely outside, clear it regardless:

else
{
    bool focusedDescendant = context.NextFocusedView is UIView next && next.IsDescendantOfView(this);
    if (!focusedDescendant && CrossPlatformLayout is IView view && view.IsFocused)
    {
        view.IsFocused = false;
    }
}

This would handle the vendor scenario (descendant gets focus → keep parent IsFocused) while still correctly clearing focus when it leaves entirely.

⚠️ Warning — Missing test for normal focus lifecycle

There is no test for the complementary case: DidUpdateFocus gains focus → _isFocusedSetByUs = true → focus moves away → IsFocused correctly cleared to false. This is the most common path and should be tested to ensure the tracking flag works correctly in both directions.

⚠️ Warning — No linked GitHub issue

The PR references a line in source code but no GitHub issue. This makes it hard to verify the actual user scenario, reproduction steps, and whether affected vendors have confirmed the fix. Is there an internal or partner-reported issue this addresses?

💡 Suggestion — TestFocusUpdateContext subclasses UIKit class via NSObjectFlag.Empty

UIFocusUpdateContext is a UIKit class not designed for public subclassing. Using NSObjectFlag.Empty bypasses Objective-C initialization. This works in tests today but is fragile — UIKit could add required initialization in a future iOS version. Consider noting this as a known test fragility, or using an alternative approach (e.g., mocking via a protocol wrapper).

💡 Suggestion — #if IOS || MACCATALYST in shared test setup

The SetupBuilder method in ContentViewTests.cs uses #if IOS || MACCATALYST to register FocusableContentViewHandler. Per UI test guidelines, platform-specific logic should be in extension methods, but this is in shared test infrastructure setup (not a test method), so it's acceptable here. However, consider moving the FocusableContentView/FocusableContentViewHandler types to a separate file for cleanliness.

✅ Correct — Test validates the fix correctly

The test sets IsFocused = true externally, then calls DidUpdateFocus with NextFocusedView pointing to the inner Entry. Without the fix, the unconditional else clears IsFocused → test would fail. With the fix, _isFocusedSetByUs is false → skip clearing → test passes. The test correctly differentiates old vs new behavior.

✅ Correct — Guard against redundant IsFocused assignments

The if (!view.IsFocused) check before setting true and if (view.IsFocused) check before setting false prevent redundant property changes that would fire OnFocused/OnUnfocus events and VisualStateManager updates unnecessarily. Good defensive coding.

CI Status

  • maui-pr: ✅ All stages pass
  • maui-pr-uitests: ❌ macOS UITests Controls ListView failed — unlikely related to an iOS focus change in MauiView
  • maui-pr-devicetests: Not yet available in this run

Devil's Advocate

  1. Is the _isFocusedSetByUs flag correctly scoped? It's an instance field on MauiView, so each platform view has its own flag. If a view is recycled (e.g., in CollectionView), the flag could carry stale state. However, MauiView isn't used in CollectionView item templates, so this is unlikely.
  2. Could this affect Focus/Unfocus event ordering? The OnIsFocusedPropertyChanged handler fires OnFocused()/OnUnfocus() synchronously when IsFocused changes. With the fix, these events simply won't fire in the vendor scenario (since MAUI doesn't change IsFocused). The vendor is expected to handle events themselves. This is consistent.
  3. Does this affect macCatalyst? MauiView is used on both iOS and macCatalyst. The DidUpdateFocus UIKit API exists on both. The fix applies to both platforms. No macCatalyst-specific concern.

Verdict: Needs Discussion

Confidence: medium
Summary: The fix addresses a real problem (vendor-set IsFocused being overwritten), but the _isFocusedSetByUs tracking approach creates a risk where IsFocused can get stuck at true if focus leaves the entire view subtree. The alternative approach of checking IsDescendantOfView would handle both the vendor scenario and the "focus leaves entirely" case more robustly. Also needs: (1) a linked GitHub issue, (2) a test for the normal focus gain→lose lifecycle, and (3) a test for the "focus leaves entirely" scenario.

Copy link
Copy Markdown
Contributor

@kubaflo kubaflo left a comment

Choose a reason for hiding this comment

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

The device tests for windows are failing

@github-project-automation github-project-automation bot moved this from Ready To Review to Changes Requested in MAUI SDK Ongoing Apr 10, 2026
@MauiBot MauiBot added s/agent-review-incomplete AI agent could not complete all phases (blocker, timeout, error) and removed s/agent-changes-requested AI agent recommends changes - found a better alternative or issues labels Apr 10, 2026
@PureWeen
Copy link
Copy Markdown
Member

The device tests for windows are failing

Are they?
Devicetests seem green

https://dev.azure.com/dnceng-public/public/_build/results?buildId=1373373&view=ms.vss-test-web.build-test-results-tab

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

s/agent-review-incomplete AI agent could not complete all phases (blocker, timeout, error) s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review)

Projects

Status: Changes Requested

Development

Successfully merging this pull request may close these issues.

5 participants