Image flickers when source is updated
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
Any progress on this issue please?
Not that I know. Though I fixed it in my app linked above if I recall well. Just not doing so many updates.
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.
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.
Any solution to this?
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}">