diff options
29 files changed, 696 insertions, 199 deletions
diff --git a/Xamarin.Forms.ControlGallery.Android/BrokenImageSourceHandler.cs b/Xamarin.Forms.ControlGallery.Android/BrokenImageSourceHandler.cs new file mode 100644 index 00000000..390d77c9 --- /dev/null +++ b/Xamarin.Forms.ControlGallery.Android/BrokenImageSourceHandler.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Android.Content; +using Android.Graphics; +using Xamarin.Forms; +using Xamarin.Forms.ControlGallery.Android; +using Xamarin.Forms.Controls.Issues; +using Xamarin.Forms.Platform.Android; + +[assembly: ExportRenderer(typeof(_51173Image), typeof(_51173CustomImageRenderer))] +namespace Xamarin.Forms.ControlGallery.Android +{ + public sealed class BrokenImageSourceHandler : IImageSourceHandler + { + public Task<Bitmap> LoadImageAsync(ImageSource imagesource, Context context, CancellationToken cancelationToken = default(CancellationToken)) + { + throw new Exception("Fail"); + } + } + + public class _51173CustomImageRenderer : ImageRenderer + { + protected override async Task TryUpdateBitmap(Image previous = null) + { + try + { + await UpdateBitmap(previous).ConfigureAwait(false); + } + catch (Exception ex) + { + await Application.Current.MainPage.DisplayAlert("Image Error 51173", $"The image failed to load, here's why: {ex.Message}", "OK"); + } + } + } +} + + diff --git a/Xamarin.Forms.ControlGallery.Android/Properties/AssemblyInfo.cs b/Xamarin.Forms.ControlGallery.Android/Properties/AssemblyInfo.cs index dc197baf..82e72528 100644 --- a/Xamarin.Forms.ControlGallery.Android/Properties/AssemblyInfo.cs +++ b/Xamarin.Forms.ControlGallery.Android/Properties/AssemblyInfo.cs @@ -2,8 +2,11 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Android.App; +using Xamarin.Forms.Controls; +using Xamarin.Forms.Platform.Android; using Xamarin.Forms; - +using Xamarin.Forms.ControlGallery.Android; + // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. @@ -37,6 +40,8 @@ using Xamarin.Forms; [assembly: Android.App.MetaData("com.google.android.maps.v2.API_KEY", Value = "AIzaSyAdstcJQswxEjzX5YjLaMcu2aRVEBJw39Y")] [assembly: Xamarin.Forms.ResolutionGroupName ("XamControl")] +// Deliberately broken image source and handler so we can test handling of image loading errors +[assembly: ExportImageSourceHandler(typeof(FailImageSource), typeof(BrokenImageSourceHandler))] #if TEST_LEGACY_RENDERERS [assembly: ExportRenderer(typeof(Button), typeof(Xamarin.Forms.Platform.Android.AppCompat.ButtonRenderer))] [assembly: ExportRenderer(typeof(Image), typeof(Xamarin.Forms.Platform.Android.ImageRenderer))] diff --git a/Xamarin.Forms.ControlGallery.Android/Resources/drawable/invalidimage.jpg b/Xamarin.Forms.ControlGallery.Android/Resources/drawable/invalidimage.jpg new file mode 100644 index 00000000..6c7ebbb3 --- /dev/null +++ b/Xamarin.Forms.ControlGallery.Android/Resources/drawable/invalidimage.jpg @@ -0,0 +1 @@ +This is certainly not a real JPEG.
\ No newline at end of file diff --git a/Xamarin.Forms.ControlGallery.Android/Xamarin.Forms.ControlGallery.Android.csproj b/Xamarin.Forms.ControlGallery.Android/Xamarin.Forms.ControlGallery.Android.csproj index b606e109..e8011acb 100644 --- a/Xamarin.Forms.ControlGallery.Android/Xamarin.Forms.ControlGallery.Android.csproj +++ b/Xamarin.Forms.ControlGallery.Android/Xamarin.Forms.ControlGallery.Android.csproj @@ -26,6 +26,9 @@ <RestorePackages>true</RestorePackages> <NuGetPackageImportStamp> </NuGetPackageImportStamp> + <AndroidTlsProvider> + </AndroidTlsProvider> + <JavaMaximumHeapSize>1G</JavaMaximumHeapSize> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)' == 'Debug'"> <AndroidKeyStore>True</AndroidKeyStore> @@ -46,20 +49,27 @@ <AndroidLinkMode>Full</AndroidLinkMode> <JavaMaximumHeapSize>1G</JavaMaximumHeapSize> <EmbedAssembliesIntoApk>True</EmbedAssembliesIntoApk> - <BundleAssemblies>False</BundleAssemblies> <CustomCommands> <CustomCommands> - <Command type="AfterBuild" command="xbuild /t:SignAndroidPackage Xamarin.Forms.ControlGallery.Android/Xamarin.Forms.ControlGallery.Android.csproj" workingdir="${SolutionDir}" /> + <Command> + <type>AfterBuild</type> + <command>xbuild /t:SignAndroidPackage Xamarin.Forms.ControlGallery.Android/Xamarin.Forms.ControlGallery.Android.csproj</command> + <workingdir>${SolutionDir}</workingdir> + </Command> </CustomCommands> </CustomCommands> - <AndroidCreatePackagePerAbi>False</AndroidCreatePackagePerAbi> <AndroidSupportedAbis>armeabi;armeabi-v7a;x86</AndroidSupportedAbis> - <Debugger>Xamarin</Debugger> - <AndroidEnableMultiDex>False</AndroidEnableMultiDex> + <Debugger>.Net (Xamarin)</Debugger> <DevInstrumentationEnabled>True</DevInstrumentationEnabled> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <NoWarn> </NoWarn> + <BundleAssemblies>False</BundleAssemblies> + <AndroidCreatePackagePerAbi>False</AndroidCreatePackagePerAbi> + <AotAssemblies>False</AotAssemblies> + <EnableLLVM>False</EnableLLVM> + <AndroidEnableMultiDex>False</AndroidEnableMultiDex> + <EnableProguard>False</EnableProguard> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <DebugType>pdbonly</DebugType> @@ -73,8 +83,6 @@ <JavaMaximumHeapSize>1G</JavaMaximumHeapSize> <AndroidLinkSkip /> <EmbedAssembliesIntoApk>True</EmbedAssembliesIntoApk> - <BundleAssemblies>False</BundleAssemblies> - <AndroidCreatePackagePerAbi>False</AndroidCreatePackagePerAbi> <AndroidSupportedAbis>armeabi-v7a,x86</AndroidSupportedAbis> <AndroidStoreUncompressedFileExtensions /> <MandroidI18n /> @@ -97,14 +105,11 @@ <AndroidLinkSkip /> <AndroidLinkMode>SdkOnly</AndroidLinkMode> <EmbedAssembliesIntoApk>False</EmbedAssembliesIntoApk> - <BundleAssemblies>False</BundleAssemblies> - <AndroidCreatePackagePerAbi>False</AndroidCreatePackagePerAbi> <AndroidSupportedAbis>armeabi,armeabi-v7a,x86</AndroidSupportedAbis> <AndroidStoreUncompressedFileExtensions /> <MandroidI18n /> <JavaMaximumHeapSize>1G</JavaMaximumHeapSize> <Debugger>Xamarin</Debugger> - <AndroidEnableMultiDex>False</AndroidEnableMultiDex> <DevInstrumentationEnabled>True</DevInstrumentationEnabled> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <NoWarn> @@ -176,6 +181,7 @@ <Compile Include="CustomRenderers.cs" /> <Compile Include="ColorPicker.cs" /> <Compile Include="_38989CustomRenderer.cs" /> + <Compile Include="BrokenImageSourceHandler.cs" /> </ItemGroup> <ItemGroup> <AndroidAsset Include="Assets\default.css" /> @@ -204,6 +210,7 @@ <AndroidResource Include="Resources\drawable\coffee.png" /> <AndroidResource Include="Resources\drawable\toolbar_close.png" /> <AndroidResource Include="Resources\drawable\test.jpg" /> + <AndroidResource Include="Resources\drawable\invalidimage.jpg" /> </ItemGroup> <ItemGroup> <AndroidResource Include="Resources\drawable\Icon.png" /> diff --git a/Xamarin.Forms.ControlGallery.Android/_38989CustomRenderer.cs b/Xamarin.Forms.ControlGallery.Android/_38989CustomRenderer.cs index 1cde98a8..ef4aad71 100644 --- a/Xamarin.Forms.ControlGallery.Android/_38989CustomRenderer.cs +++ b/Xamarin.Forms.ControlGallery.Android/_38989CustomRenderer.cs @@ -30,6 +30,4 @@ namespace Xamarin.Forms.ControlGallery.Android return nativeView; } } - - }
\ No newline at end of file diff --git a/Xamarin.Forms.ControlGallery.Windows/BrokenImageSourceHandler.cs b/Xamarin.Forms.ControlGallery.Windows/BrokenImageSourceHandler.cs index ffe999ee..42ee3402 100644 --- a/Xamarin.Forms.ControlGallery.Windows/BrokenImageSourceHandler.cs +++ b/Xamarin.Forms.ControlGallery.Windows/BrokenImageSourceHandler.cs @@ -1,16 +1,20 @@ using System; using System.Threading; using System.Threading.Tasks; - +using Xamarin.Forms.Controls.Issues; using WImageSource = Windows.UI.Xaml.Media.ImageSource; #if WINDOWS_UWP using Xamarin.Forms.Platform.UWP; +using Xamarin.Forms.ControlGallery.WindowsUniversal; +[assembly: ExportRenderer(typeof(_51173Image), typeof(_51173CustomImageRenderer))] namespace Xamarin.Forms.ControlGallery.WindowsUniversal #else using Xamarin.Forms.Platform.WinRT; +using Xamarin.Forms.ControlGallery.WinRT; +[assembly: ExportRenderer(typeof(_51173Image), typeof(_51173CustomImageRenderer))] namespace Xamarin.Forms.ControlGallery.WinRT #endif { @@ -21,4 +25,19 @@ namespace Xamarin.Forms.ControlGallery.WinRT throw new Exception("Fail"); } } + + public class _51173CustomImageRenderer : ImageRenderer + { + protected override async Task TryUpdateSource() + { + try + { + await UpdateSource().ConfigureAwait(false); + } + catch (Exception ex) + { + await Xamarin.Forms.Application.Current.MainPage.DisplayAlert("Image Error 51173", $"The image failed to load, here's why: {ex.Message}", "OK"); + } + } + } }
\ No newline at end of file diff --git a/Xamarin.Forms.ControlGallery.iOS/BrokenImageSourceHandler.cs b/Xamarin.Forms.ControlGallery.iOS/BrokenImageSourceHandler.cs new file mode 100644 index 00000000..5e58764b --- /dev/null +++ b/Xamarin.Forms.ControlGallery.iOS/BrokenImageSourceHandler.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using UIKit; +using Xamarin.Forms; +using Xamarin.Forms.ControlGallery.iOS; +using Xamarin.Forms.Controls.Issues; +using Xamarin.Forms.Platform.iOS; + +[assembly: ExportRenderer(typeof(_51173Image), typeof(_51173CustomImageRenderer))] +namespace Xamarin.Forms.ControlGallery.iOS +{ + public sealed class BrokenImageSourceHandler : IImageSourceHandler + { + public Task<UIImage> LoadImageAsync(ImageSource imagesource, CancellationToken cancelationToken = new CancellationToken(), + float scale = 1) + { + throw new Exception("Fail"); + } + } + + public class _51173CustomImageRenderer : ImageRenderer + { + protected override async Task TrySetImage(Image previous = null) + { + try + { + await SetImage(previous).ConfigureAwait(false); + } + catch (Exception ex) + { + await Xamarin.Forms.Application.Current.MainPage.DisplayAlert("Image Error 51173", $"The image failed to load, here's why: {ex.Message}", "OK"); + } + } + } +} diff --git a/Xamarin.Forms.ControlGallery.iOS/Properties/AssemblyInfo.cs b/Xamarin.Forms.ControlGallery.iOS/Properties/AssemblyInfo.cs index e7f2b400..d11d7e2f 100644 --- a/Xamarin.Forms.ControlGallery.iOS/Properties/AssemblyInfo.cs +++ b/Xamarin.Forms.ControlGallery.iOS/Properties/AssemblyInfo.cs @@ -1,6 +1,9 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using Xamarin.Forms; +using Xamarin.Forms.ControlGallery.iOS; +using Xamarin.Forms.Controls; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information @@ -35,3 +38,6 @@ using System.Runtime.InteropServices; [assembly: AssemblyVersion ("1.0.0.0")] [assembly: AssemblyFileVersion ("1.0.0.0")] [assembly: Xamarin.Forms.ResolutionGroupName("XamControl")] + +// Deliberately broken image source and handler so we can test handling of image loading errors +[assembly: ExportImageSourceHandler(typeof(FailImageSource), typeof(BrokenImageSourceHandler))]
\ No newline at end of file diff --git a/Xamarin.Forms.ControlGallery.iOS/Resources/invalidimage.jpg b/Xamarin.Forms.ControlGallery.iOS/Resources/invalidimage.jpg new file mode 100644 index 00000000..6c7ebbb3 --- /dev/null +++ b/Xamarin.Forms.ControlGallery.iOS/Resources/invalidimage.jpg @@ -0,0 +1 @@ +This is certainly not a real JPEG.
\ No newline at end of file diff --git a/Xamarin.Forms.ControlGallery.iOS/Xamarin.Forms.ControlGallery.iOS.csproj b/Xamarin.Forms.ControlGallery.iOS/Xamarin.Forms.ControlGallery.iOS.csproj index d76b5e58..4e2e7706 100644 --- a/Xamarin.Forms.ControlGallery.iOS/Xamarin.Forms.ControlGallery.iOS.csproj +++ b/Xamarin.Forms.ControlGallery.iOS/Xamarin.Forms.ControlGallery.iOS.csproj @@ -159,6 +159,7 @@ <TreatWarningsAsErrors>true</TreatWarningsAsErrors> </PropertyGroup> <ItemGroup> + <Compile Include="BrokenImageSourceHandler.cs" /> <Compile Include="BrokenNativeControl.cs" /> <Compile Include="CustomRenderer40251.cs" /> <Compile Include="Main.cs" /> @@ -218,6 +219,7 @@ <Content Include="oasis.jpg" /> <BundleResource Include="oasissmall.jpg" /> <Content Include="coffee%402x.png" /> + <BundleResource Include="Resources\invalidimage.jpg" /> <Content Include="settings%402x.png" /> <Content Include="menuIcon%402x.png" /> <Content Include="crimsonsmall.jpg" /> diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla37625.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla37625.cs index 06d59c57..83df91e0 100644 --- a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla37625.cs +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla37625.cs @@ -13,13 +13,13 @@ namespace Xamarin.Forms.Controls.Issues { [Preserve (AllMembers = true)] [Issue (IssueTracker.Bugzilla, 37625, "App crashes when quickly adding/removing Image views (Windows UWP)")] - public class Bugzilla37625 : TestContentPage // or TestMasterDetailPage, etc ... + public class Bugzilla37625 : TestContentPage { protected override async void Init () { int retry = 5; while (retry-- >= 0) { - var imageUri = new Uri ("https://xamarin.com/content/images/pages/products/platform.png"); + var imageUri = new Uri ("https://raw.githubusercontent.com/xamarin/Xamarin.Forms/master/Xamarin.Forms.ControlGallery.Android/Assets/WebImages/XamarinLogo.png"); Content = new Image () { Source = new UriImageSource () { Uri = imageUri }, BackgroundColor = Color.Black, AutomationId = "success" }; await Task.Delay (50); diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla51173.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla51173.cs new file mode 100644 index 00000000..46841122 --- /dev/null +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla51173.cs @@ -0,0 +1,187 @@ +using System; +using System.Globalization; +using Xamarin.Forms.CustomAttributes; +using Xamarin.Forms.Internals; + +#if UITEST +using Xamarin.Forms.Core.UITests; +using Xamarin.UITest; +using NUnit.Framework; +#endif + +namespace Xamarin.Forms.Controls.Issues +{ +#if UITEST + [Category(UITestCategories.Image)] +#endif + + [Preserve(AllMembers = true)] + [Issue(IssueTracker.Bugzilla, 51173, "ImageRenderer, async void SetImage - Cannot catch exceptions", PlatformAffected.All)] + public class Bugzilla51173 : TestContentPage + { +#if UITEST + [Test(Description = "Attempt to load an image from a URI which does not exist")] + public void Bugzilla51173_NonexistentUri() + { + + RunningApp.WaitForElement(q => q.Marked(UriDoesNotExist)); + + RunningApp.Tap(UriDoesNotExist); + RunningApp.WaitForElement(q => q.Marked(ErrorLogged)); + RunningApp.WaitForElement(q => q.Marked(NotLoading)); + } + + [Test(Description = "The IImageSourceHandler itself throws an exception")] + public void Bugzilla51173_HandlerThrowsException() + { + RunningApp.WaitForElement(q => q.Marked(HandlerThrows)); + + RunningApp.Tap(HandlerThrows); + RunningApp.WaitForElement(q => q.Marked(ErrorLogged)); + RunningApp.WaitForElement(q => q.Marked(NotLoading)); + } + + [Test(Description = "The URI is valid, but the image data is not")] + public void Bugzilla51173_RealUriWithInvalidImageData() + { + RunningApp.WaitForElement(q => q.Marked(RealUriInvalidImage)); + + RunningApp.Tap(RealUriInvalidImage); + RunningApp.WaitForElement(q => q.Marked(ErrorLogged)); + RunningApp.WaitForElement(q => q.Marked(NotLoading)); + } + + [Test(Description = "Attempt to load a local image which does not exist")] + public void Bugzilla51173_NonexistentImage() + { + RunningApp.WaitForElement(q => q.Marked(ImageDoesNotExist)); + + RunningApp.Tap(ImageDoesNotExist); + RunningApp.WaitForElement(q => q.Marked(ErrorLogged)); + RunningApp.WaitForElement(q => q.Marked(NotLoading)); + } + + [Test(Description = "Attempt to load a local image which exists, but has corrupt data")] + public void Bugzilla51173_InvalidImage() + { + RunningApp.WaitForElement(q => q.Marked(ImageIsInvalid)); + + RunningApp.Tap(ImageIsInvalid); + RunningApp.WaitForElement(q => q.Marked(ErrorLogged)); + RunningApp.WaitForElement(q => q.Marked(NotLoading)); + } + + [Test(Description = "Load a valid image")] + public void Bugzilla51173_ValidImage() + { + RunningApp.WaitForElement(q => q.Marked(ValidImage)); + RunningApp.Tap(ValidImage); + RunningApp.WaitForElement(q => q.Marked(NotLoading)); + } +#endif + + const string ValidImage = "Valid Image"; + const string ImageDoesNotExist = "Non-existent Image File"; + const string ImageIsInvalid = "Invalid Image File (bad data)"; + const string UriDoesNotExist = "Non-existent URI"; + const string HandlerThrows = "Source Throws Exception"; + const string RealUriInvalidImage = "Valid URI with invalid image file"; + const string ErrorLogged = "Error logged"; + + const string Loading = "Loading"; + const string NotLoading = "Not Loading"; + + Label _results; + Image _image; + + class LoadingConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if ((bool)value) + { + return Loading; + } + + return NotLoading; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } + + protected override void Init() + { + _results = new Label { Margin = 10, FontAttributes = FontAttributes.Bold, BackgroundColor = Color.Silver, HorizontalTextAlignment = TextAlignment.Center}; + + var errorMessage = new Label(); + + Log.Listeners.Add( + new DelegateLogListener((c, m) => Device.BeginInvokeOnMainThread(() => + { + _results.Text = ErrorLogged; + errorMessage.Text = m; + }))); + + var instructions = new Label + { + Text = + "Pressing the 'Valid Image' button should display an image of a coffee cup. Every other button should cause the messager 'Error logged' to appear at the top of the page." + }; + + _image = new Image { BackgroundColor = Color.Bisque, HeightRequest = 20 }; + + var loadingState = new Label(); + loadingState.SetBinding(Label.TextProperty, new Binding(Image.IsLoadingProperty.PropertyName, BindingMode.Default, new LoadingConverter())); + loadingState.BindingContext = _image; + + var legit = CreateTest(() => _image.Source = ImageSource.FromFile("coffee.png"), ValidImage); + + var invalidImageFileName = CreateTest(() => _image.Source = ImageSource.FromFile("fake.png"), ImageDoesNotExist); + + var invalidImageFile = CreateTest(() => _image.Source = ImageSource.FromFile("invalidimage.jpg"), ImageIsInvalid); + + var fakeUri = CreateTest(() => _image.Source = ImageSource.FromUri(new Uri("http://not.real")), UriDoesNotExist); + + var crashImage = CreateTest(() => _image.Source = new FailImageSource(), HandlerThrows); + + var uriInvalidImageData = + CreateTest(() => _image.Source = ImageSource.FromUri(new Uri("https://gist.githubusercontent.com/hartez/a2dda6b5c78852bcf4832af18f21a023/raw/39f4cd2e9fe8514694ac7fa0943017eb9308853d/corrupt.jpg")), + RealUriInvalidImage); + + Content = new StackLayout + { + Margin = new Thickness(5, 20, 5, 0), + Children = + { + _image, + instructions, + legit, + invalidImageFileName, + invalidImageFile, + fakeUri, + crashImage, + uriInvalidImageData, + _results, + loadingState, + errorMessage + } + }; + } + + Button CreateTest(Action imageLoadAction, string title) + { + var button = new Button { Text = title }; + + button.Clicked += (sender, args) => + { + _results.Text = ""; + imageLoadAction(); + }; + + return button; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/CustomImageRendererErrorHandling.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/CustomImageRendererErrorHandling.cs new file mode 100644 index 00000000..3e4ea56a --- /dev/null +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/CustomImageRendererErrorHandling.cs @@ -0,0 +1,40 @@ +using Xamarin.Forms.CustomAttributes; +using Xamarin.Forms.Internals; + +namespace Xamarin.Forms.Controls.Issues +{ + [Preserve(AllMembers = true)] + [Issue(IssueTracker.None, 51173, "Custom ImageRenderer error handling demo", PlatformAffected.All)] + public class CustomImageRendererErrorHandling : TestContentPage + { + protected override void Init() + { + var layout = new StackLayout { Margin = new Thickness(5, 40, 5, 0) }; + + var instructions = new Label + { + Text = + @" +Click 'Update Image Source'; it will update the coffee image with an image source which will throw an exception. +Instead of just logging an error, the custom renderer will display an alert dialog about the error. +" + }; + + var image = new _51173Image { Source = "coffee.png" }; + + var button = new Button { Text = "Update Image Source" }; + + button.Clicked += (sender, args) => image.Source = new FailImageSource(); + + layout.Children.Add(instructions); + layout.Children.Add(image); + layout.Children.Add(button); + + Content = layout; + } + } + + // custom image type for demonstrating custom error handling in a custom renderer + [Preserve(AllMembers = true)] + public class _51173Image : Image { } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/ImageLoadingErrorHandling.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/ImageLoadingErrorHandling.cs deleted file mode 100644 index b2607c71..00000000 --- a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/ImageLoadingErrorHandling.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System; -using Xamarin.Forms.CustomAttributes; -using Xamarin.Forms.Internals; - -namespace Xamarin.Forms.Controls -{ - [Preserve(AllMembers = true)] - [Issue(IssueTracker.None, 0, "Image Loading Error Handling", PlatformAffected.WinRT | PlatformAffected.UWP)] - public class ImageLoadingErrorHandling : TestContentPage - { - protected override void Init() - { - Log.Listeners.Add( - new DelegateLogListener((c, m) => Device.BeginInvokeOnMainThread(() => DisplayAlert(c, m, "Cool, Thanks")))); - - var image = new Image() {BackgroundColor = Color.White}; - - Grid legit = CreateTest(() => image.Source = ImageSource.FromFile("coffee.png"), - "Valid Image", - "Clicking this button should load an image at the top of the page.", - Color.Silver); - - Grid invalidImageFileName = CreateTest(() => image.Source = ImageSource.FromFile("fake.png"), - "Non-existent Image File", - "Clicking this button should display an alert dialog with an error that the image failed to load."); - - Grid invalidImageFile = CreateTest(() => image.Source = ImageSource.FromFile("invalidimage.jpg"), - "Invalid Image File (bad data)", - "Clicking this button should display an alert dialog with an error that the image failed to load.", - Color.Silver); - - Grid fakeUri = CreateTest(() => image.Source = ImageSource.FromUri(new Uri("http://not.real")), - "Non-existent URI", - (Device.RuntimePlatform == Device.UWP || Device.RuntimePlatform == Device.WinRT) && Device.Idiom == TargetIdiom.Phone - ? "Clicking this button should display an alert dialog. The error message should include the text 'NotFound'." - : "Clicking this button should display an alert dialog. The error message should include the text 'the server name or address could not be resolved'."); - - // This used to crash the app with an uncatchable error; need to make sure it's not still doing that - Grid crashImage = CreateTest(() => image.Source = new FailImageSource(), - "Source Throws Exception", - "Clicking this button should display an alert dialog. The error messages hould include the test 'error updating image source'.", - Color.Silver); - - Grid uriInvalidImageData = - CreateTest(() => image.Source = ImageSource.FromUri(new Uri("https://gist.githubusercontent.com/hartez/a2dda6b5c78852bcf4832af18f21a023/raw/39f4cd2e9fe8514694ac7fa0943017eb9308853d/corrupt.jpg")), - "Valid URI with invalid image file", - "Clicking this button should display an alert dialog. The error message should include the text 'UriImageSourceHandler could not load https://gist.githubusercontent.com/hartez/a2dda6b5c78852bcf4832af18f21a023/raw/39f4cd2e9fe8514694ac7fa0943017eb9308853d/corrupt.jpg'"); - - Content = new StackLayout - { - Children = - { - image, - legit, - invalidImageFileName, - invalidImageFile, - fakeUri, - crashImage, - uriInvalidImageData - } - }; - } - - static Grid CreateTest(Action imageLoadAction, string title, string instructions, Color? backgroundColor = null) - { - var button = new Button { Text = "Test" }; - - button.Clicked += (sender, args) => imageLoadAction(); - - var titleLabel = new Label - { - Text = title, - FontAttributes = FontAttributes.Bold - }; - - var label = new Label - { - Text = instructions - }; - - var grid = new Grid - { - ColumnDefinitions = - new ColumnDefinitionCollection { new ColumnDefinition(), new ColumnDefinition(), new ColumnDefinition() }, - RowDefinitions = new RowDefinitionCollection { new RowDefinition { Height = 80 } } - }; - - if (backgroundColor.HasValue) - { - grid.BackgroundColor = backgroundColor.Value; - } - - grid.AddChild(titleLabel, 0, 0); - grid.AddChild(label, 1, 0); - grid.AddChild(button, 2, 0); - - return grid; - } - } -}
\ No newline at end of file diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems index 73f1bc2f..f33dc6d7 100644 --- a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems @@ -200,6 +200,7 @@ <Compile Include="$(MSBuildThisFileDirectory)Bugzilla34561.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Bugzilla34727.cs" /> <Compile Include="$(MSBuildThisFileDirectory)ComplexListView.cs" /> + <Compile Include="$(MSBuildThisFileDirectory)CustomImageRendererErrorHandling.cs" /> <Compile Include="$(MSBuildThisFileDirectory)DefaultColorToggleTest.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Bugzilla38416.xaml.cs"> <DependentUpon>Bugzilla38416.xaml</DependentUpon> @@ -253,7 +254,7 @@ <Compile Include="$(MSBuildThisFileDirectory)TestPages\ScreenshotConditionalApp.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Bugzilla41842.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Bugzilla42277.cs" /> - <Compile Include="$(MSBuildThisFileDirectory)ImageLoadingErrorHandling.cs" /> + <Compile Include="$(MSBuildThisFileDirectory)Bugzilla51173.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Bugzilla33561.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Bugzilla43214.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Bugzilla42602.cs" /> diff --git a/Xamarin.Forms.Core/FileImageSource.cs b/Xamarin.Forms.Core/FileImageSource.cs index 9e1d1e73..f2f34719 100644 --- a/Xamarin.Forms.Core/FileImageSource.cs +++ b/Xamarin.Forms.Core/FileImageSource.cs @@ -18,6 +18,11 @@ namespace Xamarin.Forms return Task.FromResult(false); } + public override string ToString() + { + return $"File: {File}"; + } + public static implicit operator FileImageSource(string file) { return (FileImageSource)FromFile(file); diff --git a/Xamarin.Forms.Core/UriImageSource.cs b/Xamarin.Forms.Core/UriImageSource.cs index 7d1a9eb2..5e0db2cb 100644 --- a/Xamarin.Forms.Core/UriImageSource.cs +++ b/Xamarin.Forms.Core/UriImageSource.cs @@ -92,6 +92,11 @@ namespace Xamarin.Forms return stream; } + public override string ToString() + { + return $"Uri: {Uri}"; + } + static string GetCacheKey(Uri uri) { return Device.PlatformServices.GetMD5Hash(uri.AbsoluteUri); diff --git a/Xamarin.Forms.Platform.Android/Extensions/ImageViewExtensions.cs b/Xamarin.Forms.Platform.Android/Extensions/ImageViewExtensions.cs index f60acb7e..d0cd2045 100644 --- a/Xamarin.Forms.Platform.Android/Extensions/ImageViewExtensions.cs +++ b/Xamarin.Forms.Platform.Android/Extensions/ImageViewExtensions.cs @@ -1,28 +1,33 @@ using System; using System.Threading.Tasks; using Android.Graphics; -using Java.IO; using AImageView = Android.Widget.ImageView; namespace Xamarin.Forms.Platform.Android { internal static class ImageViewExtensions { - public static async void UpdateBitmap(this AImageView imageView, Image newImage, Image previousImage = null) + // TODO hartez 2017/04/07 09:33:03 Review this again, not sure it's handling the transition from previousImage to 'null' newImage correctly + public static async Task UpdateBitmap(this AImageView imageView, Image newImage, Image previousImage = null) { + if (imageView == null || imageView.IsDisposed()) + return; + if (Device.IsInvokeRequired) throw new InvalidOperationException("Image Bitmap must not be updated from background thread"); if (previousImage != null && Equals(previousImage.Source, newImage.Source)) return; - ((IImageController)newImage).SetIsLoading(true); + var imageController = newImage as IImageController; + + imageController?.SetIsLoading(true); - (imageView as IImageRendererController).SkipInvalidate(); + (imageView as IImageRendererController)?.SkipInvalidate(); imageView.SetImageResource(global::Android.Resource.Color.Transparent); - ImageSource source = newImage.Source; + ImageSource source = newImage?.Source; Bitmap bitmap = null; IImageSourceHandler handler; @@ -34,10 +39,7 @@ namespace Xamarin.Forms.Platform.Android } catch (TaskCanceledException) { - } - catch (IOException ex) - { - Internals.Log.Warning("Xamarin.Forms.Platform.Android.ImageRenderer", "Error updating bitmap: {0}", ex); + imageController?.SetIsLoading(false); } } @@ -47,14 +49,19 @@ namespace Xamarin.Forms.Platform.Android return; } - if (bitmap == null && source is FileImageSource) - imageView.SetImageResource(ResourceManager.GetDrawableByName(((FileImageSource)source).File)); - else - imageView.SetImageBitmap(bitmap); + if (!imageView.IsDisposed()) + { + if (bitmap == null && source is FileImageSource) + imageView.SetImageResource(ResourceManager.GetDrawableByName(((FileImageSource)source).File)); + else + { + imageView.SetImageBitmap(bitmap); + } + } bitmap?.Dispose(); - ((IImageController)newImage).SetIsLoading(false); + imageController?.SetIsLoading(false); ((IVisualElementController)newImage).NativeSizeChanged(); } } diff --git a/Xamarin.Forms.Platform.Android/Extensions/JavaObjectExtensions.cs b/Xamarin.Forms.Platform.Android/Extensions/JavaObjectExtensions.cs new file mode 100644 index 00000000..007d8759 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Extensions/JavaObjectExtensions.cs @@ -0,0 +1,12 @@ +using System; + +namespace Xamarin.Forms.Platform.Android +{ + internal static class JavaObjectExtensions + { + public static bool IsDisposed(this Java.Lang.Object obj) + { + return obj.Handle == IntPtr.Zero; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/FastRenderers/ImageRenderer.cs b/Xamarin.Forms.Platform.Android/FastRenderers/ImageRenderer.cs index 845ec449..5ca1bd20 100644 --- a/Xamarin.Forms.Platform.Android/FastRenderers/ImageRenderer.cs +++ b/Xamarin.Forms.Platform.Android/FastRenderers/ImageRenderer.cs @@ -1,8 +1,10 @@ using System; using System.ComponentModel; +using System.Threading.Tasks; using AImageView = Android.Widget.ImageView; using AView = Android.Views.View; using Android.Views; +using Xamarin.Forms.Internals; namespace Xamarin.Forms.Platform.Android.FastRenderers { @@ -17,30 +19,32 @@ namespace Xamarin.Forms.Platform.Android.FastRenderers protected override void Dispose(bool disposing) { - base.Dispose(disposing); - if (_disposed) return; _disposed = true; - if (!disposing) - return; - - if (_visualElementTracker != null) + if (disposing) { - _visualElementTracker.Dispose(); - _visualElementTracker = null; + if (_visualElementTracker != null) + { + _visualElementTracker.Dispose(); + _visualElementTracker = null; + } + + if (_visualElementRenderer != null) + { + _visualElementRenderer.Dispose(); + _visualElementRenderer = null; + } + + if (_element != null) + { + _element.PropertyChanged -= OnElementPropertyChanged; + } } - if (_visualElementRenderer != null) - { - _visualElementRenderer.Dispose(); - _visualElementRenderer = null; - } - - if (_element != null) - _element.PropertyChanged -= OnElementPropertyChanged; + base.Dispose(disposing); } public override void Invalidate() @@ -54,9 +58,9 @@ namespace Xamarin.Forms.Platform.Android.FastRenderers base.Invalidate(); } - protected virtual void OnElementChanged(ElementChangedEventArgs<Image> e) + protected virtual async void OnElementChanged(ElementChangedEventArgs<Image> e) { - this.UpdateBitmap(e.NewElement, e.OldElement); + await TryUpdateBitmap(e.OldElement); UpdateAspect(); ElementChanged?.Invoke(this, new VisualElementChangedEventArgs(e.OldElement, e.NewElement)); @@ -77,6 +81,11 @@ namespace Xamarin.Forms.Platform.Android.FastRenderers SizeRequest IVisualElementRenderer.GetDesiredSize(int widthConstraint, int heightConstraint) { + if (_disposed) + { + return new SizeRequest(); + } + Measure(widthConstraint, heightConstraint); return new SizeRequest(new Size(MeasuredWidth, MeasuredHeight), MinimumSize()); } @@ -114,7 +123,7 @@ namespace Xamarin.Forms.Platform.Android.FastRenderers _element?.SendViewInitialized(Control); } - + void IVisualElementRenderer.SetLabelFor(int? id) { if (_defaultLabelFor == null) @@ -144,18 +153,53 @@ namespace Xamarin.Forms.Platform.Android.FastRenderers { } - protected virtual void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + protected virtual async void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == Image.SourceProperty.PropertyName) - this.UpdateBitmap(_element); + await TryUpdateBitmap(); else if (e.PropertyName == Image.AspectProperty.PropertyName) UpdateAspect(); ElementPropertyChanged?.Invoke(this, e); } + protected virtual async Task TryUpdateBitmap(Image previous = null) + { + // By default we'll just catch and log any exceptions thrown by UpdateBitmap so they don't bring down + // the application; a custom renderer can override this method and handle exceptions from + // UpdateBitmap differently if it wants to + + try + { + await UpdateBitmap(previous); + } + catch (Exception ex) + { + Log.Warning(nameof(ImageRenderer), "Error loading image: {0}", ex); + } + finally + { + ((IImageController)_element)?.SetIsLoading(false); + } + } + + protected async Task UpdateBitmap(Image previous = null) + { + if (_element == null || _disposed) + { + return; + } + + await Control.UpdateBitmap(_element, previous); + } + void UpdateAspect() { + if (_element == null || _disposed) + { + return; + } + ScaleType type = _element.Aspect.ToScaleType(); SetScaleType(type); } diff --git a/Xamarin.Forms.Platform.Android/Renderers/FileImageSourceHandler.cs b/Xamarin.Forms.Platform.Android/Renderers/FileImageSourceHandler.cs index 93124fef..95d66fb7 100644 --- a/Xamarin.Forms.Platform.Android/Renderers/FileImageSourceHandler.cs +++ b/Xamarin.Forms.Platform.Android/Renderers/FileImageSourceHandler.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Android.Content; using Android.Graphics; +using Xamarin.Forms.Internals; namespace Xamarin.Forms.Platform.Android { @@ -17,10 +18,18 @@ namespace Xamarin.Forms.Platform.Android public async Task<Bitmap> LoadImageAsync(ImageSource imagesource, Context context, CancellationToken cancelationToken = default(CancellationToken)) { string file = ((FileImageSource)imagesource).File; + Bitmap bitmap; if (File.Exists (file)) - return !DecodeSynchronously ? (await BitmapFactory.DecodeFileAsync (file).ConfigureAwait (false)) : BitmapFactory.DecodeFile (file); + bitmap = !DecodeSynchronously ? (await BitmapFactory.DecodeFileAsync (file).ConfigureAwait (false)) : BitmapFactory.DecodeFile (file); else - return !DecodeSynchronously ? (await context.Resources.GetBitmapAsync (file).ConfigureAwait (false)) : context.Resources.GetBitmap (file); + bitmap = !DecodeSynchronously ? (await context.Resources.GetBitmapAsync (file).ConfigureAwait (false)) : context.Resources.GetBitmap (file); + + if (bitmap == null) + { + Log.Warning(nameof(FileImageSourceHandler), "Could not find image or image file was invalid: {0}", imagesource); + } + + return bitmap; } } }
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/ImageLoaderSourceHandler.cs b/Xamarin.Forms.Platform.Android/Renderers/ImageLoaderSourceHandler.cs index 8d0ae3d9..b22ca485 100644 --- a/Xamarin.Forms.Platform.Android/Renderers/ImageLoaderSourceHandler.cs +++ b/Xamarin.Forms.Platform.Android/Renderers/ImageLoaderSourceHandler.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Android.Content; using Android.Graphics; +using Xamarin.Forms.Internals; namespace Xamarin.Forms.Platform.Android { @@ -11,12 +12,19 @@ namespace Xamarin.Forms.Platform.Android public async Task<Bitmap> LoadImageAsync(ImageSource imagesource, Context context, CancellationToken cancelationToken = default(CancellationToken)) { var imageLoader = imagesource as UriImageSource; - if (imageLoader != null && imageLoader.Uri != null) + Bitmap bitmap = null; + if (imageLoader?.Uri != null) { using (Stream imageStream = await imageLoader.GetStreamAsync(cancelationToken).ConfigureAwait(false)) - return await BitmapFactory.DecodeStreamAsync(imageStream).ConfigureAwait(false); + bitmap = await BitmapFactory.DecodeStreamAsync(imageStream).ConfigureAwait(false); } - return null; + + if (bitmap == null) + { + Log.Warning(nameof(ImageLoaderSourceHandler), "Could not retrieve image or image data was invalid: {0}", imageLoader); + } + + return bitmap; } } }
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/ImageRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/ImageRenderer.cs index 57937d89..43dac1fe 100644 --- a/Xamarin.Forms.Platform.Android/Renderers/ImageRenderer.cs +++ b/Xamarin.Forms.Platform.Android/Renderers/ImageRenderer.cs @@ -1,10 +1,10 @@ using System; using System.ComponentModel; +using System.Threading.Tasks; using Android.Graphics; using Android.Views; using AImageView = Android.Widget.ImageView; using Xamarin.Forms.Internals; -using static Xamarin.Forms.Platform.Android.ImageViewExtensions; namespace Xamarin.Forms.Platform.Android { @@ -20,7 +20,6 @@ namespace Xamarin.Forms.Platform.Android public ImageRenderer() { - System.Diagnostics.Debug.WriteLine(">>>>> Old Image Renderer"); AutoPackage = false; } @@ -39,7 +38,7 @@ namespace Xamarin.Forms.Platform.Android return new FormsImageView(Context); } - protected override void OnElementChanged(ElementChangedEventArgs<Image> e) + protected override async void OnElementChanged(ElementChangedEventArgs<Image> e) { base.OnElementChanged(e); @@ -50,28 +49,63 @@ namespace Xamarin.Forms.Platform.Android } _motionEventHelper.UpdateElement(e.NewElement); - - Control.UpdateBitmap(e.NewElement, e.OldElement); + + await TryUpdateBitmap(e.OldElement); UpdateAspect(); } - protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + protected override async void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) { base.OnElementPropertyChanged(sender, e); if (e.PropertyName == Image.SourceProperty.PropertyName) - Control.UpdateBitmap(Element); + await TryUpdateBitmap(); else if (e.PropertyName == Image.AspectProperty.PropertyName) UpdateAspect(); } void UpdateAspect() { + if (Element == null || Control == null || Control.IsDisposed()) + { + return; + } + AImageView.ScaleType type = Element.Aspect.ToScaleType(); Control.SetScaleType(type); } + protected virtual async Task TryUpdateBitmap(Image previous = null) + { + // By default we'll just catch and log any exceptions thrown by UpdateBitmap so they don't bring down + // the application; a custom renderer can override this method and handle exceptions from + // UpdateBitmap differently if it wants to + + try + { + await UpdateBitmap(previous); + } + catch (Exception ex) + { + Log.Warning(nameof(ImageRenderer), "Error loading image: {0}", ex); + } + finally + { + ((IImageController)Element)?.SetIsLoading(false); + } + } + + protected async Task UpdateBitmap(Image previous = null) + { + if (Element == null || Control == null || Control.IsDisposed()) + { + return; + } + + await Control.UpdateBitmap(Element, previous); + } + public override bool OnTouchEvent(MotionEvent e) { if (base.OnTouchEvent(e)) diff --git a/Xamarin.Forms.Platform.Android/Renderers/StreamImagesourceHandler.cs b/Xamarin.Forms.Platform.Android/Renderers/StreamImagesourceHandler.cs index 3128995d..a46e3abc 100644 --- a/Xamarin.Forms.Platform.Android/Renderers/StreamImagesourceHandler.cs +++ b/Xamarin.Forms.Platform.Android/Renderers/StreamImagesourceHandler.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Android.Content; using Android.Graphics; +using Xamarin.Forms.Internals; namespace Xamarin.Forms.Platform.Android { @@ -11,12 +12,19 @@ namespace Xamarin.Forms.Platform.Android public async Task<Bitmap> LoadImageAsync(ImageSource imagesource, Context context, CancellationToken cancelationToken = default(CancellationToken)) { var streamsource = imagesource as StreamImageSource; - if (streamsource != null && streamsource.Stream != null) + Bitmap bitmap = null; + if (streamsource?.Stream != null) { using (Stream stream = await ((IStreamImageSource)streamsource).GetStreamAsync(cancelationToken).ConfigureAwait(false)) - return await BitmapFactory.DecodeStreamAsync(stream).ConfigureAwait(false); + bitmap = await BitmapFactory.DecodeStreamAsync(stream).ConfigureAwait(false); } - return null; + + if (bitmap == null) + { + Log.Warning(nameof(ImageLoaderSourceHandler), "Image data was invalid: {0}", streamsource); + } + + return bitmap; } } }
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Xamarin.Forms.Platform.Android.csproj b/Xamarin.Forms.Platform.Android/Xamarin.Forms.Platform.Android.csproj index bf5344ff..5100b873 100644 --- a/Xamarin.Forms.Platform.Android/Xamarin.Forms.Platform.Android.csproj +++ b/Xamarin.Forms.Platform.Android/Xamarin.Forms.Platform.Android.csproj @@ -103,6 +103,7 @@ <Compile Include="AndroidApplicationLifecycleState.cs" /> <Compile Include="AndroidTitleBarVisibility.cs" /> <Compile Include="AppCompat\FrameRenderer.cs" /> + <Compile Include="Extensions\JavaObjectExtensions.cs" /> <Compile Include="FastRenderers\AccessibilityProvider.cs" /> <Compile Include="FastRenderers\ButtonRenderer.cs" /> <Compile Include="AppCompat\FormsViewPager.cs" /> diff --git a/Xamarin.Forms.Platform.WinRT/ImageRenderer.cs b/Xamarin.Forms.Platform.WinRT/ImageRenderer.cs index 8798b20e..56026d18 100644 --- a/Xamarin.Forms.Platform.WinRT/ImageRenderer.cs +++ b/Xamarin.Forms.Platform.WinRT/ImageRenderer.cs @@ -17,6 +17,7 @@ namespace Xamarin.Forms.Platform.WinRT public class ImageRenderer : ViewRenderer<Image, Windows.UI.Xaml.Controls.Image> { bool _measured; + bool _disposed; public override SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint) { @@ -32,10 +33,20 @@ namespace Xamarin.Forms.Platform.WinRT protected override void Dispose(bool disposing) { - if (Control != null) + if (_disposed) { - Control.ImageOpened -= OnImageOpened; - Control.ImageFailed -= OnImageFailed; + return; + } + + _disposed = true; + + if (disposing) + { + if (Control != null) + { + Control.ImageOpened -= OnImageOpened; + Control.ImageFailed -= OnImageFailed; + } } base.Dispose(disposing); @@ -55,7 +66,7 @@ namespace Xamarin.Forms.Platform.WinRT SetNativeControl(image); } - await UpdateSource(); + await TryUpdateSource(); UpdateAspect(); } } @@ -65,7 +76,7 @@ namespace Xamarin.Forms.Platform.WinRT base.OnElementPropertyChanged(sender, e); if (e.PropertyName == Image.SourceProperty.PropertyName) - await UpdateSource(); + await TryUpdateSource(); else if (e.PropertyName == Image.AspectProperty.PropertyName) UpdateAspect(); } @@ -94,7 +105,7 @@ namespace Xamarin.Forms.Platform.WinRT Element?.SetIsLoading(false); } - void OnImageFailed(object sender, ExceptionRoutedEventArgs exceptionRoutedEventArgs) + protected virtual void OnImageFailed(object sender, ExceptionRoutedEventArgs exceptionRoutedEventArgs) { Log.Warning("Image Loading", $"Image failed to load: {exceptionRoutedEventArgs.ErrorMessage}" ); Element?.SetIsLoading(false); @@ -107,6 +118,11 @@ namespace Xamarin.Forms.Platform.WinRT void UpdateAspect() { + if (_disposed || Element == null || Control == null) + { + return; + } + Control.Stretch = GetStretch(Element.Aspect); if (Element.Aspect == Aspect.AspectFill) // Then Center Crop { @@ -120,15 +136,40 @@ namespace Xamarin.Forms.Platform.WinRT } } - async Task UpdateSource() + protected virtual async Task TryUpdateSource() { + // By default we'll just catch and log any exceptions thrown by UpdateSource so we don't bring down + // the application; a custom renderer can override this method and handle exceptions from + // UpdateSource differently if it wants to + + try + { + await UpdateSource().ConfigureAwait(false); + } + catch (Exception ex) + { + Log.Warning(nameof(ImageRenderer), "Error loading image: {0}", ex); + } + finally + { + ((IImageController)Element)?.SetIsLoading(false); + } + } + + protected async Task UpdateSource() + { + if (_disposed || Element == null || Control == null) + { + return; + } + Element.SetIsLoading(true); ImageSource source = Element.Source; IImageSourceHandler handler; if (source != null && (handler = Registrar.Registered.GetHandler<IImageSourceHandler>(source.GetType())) != null) { - Windows.UI.Xaml.Media.ImageSource imagesource = null; + Windows.UI.Xaml.Media.ImageSource imagesource; try { @@ -138,10 +179,6 @@ namespace Xamarin.Forms.Platform.WinRT { imagesource = null; } - catch (Exception ex) - { - Log.Warning("Image Loading", $"Error updating image source: {ex}"); - } // In the time it takes to await the imagesource, some zippy little app // might have disposed of this Image already. @@ -155,7 +192,7 @@ namespace Xamarin.Forms.Platform.WinRT else { Control.Source = null; - Element?.SetIsLoading(false); + Element.SetIsLoading(false); } } } diff --git a/Xamarin.Forms.Platform.iOS/Renderers/ImageRenderer.cs b/Xamarin.Forms.Platform.iOS/Renderers/ImageRenderer.cs index 6e11d96c..6bc28b4a 100644 --- a/Xamarin.Forms.Platform.iOS/Renderers/ImageRenderer.cs +++ b/Xamarin.Forms.Platform.iOS/Renderers/ImageRenderer.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Foundation; using UIKit; +using Xamarin.Forms.Internals; using RectangleF = CoreGraphics.CGRect; namespace Xamarin.Forms.Platform.iOS @@ -30,8 +31,6 @@ namespace Xamarin.Forms.Platform.iOS { bool _isDisposed; - IElementController ElementController => Element as IElementController; - protected override void Dispose(bool disposing) { if (_isDisposed) @@ -43,7 +42,6 @@ namespace Xamarin.Forms.Platform.iOS if (Control != null && (oldUIImage = Control.Image) != null) { oldUIImage.Dispose(); - oldUIImage = null; } } @@ -52,7 +50,7 @@ namespace Xamarin.Forms.Platform.iOS base.Dispose(disposing); } - protected override void OnElementChanged(ElementChangedEventArgs<Image> e) + protected override async void OnElementChanged(ElementChangedEventArgs<Image> e) { if (Control == null) { @@ -65,18 +63,18 @@ namespace Xamarin.Forms.Platform.iOS if (e.NewElement != null) { SetAspect(); - SetImage(e.OldElement); + await TrySetImage(e.OldElement); SetOpacity(); } base.OnElementChanged(e); } - protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + protected override async void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) { base.OnElementPropertyChanged(sender, e); if (e.PropertyName == Image.SourceProperty.PropertyName) - SetImage(); + await TrySetImage(); else if (e.PropertyName == Image.IsOpaqueProperty.PropertyName) SetOpacity(); else if (e.PropertyName == Image.AspectProperty.PropertyName) @@ -85,11 +83,41 @@ namespace Xamarin.Forms.Platform.iOS void SetAspect() { + if (_isDisposed || Element == null || Control == null) + { + return; + } + Control.ContentMode = Element.Aspect.ToUIViewContentMode(); } - async void SetImage(Image oldElement = null) + protected virtual async Task TrySetImage(Image previous = null) { + // By default we'll just catch and log any exceptions thrown by SetImage so they don't bring down + // the application; a custom renderer can override this method and handle exceptions from + // SetImage differently if it wants to + + try + { + await SetImage(previous).ConfigureAwait(false); + } + catch (Exception ex) + { + Log.Warning(nameof(ImageRenderer), "Error loading image: {0}", ex); + } + finally + { + ((IImageController)Element)?.SetIsLoading(false); + } + } + + protected async Task SetImage(Image oldElement = null) + { + if (_isDisposed || Element == null || Control == null) + { + return; + } + var source = Element.Source; if (oldElement != null) @@ -108,7 +136,8 @@ namespace Xamarin.Forms.Platform.iOS Element.SetIsLoading(true); - if (source != null && (handler = Internals.Registrar.Registered.GetHandler<IImageSourceHandler>(source.GetType())) != null) + if (source != null && + (handler = Internals.Registrar.Registered.GetHandler<IImageSourceHandler>(source.GetType())) != null) { UIImage uiimage; try @@ -120,22 +149,30 @@ namespace Xamarin.Forms.Platform.iOS uiimage = null; } + if (_isDisposed) + return; + var imageView = Control; if (imageView != null) imageView.Image = uiimage; - if (!_isDisposed) - ((IVisualElementController)Element).NativeSizeChanged(); + ((IVisualElementController)Element).NativeSizeChanged(); } else + { Control.Image = null; + } - if (!_isDisposed) - Element.SetIsLoading(false); + Element.SetIsLoading(false); } void SetOpacity() { + if (_isDisposed || Element == null || Control == null) + { + return; + } + Control.Opaque = Element.IsOpaque; } } @@ -151,12 +188,15 @@ namespace Xamarin.Forms.Platform.iOS { UIImage image = null; var filesource = imagesource as FileImageSource; - if (filesource != null) + var file = filesource?.File; + if (!string.IsNullOrEmpty(file)) + image = File.Exists(file) ? new UIImage(file) : UIImage.FromBundle(file); + + if (image == null) { - var file = filesource.File; - if (!string.IsNullOrEmpty(file)) - image = File.Exists(file) ? new UIImage(file) : UIImage.FromBundle(file); + Log.Warning(nameof(FileImageSourceHandler), "Could not find image: {0}", imagesource); } + return Task.FromResult(image); } } @@ -167,7 +207,7 @@ namespace Xamarin.Forms.Platform.iOS { UIImage image = null; var streamsource = imagesource as StreamImageSource; - if (streamsource != null && streamsource.Stream != null) + if (streamsource?.Stream != null) { using (var streamImage = await ((IStreamImageSource)streamsource).GetStreamAsync(cancelationToken).ConfigureAwait(false)) { @@ -175,6 +215,12 @@ namespace Xamarin.Forms.Platform.iOS image = UIImage.LoadFromData(NSData.FromStream(streamImage), scale); } } + + if (image == null) + { + Log.Warning(nameof(StreamImagesourceHandler), "Could not load image: {0}", streamsource); + } + return image; } } @@ -185,7 +231,7 @@ namespace Xamarin.Forms.Platform.iOS { UIImage image = null; var imageLoader = imagesource as UriImageSource; - if (imageLoader != null && imageLoader.Uri != null) + if (imageLoader?.Uri != null) { using (var streamImage = await imageLoader.GetStreamAsync(cancelationToken).ConfigureAwait(false)) { @@ -193,6 +239,12 @@ namespace Xamarin.Forms.Platform.iOS image = UIImage.LoadFromData(NSData.FromStream(streamImage), scale); } } + + if (image == null) + { + Log.Warning(nameof(ImageLoaderSourceHandler), "Could not load image: {0}", imageLoader); + } + return image; } } diff --git a/docs/Xamarin.Forms.Core/Xamarin.Forms/FileImageSource.xml b/docs/Xamarin.Forms.Core/Xamarin.Forms/FileImageSource.xml index 61fe4dcc..cdd27028 100644 --- a/docs/Xamarin.Forms.Core/Xamarin.Forms/FileImageSource.xml +++ b/docs/Xamarin.Forms.Core/Xamarin.Forms/FileImageSource.xml @@ -183,5 +183,22 @@ <remarks>To be added.</remarks> </Docs> </Member> + <Member MemberName="ToString"> + <MemberSignature Language="C#" Value="public override string ToString ();" /> + <MemberSignature Language="ILAsm" Value=".method public hidebysig virtual instance string ToString() cil managed" /> + <MemberType>Method</MemberType> + <AssemblyInfo> + <AssemblyVersion>2.0.0.0</AssemblyVersion> + </AssemblyInfo> + <ReturnValue> + <ReturnType>System.String</ReturnType> + </ReturnValue> + <Parameters /> + <Docs> + <summary>To be added.</summary> + <returns>To be added.</returns> + <remarks>To be added.</remarks> + </Docs> + </Member> </Members> </Type> diff --git a/docs/Xamarin.Forms.Core/Xamarin.Forms/UriImageSource.xml b/docs/Xamarin.Forms.Core/Xamarin.Forms/UriImageSource.xml index 889b02df..303717b8 100644 --- a/docs/Xamarin.Forms.Core/Xamarin.Forms/UriImageSource.xml +++ b/docs/Xamarin.Forms.Core/Xamarin.Forms/UriImageSource.xml @@ -114,6 +114,23 @@ <remarks>To be added.</remarks> </Docs> </Member> + <Member MemberName="ToString"> + <MemberSignature Language="C#" Value="public override string ToString ();" /> + <MemberSignature Language="ILAsm" Value=".method public hidebysig virtual instance string ToString() cil managed" /> + <MemberType>Method</MemberType> + <AssemblyInfo> + <AssemblyVersion>2.0.0.0</AssemblyVersion> + </AssemblyInfo> + <ReturnValue> + <ReturnType>System.String</ReturnType> + </ReturnValue> + <Parameters /> + <Docs> + <summary>To be added.</summary> + <returns>To be added.</returns> + <remarks>To be added.</remarks> + </Docs> + </Member> <Member MemberName="Uri"> <MemberSignature Language="C#" Value="public Uri Uri { get; set; }" /> <MemberSignature Language="ILAsm" Value=".property instance class System.Uri Uri" /> |