dotnet / dotnet/wpf

DownloadFailed event when ListBox display multiple Image controls with same Uri and with BitmapCreateOptions.IgnoreImageCache enabled

Open
#4,826 0 comments 3 reactions 0 assignees View on GitHub
.NET Framework Bug
Dominant language
C#
Stars
7.7k
Forks
1.3k
Avg merge
1d 11h
Merged PRs (30d)
61

Description

* .NET Core Version: 3.1
* Windows version: Only tested on 10.0.19041.0
* Does the bug reproduce also in WPF for .NET Framework 4.8?: Yes
* Is this bug related specifically to tooling in Visual Studio (e.g. XAML Designer, Code editing, etc...)? No



**Problem description:**
* The sample displays two rows in a ListBox and the same image (same uri) should be displayed in both rows.
* `BitmapSource` for both images is configured with `BitmapCacheOption.OnLoad` and `BitmapCreateOptions.IgnoreImageCache`
* Run sample: [WpfApp1.zip](https://github.com/dotnet/wpf/files/6793333/WpfApp1.zip)

**Actual behavior:**
* ListBox has two rows
* Row#1 displays image correctly
* Row#2 DOESN'T display image AND `BitmapImage.DownloadFailed` event is received with `FileFormatException. The image cannot be decoded. The image header might be corrupted.`

**Expected behavior:**
* Row#2 should display same image as Row#1 without BitmapImage.DownloadFailed event

**Investigation:**

I might be wrong, but the issue seems related to the fact that two BitmapDecoder are reading the same FileStream when `BitmapCreateOptions.IgnoreImageCache` is enabled. The stream position is somewhat not reset between each read.
- The first decoder reads the stream from the start. without any issue.
- Then, when the second decoder tries to read the same stream, the position of the stream is at the end instead of the beginning. This seems to cause the `FileFormatException` because it cannot read the header.

Here is a detailed explanation:

**When `BitmapCreateOptions.IgnoreImageCache` **IS** enabled**

* Request to start download `URI#1` is sent to `BitmapDownload` for Row#1
* Image with `UR#1` is not already being downloaded so download is started to a temporary file through a `FileStream`. [(reference)](https://referencesource.microsoft.com/#PresentationCore/Core/CSharp/System/Windows/Media/Imaging/BitmapDownload.cs,238)
- `LateBoundBitmapDecoder#1` is associated to the download request. [(reference)](https://referencesource.microsoft.com/#PresentationCore/Core/CSharp/System/Windows/Media/Imaging/BitmapDownload.cs,142)
- Request to start download `URI#1` is again sent to `BitmapDownload` for Row#2
- Image with `URI#1` is already being downloaded, so `BitmapDownload` don't start a new download. [(reference)](https://referencesource.microsoft.com/#PresentationCore/Core/CSharp/System/Windows/Media/Imaging/BitmapDownload.cs,130)
- `LateBoundBitmapDecoder#2` is associated to the existing download request.
- When download of URI#1 completes, `BitmapDownload` notifies all `LateBoundBitmapDecoder` associated to the request with the `FileStream` associated to the temporary file. [(reference)](https://referencesource.microsoft.com/#PresentationCore/Core/CSharp/System/Windows/Media/Imaging/BitmapDownload.cs,390)
- `LateBoundBitmapDecoder#1` receives download notification from `BitmapDownload`
- Since no `BitmapDecoder` exist in cache for `URI#1`, `BitmapDecoder#1` is instantiated and added to decoder cache.
- BitmapDecoder#1 reads `FileStream` for initialization. [(reference)](https://referencesource.microsoft.com/#PresentationCore/Core/CSharp/System/Windows/Media/Imaging/BitmapDecoder.cs,1298)
- `LateBoundBitmapDecoder#2` receives same download notification from `BitmapDownload`
- Because `BitmapCreateOptions.IgnoreImageCache` is enabled, BitmapDecoder#1 is removed from decoder cache. [(reference)](https://referencesource.microsoft.com/#PresentationCore/Core/CSharp/System/Windows/Media/Imaging/BitmapDecoder.cs,316)
- `BitmapDecoder#2` is instantiated and added to decoder cache.
- `BitmapDecoder#2` tries to read the same `FileStream` for initialization (but position of `FileStream` was not reset at the beginning) [(reference)](https://referencesource.microsoft.com/#PresentationCore/Core/CSharp/System/Windows/Media/Imaging/BitmapDecoder.cs,1298)
- BitmapDecoder handle and rethrow the following exception [(reference)](https://referencesource.microsoft.com/#PresentationCore/Core/CSharp/System/Windows/Media/Imaging/BitmapDecoder.cs,1332):
```
- System.NotSupportedException: No imaging component suitable to complete this operation was found. ---> System.Runtime.InteropServices.COMException: The component cannot be found. (Exception from HRESULT: 0x88982F50)
--- End of inner exception stack trace ---
at MS.Internal.HRESULT.Check(Int32 hr)
at System.Windows.Media.Imaging.BitmapDecoder.SetupDecoderFromUriOrStream(Uri uri, Stream stream, BitmapCacheOption cacheOption, Guid& clsId, Boolean& isOriginalWritable, Stream& uriStream, UnmanagedMemoryStream& unmanagedMemoryStream, SafeFileHandle& safeFilehandle)
```


**When `BitmapCreateOptions.IgnoreImageCache` **IS NOT** enabled**

- Same as previous example, but only one `BitmapDecoder` is created because it is not removed from the decoder cache [(reference)](https://referencesource.microsoft.com/#PresentationCore/Core/CSharp/System/Windows/Media/Imaging/BitmapDecoder.cs,316)
- In that case, `FileStream` is only read once and no issue occurs BUT you cannot refresh the cache for this URI (which was the IgnoreImageCache intention in the first place I believe).

**Potential fix?**
I didn't try it, but would it make sense for the `BitmapDownload` to reset `FileStream` position each time it notifies `BitmapDecoder` of a `DownloadCallback`? [(reference)](https://referencesource.microsoft.com/#PresentationCore/Core/CSharp/System/Windows/Media/Imaging/BitmapDownload.cs,395)

```
foreach (WeakReference decoderReference in entry.decoders)
{
LateBoundBitmapDecoder decoder = decoderReference.Target as LateBoundBitmapDecoder;
if (decoder != null)
{
MarshalEvents(
decoder,
new DispatcherOperationCallback(decoder.ProgressCallback),
100
);

// !!! Reset FileStream position !!!
entry.outputStream.Seek(0, SeekOrigin.Begin);

MarshalEvents(
decoder,
new DispatcherOperationCallback(decoder.DownloadCallback),
entry.outputStream
);
}
}
```

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.