@@ -29,6 +29,16 @@ public partial class IconBox : ContentControl
2929 private long _requestVersion ;
3030 private IconRequestMeasurement _activeRequestDiagnostics ;
3131 private IIconRequestDemand ? _activeRequestDemand ;
32+
33+ // ImageIconSource does not render through IconSourceElement. Reassigning Source on
34+ // one realized Image left recycled rows intermittently blank in testing. Keep a
35+ // stable Grid and alternate inactive Image slots so Source changes occur only on a
36+ // collapsed element before it is shown.
37+ private Grid ? _imagePresenter ;
38+ private Image ? _firstImageSlot ;
39+ private Image ? _secondImageSlot ;
40+ private Image ? _activeImageSlot ;
41+ private bool _imagePresenterDisabled ;
3242 private long _diagnosticId ;
3343 private IconRequestSite _derivedRequestSite ;
3444 private bool _hasDerivedRequestSite ;
@@ -429,11 +439,18 @@ private static void OnSourcePropertyChanged(DependencyObject d, DependencyProper
429439 switch ( e . NewValue )
430440 {
431441 case null :
432- self . Content = null ;
442+ var imagePresenterWasActive = ReferenceEquals ( self . Content , self . _imagePresenter ) ;
443+ self . ClearImagePresenter ( ) ;
444+ if ( ! imagePresenterWasActive )
445+ {
446+ self . Content = null ;
447+ }
448+
433449 self . Padding = default ;
434450 break ;
435451 case FontIconSource fontIcon :
436452 var fontElementStartedAt = IconLoadDiagnostics . BeginElementUpdate ( ) ;
453+ self . ClearImagePresenter ( ) ;
437454 self . UpdateLastFontSize ( ) ;
438455 fontIcon . FontSize = self . _lastFontSize ;
439456 if ( self . Content is IconSourceElement iconSourceElement )
@@ -452,6 +469,7 @@ private static void OnSourcePropertyChanged(DependencyObject d, DependencyProper
452469 break ;
453470 case BitmapIconSource bitmapIcon :
454471 var bitmapElementStartedAt = IconLoadDiagnostics . BeginElementUpdate ( ) ;
472+ self . ClearImagePresenter ( ) ;
455473 if ( self . Content is IconSourceElement iconSourceElement2 )
456474 {
457475 iconSourceElement2 . IconSource = bitmapIcon ;
@@ -467,8 +485,16 @@ private static void OnSourcePropertyChanged(DependencyObject d, DependencyProper
467485
468486 break ;
469487
488+ case ImageIconSource imageIcon :
489+ var imageElementStartedAt = IconLoadDiagnostics . BeginElementUpdate ( ) ;
490+ var reusedImagePresenter = self . PresentImageIconSource ( imageIcon ) ;
491+ IconLoadDiagnostics . RecordElementUpdate ( reusedImagePresenter , imageIcon , imageElementStartedAt ) ;
492+ self . Padding = default ;
493+ break ;
494+
470495 case IconSource source :
471496 var sourceElementStartedAt = IconLoadDiagnostics . BeginElementUpdate ( ) ;
497+ self . ClearImagePresenter ( ) ;
472498 self . Content = source . CreateIconElement ( ) ;
473499 IconLoadDiagnostics . RecordElementUpdate ( reused : false , source , sourceElementStartedAt ) ;
474500 self . Padding = default ;
@@ -479,6 +505,134 @@ private static void OnSourcePropertyChanged(DependencyObject d, DependencyProper
479505 }
480506 }
481507
508+ private bool PresentImageIconSource ( ImageIconSource source )
509+ {
510+ if ( _imagePresenterDisabled )
511+ {
512+ Content = source . CreateIconElement ( ) ;
513+ return false ;
514+ }
515+
516+ if ( source . ImageSource is null )
517+ {
518+ var imagePresenterWasActive = ReferenceEquals ( Content , _imagePresenter ) ;
519+ ClearImagePresenter ( ) ;
520+ if ( ! imagePresenterWasActive )
521+ {
522+ Content = null ;
523+ }
524+
525+ return _imagePresenter is not null ;
526+ }
527+
528+ var reused = _imagePresenter is not null ;
529+
530+ try
531+ {
532+ var presenter = EnsureImagePresenter ( ) ;
533+ var nextSlot = ReferenceEquals ( _activeImageSlot , _firstImageSlot )
534+ ? EnsureSecondImageSlot ( )
535+ : _firstImageSlot ! ;
536+ var previousSlot = _activeImageSlot ;
537+
538+ if ( previousSlot is not null )
539+ {
540+ previousSlot . Visibility = Visibility . Collapsed ;
541+ }
542+
543+ nextSlot . Source = source . ImageSource ;
544+ nextSlot . Visibility = Visibility . Visible ;
545+
546+ if ( previousSlot is not null )
547+ {
548+ previousSlot . Source = null ;
549+ }
550+
551+ _activeImageSlot = nextSlot ;
552+ if ( ! ReferenceEquals ( Content , presenter ) )
553+ {
554+ Content = presenter ;
555+ }
556+
557+ return reused ;
558+ }
559+ catch ( Exception ex )
560+ {
561+ DisableImagePresenter ( ) ;
562+ Logger . LogError ( $ "Failed to update reusable image presenter ({ GetDiagnosticDescription ( ) } )", ex ) ;
563+ Content = source . CreateIconElement ( ) ;
564+ return false ;
565+ }
566+ }
567+
568+ private Grid EnsureImagePresenter ( )
569+ {
570+ if ( _imagePresenter is not null )
571+ {
572+ return _imagePresenter ;
573+ }
574+
575+ var presenter = new Grid
576+ {
577+ IsHitTestVisible = false ,
578+ } ;
579+ var firstSlot = CreateImageSlot ( ) ;
580+ presenter . Children . Add ( firstSlot ) ;
581+
582+ _imagePresenter = presenter ;
583+ _firstImageSlot = firstSlot ;
584+ return presenter ;
585+ }
586+
587+ private Image EnsureSecondImageSlot ( )
588+ {
589+ if ( _secondImageSlot is not null )
590+ {
591+ return _secondImageSlot ;
592+ }
593+
594+ var secondSlot = CreateImageSlot ( ) ;
595+ _imagePresenter ! . Children . Add ( secondSlot ) ;
596+ _secondImageSlot = secondSlot ;
597+ return secondSlot ;
598+ }
599+
600+ private static Image CreateImageSlot ( )
601+ {
602+ return new Image
603+ {
604+ IsHitTestVisible = false ,
605+ Stretch = Stretch . Uniform ,
606+ Visibility = Visibility . Collapsed ,
607+ } ;
608+ }
609+
610+ private void ClearImagePresenter ( )
611+ {
612+ _activeImageSlot ? . Visibility = Visibility . Collapsed ;
613+ _firstImageSlot ? . Source = null ;
614+ _secondImageSlot ? . Source = null ;
615+ _activeImageSlot = null ;
616+ }
617+
618+ private void DisableImagePresenter ( )
619+ {
620+ try
621+ {
622+ ClearImagePresenter ( ) ;
623+ }
624+ catch ( Exception ex )
625+ {
626+ Logger . LogError ( $ "Failed to clear reusable image presenter ({ GetDiagnosticDescription ( ) } )", ex ) ;
627+ }
628+
629+ _imagePresenter = null ;
630+ _firstImageSlot = null ;
631+ _secondImageSlot = null ;
632+ _activeImageSlot = null ;
633+ _imagePresenterDisabled = true ;
634+ }
635+
482636 private static void OnFallbackSourcePropertyChanged ( DependencyObject d , DependencyPropertyChangedEventArgs e )
483637 {
484638 if ( d is not IconBox self )
@@ -518,9 +672,15 @@ private static void OnSourceKeyPropertyChanged(DependencyObject d, DependencyPro
518672 return ;
519673 }
520674
521- // A recycled IconBox must stop presenting the preceding item's icon before
522- // the replacement request has a chance to yield or enter the load queue.
523- self . UpdatePresentedSource ( ) ;
675+ // Keep the preceding source only while a replacement has a chance to resolve
676+ // synchronously in Refresh. RequestIconFromSource presents the fallback before
677+ // an asynchronous suspension, so the preceding item can never reach another frame.
678+ // If no request can start now, clear it immediately instead.
679+ if ( ! self . IsLoaded || self . _sourceRequested is null )
680+ {
681+ self . UpdatePresentedSource ( ) ;
682+ }
683+
524684 self . RequestRefresh ( IconRequestReason . SourceChanged ) ;
525685 }
526686
@@ -582,6 +742,17 @@ private static async void RequestIconFromSource(
582742
583743 if ( requestVersion == iconBox . _requestVersion )
584744 {
745+ // A synchronous provider failure occurs before the pending-source path
746+ // gets a chance to clear the preceding item.
747+ try
748+ {
749+ iconBox . UpdatePresentedSource ( ) ;
750+ }
751+ catch ( Exception presentationException )
752+ {
753+ Logger . LogError ( $ "Failed to clear icon after a request failure ({ iconBox . GetDiagnosticDescription ( ) } )", presentationException ) ;
754+ }
755+
585756 // Do not dispatch immediately: a deterministic failure would recurse forever.
586757 // Keep the request pending for the next external lifecycle or source trigger.
587758 iconBox . MarkRefreshPending ( IconRequestReason . Retry ) ;
0 commit comments