microsoft-ui-xaml icon indicating copy to clipboard operation
microsoft-ui-xaml copied to clipboard

Image flickers when source is updated

Open Slion opened this issue 2 years ago • 6 comments

Describe the bug

My binding looks like that:

  <Image                         
       Width ="{Binding ElementName=iMainWindow, Path=Settings.IconSize, Mode=OneWay}"
       Height="{Binding ElementName=iMainWindow, Path=Settings.IconSize, Mode=OneWay}"
       Margin="0"
       VerticalAlignment="Center"
       HorizontalAlignment="Center"
       Source="{x:Bind IconPath, Mode=OneWay}"

When IconPath is updated it flickers. So it looks like at least one empty frame is drawn before the new image. It's notably very visible when the new image is the same as the previous one and you are doing a lot of updates. One should avoid doing that but still the Image should not flicker.

Steps to reproduce the bug

This can be seen on Taskbar Pro when inside at git repository folder in Windows File Explorer.

Expected behavior

Image should not flicker when source is updated.

Screenshots

No response

NuGet package version

WinUI 3 - Windows App SDK 1.3.3: 1.3.230724000

Windows version

Windows 11 (22H2): Build 22621

Additional context

Looks MAUI had a similar issue: https://github.com/dotnet/maui/issues/6962

Slion avatar Aug 12 '23 06:08 Slion

Any progress on this issue please?

MartyIX avatar Jan 18 '24 10:01 MartyIX

Not that I know. Though I fixed it in my app linked above if I recall well. Just not doing so many updates.

Slion avatar Jan 18 '24 12:01 Slion

btw: Our use case is a bit different. We want to modify an image when pointer is hovering over it and return the image back to original when pointer exits the image.

That seems like a much more common scenario than replacing one image with the same image.

MartyIX avatar Jan 22 '24 10:01 MartyIX

I don't remember this being an issue in WPF/UWP, not sure why it became an issue in WinUI. I've run into the same problem with updating the Image source binding via a converter based on the system state of the application. I was going to try creating multiple stacked images and then change the visibility based on which one should be visible, but that just seems clunky and a waste of control resources.

GuildOfCalamity avatar Apr 29 '24 14:04 GuildOfCalamity

Any solution to this?

ramjke avatar Nov 21 '25 05:11 ramjke

The workaround

<?xml version="1.0" encoding="utf-8" ?>
<UserControl
    x:Class="Components.NoFlickerImage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="using:Components"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid>
        <Image x:Name="FirstImage" Visibility="Visible" />
        <Image x:Name="SecondImage" Visibility="Collapsed" />
    </Grid>
</UserControl>
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Imaging;
using System;
using System.Threading.Tasks;

namespace Components
{
    public sealed partial class NoFlickerImage : UserControl
    {
        public static readonly DependencyProperty SourceProperty =
            DependencyProperty.Register(
                nameof(Source),
                typeof(string),
                typeof(NoFlickerImage),
                new PropertyMetadata(string.Empty, OnSourceChanged));

        public string Source
        {
            get => (string)GetValue(SourceProperty);
            set => SetValue(SourceProperty, value);
        }

        private Image _activeImage;
        private Image _hiddenImage;

        // To handle rapid changes, we track the current "request ID"
        private long _currentLoadId = 0;

        private static void OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var control = (NoFlickerImage)d;
            control.LoadImage((string)e.NewValue);
        }

        private async void LoadImage(string uriString)
        {
            if (string.IsNullOrEmpty(uriString)) return;

            // 1. Increment ID to invalidate any previous ongoing loads
            long myLoadId = ++_currentLoadId;

            try
            {
                var uri = new Uri(uriString);
                var tcs = new TaskCompletionSource<bool>();

                // 2. Setup the hidden image
                var bitmap = new BitmapImage();

                // OPTIONAL: Ignore cache if you strictly need fresh images
                // bitmap.CreateOptions = BitmapCreateOptions.IgnoreImageCache; 

                bitmap.UriSource = uri;

                // 3. Wait for the ImageOpened event (The "Real" Delay)
                RoutedEventHandler openHandler = (s, e) => tcs.TrySetResult(true);
                ExceptionRoutedEventHandler errorHandler = (s, e) => tcs.TrySetResult(false);

                bitmap.ImageOpened += openHandler;
                bitmap.ImageFailed += errorHandler;

                // Apply source to hidden image to start download/decode
                _hiddenImage.Source = bitmap;

                // Wait for the event
                bool success = await tcs.Task;

                bitmap.ImageOpened -= openHandler;
                bitmap.ImageFailed -= errorHandler;

                // 4. CRITICAL CHECK: Has the source changed while we were loading?
                if (myLoadId != _currentLoadId)
                {
                    // Another load started after us. Do nothing. Abort.
                    return;
                }

                if (success)
                {
                    // 5. The Swap
                    _hiddenImage.Opacity = 0;
                    _hiddenImage.Visibility = Visibility.Visible;

                    // Optional: Simple Crossfade via Opacity
                    // In a real scenario, you might use a Storyboard here
                    _hiddenImage.Opacity = 1;
                    _activeImage.Visibility = Visibility.Collapsed;

                    // 6. Update References (Simple Swap)
                    var temp = _activeImage;
                    _activeImage = _hiddenImage;
                    _hiddenImage = temp;

                    // Clean up the old image to free memory
                    _hiddenImage.Source = null;
                }
            }
            catch (Exception ex)
            {
                System.Diagnostics.Debug.WriteLine($"Image load failed: {ex.Message}");
            }
        }

        public NoFlickerImage()
        {
            InitializeComponent();
            _activeImage = FirstImage;
            _hiddenImage = SecondImage;
        }
    }
}

Usage:

<NoFlickerImage Source="{x:Bind ImagePathString, Mode=OneWay}">

ramjke avatar Nov 21 '25 06:11 ramjke