unoplatform / uno

Build Mobile, Desktop and WebAssembly apps with C# and XAML. Today. Open source and professionally supported.
https://platform.uno
Apache License 2.0
8.77k stars 706 forks source link

[iOS] Setting `BitmapImage.Source` after it has been assigned to an image does not work #5453

Open mikernet opened 3 years ago

mikernet commented 3 years ago

Current behavior

Consider the following code:

        public BitmapImage GetBitmap()
        {
            var image = new BitmapImage();
            LoadImage();
            return image;

            async void LoadImage()
            {
                await Task.Delay(100);
                image.SetSource(GetSomeStream());
            }
        }

And a XAML binding such as:

<Image Source="{x:Bind GetBitmap()}"/>

The image never shows anything. This works fine in UWP and WASM.

Expected behavior

The image should show whatever the source is set to later.

Workaround

Bind to an image source that fires a property changed notification when it is finished loading and provides a "ready" BitmapImage, but this would require considerable changes to the application architecture to facilitate.

Environment

Nuget Package:

Nuget Package Version(s): 3.5.0-dev.385, 3,5.1, 3.6.0-dev.378

Affected platform(s):

Anything else we need to know?

I'm guessing this may be a problem on other platforms (i.e. Android) as well, but I'm not sure as of right now.

jeromelaban commented 3 years ago

Thanks for the report. Can you create a repro sample of the problem ? The code seems simple enough, but it'll be easier to make sure this is actually related to BitmapImage.

mikernet commented 3 years ago

Give me a sec with this, I'm trying a few things out to trace the issue - the above sample code actually works, the file path is just case sensitive on iOS (which makes sense) and I didn't have the casing correct so the dumbed-down version above doesn't actually reproduce the issue. Our code works fine on WASM and UWP but iOS refuses to show the image for some odd reason. I'll follow up on this and edit the problem once I figure out more.

mikernet commented 3 years ago

Okay, so setting UriSource actually works fine but SetSource() / SetSourceAsync() does not:

        public static BitmapImage GetBitmap(ImageType imageType, long? id)
        {
            var image = new BitmapImage();
            LoadImage();
            return image;

            async void LoadImage()
            {
                try {
                    await Task.Delay(100);
                    string fname = @"Assets/Logo.png";
                    StorageFolder InstallationFolder = Windows.ApplicationModel.Package.Current.InstalledLocation;
                    var imageStream = new MemoryStream();
                    using (var fs = File.OpenRead(Path.Combine(InstallationFolder.Path, fname))) {
                        fs.CopyTo(imageStream);
                    }

                    // option 1 (works):
                    image.UriSource = new Uri("ms-appx:///Assets/Logo.png");
                    // option 2 (doesn't work):
                    image.SetSource(imageStream.AsRandomAccessStream());
                }
                catch (Exception ex) {
                    Console.WriteLine(ex.Message);
                }

                Console.WriteLine("Image set");
            }
        }

If I leave all the code untouched except for picking either option 1 or option 2, it works in the former case but fails to show the image in the latter case on iOS. It does not hit the exception catch block. Both options work fine on WASM and UWP.

I'll throw a repro project up on github if you like.

mikernet commented 3 years ago

I changed the code in my post above a bit - if I comment out the await Task.Delay line then it works, so it's definitely an issue with calling SetSource() after a delay.

mikernet commented 3 years ago

Here's the minimal repro you can just paste into any test project - just change the file name to an image in the assets folder and set an image's source to the method's return value:

        public static BitmapImage GetBitmap()
        {
            var image = new BitmapImage();
            LoadImage();
            return image;

            async void LoadImage()
            {
                //await Task.Delay(100);
                string fname = @"Assets/Logo.png";
                StorageFolder InstallationFolder = Windows.ApplicationModel.Package.Current.InstalledLocation;
                using (var fs = File.OpenRead(Path.Combine(InstallationFolder.Path, fname))) {
                    image.SetSource(fs.AsRandomAccessStream());
                }
            }
        }

It works until you uncomment the await line.

dr1rrb commented 3 years ago

Note for contributors

It looks like the image is opened only once per consumer. We should multi-target the pattern used for wasm and skia (i.e. the Subscribe), and enable the ForceLoad which invokes the InvalidateSource() method which will updates all listeners (i.e. images and brushes).

mikernet commented 3 years ago

Could you point me towards the area in source where these changes need to be made?

Unfortunately, there seem to be some layout regressions in 3.6.0-dev.latest that prevent me from being able to use it in iOS so I'm probably going to have to backport the changes to 3.5.1 or figure out what is going on there. I'll try to PR the fix to this image issue since working around this issue is going to be a royal PITA for how we have things setup across the application.

ghost commented 2 years ago

i created a button to set a component Image source like that:

imgLogo.Source = GetBitmap();

(using SetSourceAsync instead SetSource)

What i could see, there`s a intermitent behavior. When you click a few times, eventually you get a blank image. Not sure if this is only in debugging. Still verifying.

ghost commented 2 years ago

@dr1rrb Hi! I tried to reproduce your code. However, SetSource() wont work in any platform, not only iOS. Using a Static GetImage method and XAML binding didn`t worked as well.

Using SetSourceAsync() works fine, besides having the intermitence i mentioned las comment. Do you mind to confirm your code please? Maybe it has been solved for SetSourceAsync( ) but not yet for SetSource( ).

dr1rrb commented 2 years ago

Hi @iury-kc, to which code do you refer? The repro from @mikernet ?

I don't remember having investigated nor tested this issue, the only thing is that if we are about to significantly change ImageSource, I want to make sure we are following the new pattern of the Subscribe. A long time ago when we started to dev uno we took a wrong approach to switch on each known kind of ImageSource directly in Image and ImageBrush (to mention only those), which drives us to copy the same code again and again on each platform and having some weird specific issues like this one.

ghost commented 2 years ago

@dr1rrb Oops. Tagged the wrong member. i mean @mikernet

ghost commented 2 years ago

Hi! Moving the delegate internal event EventHandler Invalidated; from WritableBitmap to ImageSource and calling the OnInvalidated when BitmapImage has changed ( URI, stream ....) solved the problem on iOS. That made the code cleaner and easier to understand. Test is done.

The pattern used by SKIA files still doesn`t work. Still investigating what is the problem there. As it will take a time to solve for all platforms, should I continue investigation or should I pause?

ghost commented 2 years ago

Did a PR that should close this issue. Also, we have Android similar behavior been solved on issue: https://github.com/unoplatform/uno/issues/7288. For that we probably will solve for SKIA/WASM as well. If not, will create a separated issue to manage those platforms.

Youssef1313 commented 1 year ago

https://github.com/unoplatform/uno/pull/12076 might fix this.