From e24db507fd20ec0cde8850caf1774aba93881a70 Mon Sep 17 00:00:00 2001 From: michael-hawker <24302614+michael-hawker@users.noreply.github.com> Date: Fri, 5 Feb 2021 17:15:12 -0800 Subject: [PATCH 1/6] Remove Custom Caching Strategy from ImageEx Too Large an Impact on App Binary Size 675kb --- .../ImageEx/CachingStrategy.cs | 23 ------- .../ImageEx/ImageExBase.Members.cs | 14 ---- .../ImageEx/ImageExBase.Source.cs | 64 +------------------ 3 files changed, 1 insertion(+), 100 deletions(-) delete mode 100644 Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/CachingStrategy.cs diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/CachingStrategy.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/CachingStrategy.cs deleted file mode 100644 index 9db1d3e1ed6..00000000000 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/CachingStrategy.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -namespace Microsoft.Toolkit.Uwp.UI.Controls -{ - /// - /// The type of caching to be applied to . - /// Default is - /// - public enum ImageExCachingStrategy - { - /// - /// Caching is handled by 's custom caching system. - /// - Custom, - - /// - /// Caching is handled internally by UWP. - /// - Internal - } -} diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.Members.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.Members.cs index 55db3a5f391..abc687227e8 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.Members.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.Members.cs @@ -40,11 +40,6 @@ public partial class ImageExBase /// public static readonly DependencyProperty IsCacheEnabledProperty = DependencyProperty.Register(nameof(IsCacheEnabled), typeof(bool), typeof(ImageExBase), new PropertyMetadata(false)); - /// - /// Identifies the dependency property. - /// - public static readonly DependencyProperty CachingStrategyProperty = DependencyProperty.Register(nameof(CachingStrategy), typeof(ImageExCachingStrategy), typeof(ImageExBase), new PropertyMetadata(ImageExCachingStrategy.Custom)); - /// /// Identifies the dependency property. /// @@ -126,15 +121,6 @@ public bool IsCacheEnabled set { SetValue(IsCacheEnabledProperty, value); } } - /// - /// Gets or sets a value indicating how the will be cached. - /// - public ImageExCachingStrategy CachingStrategy - { - get { return (ImageExCachingStrategy)GetValue(CachingStrategyProperty); } - set { SetValue(CachingStrategyProperty, value); } - } - /// /// Gets or sets a value indicating whether gets or sets is lazy loading enable. (17763 or higher supported) /// diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.Source.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.Source.cs index 2b13c681121..2502c808363 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.Source.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.Source.cs @@ -138,17 +138,7 @@ private async Task LoadImageAsync(Uri imageUri) { if (IsCacheEnabled) { - switch (CachingStrategy) - { - case ImageExCachingStrategy.Custom when _isHttpSource: - await SetHttpSourceCustomCached(imageUri); - break; - case ImageExCachingStrategy.Custom: - case ImageExCachingStrategy.Internal: - default: - AttachSource(new BitmapImage(imageUri)); - break; - } + AttachSource(new BitmapImage(imageUri)); } else if (string.Equals(_uri.Scheme, "data", StringComparison.OrdinalIgnoreCase)) { @@ -172,57 +162,5 @@ private async Task LoadImageAsync(Uri imageUri) } } } - - private async Task SetHttpSourceCustomCached(Uri imageUri) - { - try - { - var propValues = new List>(); - - if (DecodePixelHeight > 0) - { - propValues.Add(new KeyValuePair(nameof(DecodePixelHeight), DecodePixelHeight)); - } - - if (DecodePixelWidth > 0) - { - propValues.Add(new KeyValuePair(nameof(DecodePixelWidth), DecodePixelWidth)); - } - - if (propValues.Count > 0) - { - propValues.Add(new KeyValuePair(nameof(DecodePixelType), DecodePixelType)); - } - - var img = await ImageCache.Instance.GetFromCacheAsync(imageUri, true, _tokenSource.Token, propValues); - - lock (LockObj) - { - // If you have many imageEx in a virtualized ListView for instance - // controls will be recycled and the uri will change while waiting for the previous one to load - if (_uri == imageUri) - { - AttachSource(img); - ImageExOpened?.Invoke(this, new ImageExOpenedEventArgs()); - VisualStateManager.GoToState(this, LoadedState, true); - } - } - } - catch (OperationCanceledException) - { - // nothing to do as cancellation has been requested. - } - catch (Exception e) - { - lock (LockObj) - { - if (_uri == imageUri) - { - ImageExFailed?.Invoke(this, new ImageExFailedEventArgs(e)); - VisualStateManager.GoToState(this, FailedState, true); - } - } - } - } } } \ No newline at end of file From c43f68a0e161971fc5d7153f1d6fdadb8fb8c955 Mon Sep 17 00:00:00 2001 From: michael-hawker <24302614+michael-hawker@users.noreply.github.com> Date: Fri, 5 Feb 2021 23:00:15 -0800 Subject: [PATCH 2/6] Remove old Unused ProgressRing from ImageEx control Fixes #3741 --- .../ImageEx/ImageExBase.cs | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.cs index f3f749a97d0..dce3e4f0598 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.cs @@ -21,7 +21,6 @@ namespace Microsoft.Toolkit.Uwp.UI.Controls [TemplateVisualState(Name = UnloadedState, GroupName = CommonGroup)] [TemplateVisualState(Name = FailedState, GroupName = CommonGroup)] [TemplatePart(Name = PartImage, Type = typeof(object))] - [TemplatePart(Name = PartProgress, Type = typeof(ProgressRing))] public abstract partial class ImageExBase : Control { private bool _isInViewport; @@ -31,11 +30,6 @@ public abstract partial class ImageExBase : Control /// protected const string PartImage = "Image"; - /// - /// ProgressRing name in template - /// - protected const string PartProgress = "Progress"; - /// /// VisualStates name in template /// @@ -66,11 +60,6 @@ public abstract partial class ImageExBase : Control /// protected object Image { get; private set; } - /// - /// Gets backing object for the ProgressRing - /// - protected ProgressRing Progress { get; private set; } - /// /// Gets object used for lock /// @@ -169,7 +158,6 @@ protected override void OnApplyTemplate() RemoveImageFailed(OnImageFailed); Image = GetTemplateChild(PartImage) as object; - Progress = GetTemplateChild(PartProgress) as ProgressRing; IsInitialized = true; @@ -191,19 +179,6 @@ protected override void OnApplyTemplate() base.OnApplyTemplate(); } - /// - protected override Size ArrangeOverride(Size finalSize) - { - var newSquareSize = Math.Min(finalSize.Width, finalSize.Height) / 8.0; - - if (Progress?.Width == newSquareSize) - { - Progress.Height = newSquareSize; - } - - return base.ArrangeOverride(finalSize); - } - private void OnImageOpened(object sender, RoutedEventArgs e) { ImageExOpened?.Invoke(this, new ImageExOpenedEventArgs()); From 628d1d5202ecf2d46959bcee832a6a7f64af589b Mon Sep 17 00:00:00 2001 From: michael-hawker <24302614+michael-hawker@users.noreply.github.com> Date: Thu, 11 Feb 2021 11:15:21 -0800 Subject: [PATCH 3/6] Update ImageEx to support custom implementation of Caching by subclassing ImageExBase/ImageEx Removed class level fields which were really local variables. Made new virtual methods. Still works fine in Sample App. --- .../ImageEx/ImageExBase.Source.cs | 121 +++++++++++++++--- .../ImageEx/ImageExBase.cs | 20 +-- ...rosoft.Toolkit.Uwp.UI.Controls.Core.csproj | 2 +- 3 files changed, 115 insertions(+), 28 deletions(-) diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.Source.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.Source.cs index 2502c808363..b83a43bb2cb 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.Source.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.Source.cs @@ -24,9 +24,11 @@ public partial class ImageExBase /// public static readonly DependencyProperty SourceProperty = DependencyProperty.Register(nameof(Source), typeof(object), typeof(ImageExBase), new PropertyMetadata(null, SourceChanged)); - private Uri _uri; - private bool _isHttpSource; - private CancellationTokenSource _tokenSource = null; + /// + /// Gets value tracking the currently requested source Uri. This can be helpful to use when implementing where loading an image from a cache takes longer and the current container has been recycled and is no longer valid since a new image has been set. + /// + protected Uri CurrentSourceUri { get; private set; } + private object _lazyLoadingSource; /// @@ -66,7 +68,11 @@ private static bool IsHttpUri(Uri uri) return uri.IsAbsoluteUri && (uri.Scheme == "http" || uri.Scheme == "https"); } - private void AttachSource(ImageSource source) + /// + /// Method to call to assign an value to the underlying powering . + /// + /// to assign to the image. + protected void AttachSource(ImageSource source) { var image = Image as Image; var brush = Image as ImageBrush; @@ -88,9 +94,7 @@ private async void SetSource(object source) return; } - this._tokenSource?.Cancel(); - - this._tokenSource = new CancellationTokenSource(); + OnNewSourceRequested(source); AttachSource(null); @@ -112,37 +116,38 @@ private async void SetSource(object source) return; } - _uri = source as Uri; - if (_uri == null) + CurrentSourceUri = source as Uri; + if (CurrentSourceUri == null) { var url = source as string ?? source.ToString(); - if (!Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out _uri)) + if (!Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out Uri uri)) { VisualStateManager.GoToState(this, FailedState, true); return; } + + CurrentSourceUri = uri; } - _isHttpSource = IsHttpUri(_uri); - if (!_isHttpSource && !_uri.IsAbsoluteUri) + if (!IsHttpUri(CurrentSourceUri) && !CurrentSourceUri.IsAbsoluteUri) { - _uri = new Uri("ms-appx:///" + _uri.OriginalString.TrimStart('/')); + CurrentSourceUri = new Uri("ms-appx:///" + CurrentSourceUri.OriginalString.TrimStart('/')); } - await LoadImageAsync(_uri); + await LoadImageAsync(CurrentSourceUri); } private async Task LoadImageAsync(Uri imageUri) { - if (_uri != null) + if (imageUri != null) { if (IsCacheEnabled) { - AttachSource(new BitmapImage(imageUri)); + await ProvideCachedResourceAsync(imageUri); } - else if (string.Equals(_uri.Scheme, "data", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(imageUri.Scheme, "data", StringComparison.OrdinalIgnoreCase)) { - var source = _uri.OriginalString; + var source = imageUri.OriginalString; const string base64Head = "base64,"; var index = source.IndexOf(base64Head); if (index >= 0) @@ -155,12 +160,90 @@ private async Task LoadImageAsync(Uri imageUri) } else { - AttachSource(new BitmapImage(_uri) + AttachSource(new BitmapImage(imageUri) { CreateOptions = BitmapCreateOptions.IgnoreImageCache }); } } } + + /// + /// This method is provided in case a developer would like their own custom caching strategy for . + /// By default it uses the built-in UWP cache provided by and + /// the control itself. Call to set + /// the retrieved cache value to the image. + /// + /// + /// + /// try + /// { + /// var propValues = new List<KeyValuePair<string, object>>(); + /// + /// if (DecodePixelHeight > 0) + /// { + /// propValues.Add(new KeyValuePair<string, object>(nameof(DecodePixelHeight), D ecodePixelHeight)); + /// } + /// if (DecodePixelWidth > 0) + /// { + /// propValues.Add(new KeyValuePair<string, object>(nameof(DecodePixelWidth), D ecodePixelWidth)); + /// } + /// if (propValues.Count > 0) + /// { + /// propValues.Add(new KeyValuePair<string, object>(nameof(DecodePixelType), DecodePixelType)); + /// } + /// + /// // A token could be provided here as well to cancel the request to the cache, + /// // if a new image is requested. That token can be canceled in the OnNewSourceRequested method. + /// var img = await ImageCache.Instance.GetFromCacheAsync(imageUri, true, initializerKeyValues: propValues); + /// + /// lock (LockObj) + /// { + /// // If you have many imageEx in a virtualized ListView for instance + /// // controls will be recycled and the uri will change while waiting for the previous one to load + /// if (_currentSourceUri == imageUri) + /// { + /// AttachSource(img); + /// ImageExOpened?.Invoke(this, new ImageExOpenedEventArgs()); + /// VisualStateManager.GoToState(this, LoadedState, true); + /// } + /// } + /// } + /// catch (OperationCanceledException) + /// { + /// // nothing to do as cancellation has been requested. + /// } + /// catch (Exception e) + /// { + /// lock (LockObj) + /// { + /// if (_currentSourceUri == imageUri) + /// { + /// ImageExFailed?.Invoke(this, new ImageExFailedEventArgs(e)); + /// VisualStateManager.GoToState(this, FailedState, true); + /// } + /// } + /// } + /// + /// + /// of the image to load from the cache. + /// + protected virtual Task ProvideCachedResourceAsync(Uri imageUri) + { + AttachSource(new BitmapImage(imageUri)); + + return Task.CompletedTask; + } + + /// + /// This method is called when a new source is requested by the control. This can be useful when + /// implementing a custom caching strategy to cancel any open request on the cache if a new + /// request comes in due to container recycling before the previous one has completed. + /// Be default, this method does nothing. + /// + /// Incoming requested source. + protected virtual void OnNewSourceRequested(object source) + { + } } } \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.cs index dce3e4f0598..91b03d4950d 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.cs @@ -60,17 +60,11 @@ public abstract partial class ImageExBase : Control /// protected object Image { get; private set; } - /// - /// Gets object used for lock - /// - protected object LockObj { get; private set; } - /// /// Initializes a new instance of the class. /// public ImageExBase() { - LockObj = new object(); } /// @@ -179,13 +173,23 @@ protected override void OnApplyTemplate() base.OnApplyTemplate(); } - private void OnImageOpened(object sender, RoutedEventArgs e) + /// + /// Underlying event handler. + /// + /// Image + /// Event Arguments + protected virtual void OnImageOpened(object sender, RoutedEventArgs e) { ImageExOpened?.Invoke(this, new ImageExOpenedEventArgs()); VisualStateManager.GoToState(this, LoadedState, true); } - private void OnImageFailed(object sender, ExceptionRoutedEventArgs e) + /// + /// Underlying event handler. + /// + /// Image + /// Event Arguments + protected virtual void OnImageFailed(object sender, ExceptionRoutedEventArgs e) { ImageExFailed?.Invoke(this, new ImageExFailedEventArgs(new Exception(e.ErrorMessage))); VisualStateManager.GoToState(this, FailedState, true); diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Core/Microsoft.Toolkit.Uwp.UI.Controls.Core.csproj b/Microsoft.Toolkit.Uwp.UI.Controls.Core/Microsoft.Toolkit.Uwp.UI.Controls.Core.csproj index b604c5a7219..973416dd091 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Core/Microsoft.Toolkit.Uwp.UI.Controls.Core.csproj +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Core/Microsoft.Toolkit.Uwp.UI.Controls.Core.csproj @@ -20,7 +20,7 @@ UWP Toolkit Windows Controls XAML Markdown CameraPreview Camera DropShadow ImageEx InAppNotification InfiniteCanvas Radial RadialProgressBar Scroll ScrollHeader Tile false - 8.0 + 9.0 From 9c1dcdf50aa5de252fbf9c46de39b98a4b226066 Mon Sep 17 00:00:00 2001 From: michael-hawker <24302614+michael-hawker@users.noreply.github.com> Date: Thu, 11 Feb 2021 11:16:10 -0800 Subject: [PATCH 4/6] Fix issues with ImageEx Lazy Loading Sample Can now use the sample button as a toggle. Close button is now visible. Made grid taller so sample is more apparent on high resolution monitors. --- .../SamplePages/ImageEx/ImageExLazyLoadingControl.xaml | 5 +++-- .../SamplePages/ImageEx/ImageExPage.xaml.cs | 10 +++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ImageEx/ImageExLazyLoadingControl.xaml b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ImageEx/ImageExLazyLoadingControl.xaml index a8f8b498d76..da20eed89e0 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ImageEx/ImageExLazyLoadingControl.xaml +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ImageEx/ImageExLazyLoadingControl.xaml @@ -11,7 +11,7 @@ - + + Click="CloseButton_Click" + Foreground="Black"> diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ImageEx/ImageExPage.xaml.cs b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ImageEx/ImageExPage.xaml.cs index f9acfd9f44e..db75a41ca6f 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ImageEx/ImageExPage.xaml.cs +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ImageEx/ImageExPage.xaml.cs @@ -82,7 +82,15 @@ private async void Load() if (lazyLoadingControlHost != null) { - lazyLoadingControlHost.Child = imageExLazyLoadingControl; + // Allow this to act as a toggle. + if (lazyLoadingControlHost.Child == null) + { + lazyLoadingControlHost.Child = imageExLazyLoadingControl; + } + else + { + lazyLoadingControlHost.Child = null; + } } }); From 823984142e82e69dfc3cb16988ea5eee2acc2056 Mon Sep 17 00:00:00 2001 From: michael-hawker <24302614+michael-hawker@users.noreply.github.com> Date: Thu, 11 Feb 2021 15:04:00 -0800 Subject: [PATCH 5/6] Updated method name and provided more guidance in comments. --- .../ImageEx/ImageExBase.Source.cs | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.Source.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.Source.cs index b83a43bb2cb..5a6fce1afd5 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.Source.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.Source.cs @@ -25,7 +25,7 @@ public partial class ImageExBase public static readonly DependencyProperty SourceProperty = DependencyProperty.Register(nameof(Source), typeof(object), typeof(ImageExBase), new PropertyMetadata(null, SourceChanged)); /// - /// Gets value tracking the currently requested source Uri. This can be helpful to use when implementing where loading an image from a cache takes longer and the current container has been recycled and is no longer valid since a new image has been set. + /// Gets value tracking the currently requested source Uri. This can be helpful to use when implementing where loading an image from a cache takes longer and the current container has been recycled and is no longer valid since a new image has been set. /// protected Uri CurrentSourceUri { get; private set; } @@ -143,7 +143,7 @@ private async Task LoadImageAsync(Uri imageUri) { if (IsCacheEnabled) { - await ProvideCachedResourceAsync(imageUri); + await AttachCachedResourceAsync(imageUri); } else if (string.Equals(imageUri.Scheme, "data", StringComparison.OrdinalIgnoreCase)) { @@ -171,8 +171,13 @@ private async Task LoadImageAsync(Uri imageUri) /// /// This method is provided in case a developer would like their own custom caching strategy for . /// By default it uses the built-in UWP cache provided by and - /// the control itself. Call to set - /// the retrieved cache value to the image. + /// the control itself. This method should call + /// to set the retrieved cache value to the image. may be checked + /// after retrieving a cached image to ensure that the current resource requested matches the one + /// requested by the parameter. + /// may be used in order to signal any cancellation events + /// using a to the call to the cache, for instance like the Toolkit's + /// own in . /// /// /// @@ -201,7 +206,7 @@ private async Task LoadImageAsync(Uri imageUri) /// { /// // If you have many imageEx in a virtualized ListView for instance /// // controls will be recycled and the uri will change while waiting for the previous one to load - /// if (_currentSourceUri == imageUri) + /// if (CurrentSourceUri == imageUri) /// { /// AttachSource(img); /// ImageExOpened?.Invoke(this, new ImageExOpenedEventArgs()); @@ -217,7 +222,7 @@ private async Task LoadImageAsync(Uri imageUri) /// { /// lock (LockObj) /// { - /// if (_currentSourceUri == imageUri) + /// if (CurrentSourceUri == imageUri) /// { /// ImageExFailed?.Invoke(this, new ImageExFailedEventArgs(e)); /// VisualStateManager.GoToState(this, FailedState, true); @@ -228,8 +233,9 @@ private async Task LoadImageAsync(Uri imageUri) /// /// of the image to load from the cache. /// - protected virtual Task ProvideCachedResourceAsync(Uri imageUri) + protected virtual Task AttachCachedResourceAsync(Uri imageUri) { + // By default we just use the built-in UWP image cache provided within the Image control. AttachSource(new BitmapImage(imageUri)); return Task.CompletedTask; From b3860217967741a1eee2c46a8f1392cd39cf46cd Mon Sep 17 00:00:00 2001 From: michael-hawker <24302614+michael-hawker@users.noreply.github.com> Date: Tue, 16 Feb 2021 16:21:44 -0800 Subject: [PATCH 6/6] Apply PR feedback - Use CancellationToken instead, Simplify calling pattern Automatically handle failure cases for image loading, added comment to clarify events. Do better type checks & cleaned-up calls to type conversions --- .../SamplePages/ImageEx/ImageExPage.xaml.cs | 7 +- .../ImageEx/ImageEx.Members.cs | 15 +- .../ImageEx/ImageExBase.Source.cs | 144 ++++++++---------- .../ImageEx/ImageExBase.cs | 28 +--- 4 files changed, 84 insertions(+), 110 deletions(-) diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ImageEx/ImageExPage.xaml.cs b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ImageEx/ImageExPage.xaml.cs index db75a41ca6f..5d9eebfa33a 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ImageEx/ImageExPage.xaml.cs +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ImageEx/ImageExPage.xaml.cs @@ -94,12 +94,7 @@ private async void Load() } }); - SampleController.Current.RegisterNewCommand("Clear image cache", async (sender, args) => - { - container?.Children?.Clear(); - GC.Collect(); // Force GC to free file locks - await ImageCache.Instance.ClearAsync(); - }); + SampleController.Current.RegisterNewCommand("Remove images", (sender, args) => container?.Children?.Clear()); await LoadDataAsync(); } diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageEx.Members.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageEx.Members.cs index 48d12972809..b0b63f55403 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageEx.Members.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageEx.Members.cs @@ -6,6 +6,7 @@ using Windows.UI.Composition; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Media; namespace Microsoft.Toolkit.Uwp.UI.Controls { @@ -33,7 +34,12 @@ public Thickness NineGrid /// public override CompositionBrush GetAlphaMask() { - return IsInitialized ? (Image as Image).GetAlphaMask() : null; + if (IsInitialized && Image is Image image) + { + return image.GetAlphaMask(); + } + + return null; } /// @@ -42,7 +48,12 @@ public override CompositionBrush GetAlphaMask() /// The image as a . public CastingSource GetAsCastingSource() { - return IsInitialized ? (Image as Image).GetAsCastingSource() : null; + if (IsInitialized && Image is Image image) + { + return image.GetAsCastingSource(); + } + + return null; } } } \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.Source.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.Source.cs index 5a6fce1afd5..da35c09ac7a 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.Source.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.Source.cs @@ -24,10 +24,8 @@ public partial class ImageExBase /// public static readonly DependencyProperty SourceProperty = DependencyProperty.Register(nameof(Source), typeof(object), typeof(ImageExBase), new PropertyMetadata(null, SourceChanged)); - /// - /// Gets value tracking the currently requested source Uri. This can be helpful to use when implementing where loading an image from a cache takes longer and the current container has been recycled and is no longer valid since a new image has been set. - /// - protected Uri CurrentSourceUri { get; private set; } + //// Used to track if we get a new request, so we can cancel any potential custom cache loading. + private CancellationTokenSource _tokenSource; private object _lazyLoadingSource; @@ -72,19 +70,24 @@ private static bool IsHttpUri(Uri uri) /// Method to call to assign an value to the underlying powering . /// /// to assign to the image. - protected void AttachSource(ImageSource source) + private void AttachSource(ImageSource source) { - var image = Image as Image; - var brush = Image as ImageBrush; - - if (image != null) + // Setting the source at this point should call ImageExOpened/VisualStateManager.GoToState + // as we register to both the ImageOpened/ImageFailed events of the underlying control. + // We only need to call those methods if we fail in other cases before we get here. + if (Image is Image image) { image.Source = source; } - else if (brush != null) + else if (Image is ImageBrush brush) { brush.ImageSource = source; } + + if (source == null) + { + VisualStateManager.GoToState(this, UnloadedState, true); + } } private async void SetSource(object source) @@ -94,13 +97,14 @@ private async void SetSource(object source) return; } - OnNewSourceRequested(source); + _tokenSource?.Cancel(); + + _tokenSource = new CancellationTokenSource(); AttachSource(null); if (source == null) { - VisualStateManager.GoToState(this, UnloadedState, true); return; } @@ -111,39 +115,54 @@ private async void SetSource(object source) { AttachSource(imageSource); - ImageExOpened?.Invoke(this, new ImageExOpenedEventArgs()); - VisualStateManager.GoToState(this, LoadedState, true); return; } - CurrentSourceUri = source as Uri; - if (CurrentSourceUri == null) + var uri = source as Uri; + if (uri == null) { var url = source as string ?? source.ToString(); - if (!Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out Uri uri)) + if (!Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out uri)) { + ImageExFailed?.Invoke(this, new ImageExFailedEventArgs(new UriFormatException("Invalid uri specified."))); VisualStateManager.GoToState(this, FailedState, true); return; } - - CurrentSourceUri = uri; } - if (!IsHttpUri(CurrentSourceUri) && !CurrentSourceUri.IsAbsoluteUri) + if (!IsHttpUri(uri) && !uri.IsAbsoluteUri) { - CurrentSourceUri = new Uri("ms-appx:///" + CurrentSourceUri.OriginalString.TrimStart('/')); + uri = new Uri("ms-appx:///" + uri.OriginalString.TrimStart('/')); } - await LoadImageAsync(CurrentSourceUri); + try + { + await LoadImageAsync(uri, _tokenSource.Token); + } + catch (OperationCanceledException) + { + // nothing to do as cancellation has been requested. + } + catch (Exception e) + { + ImageExFailed?.Invoke(this, new ImageExFailedEventArgs(e)); + VisualStateManager.GoToState(this, FailedState, true); + } } - private async Task LoadImageAsync(Uri imageUri) + private async Task LoadImageAsync(Uri imageUri, CancellationToken token) { if (imageUri != null) { if (IsCacheEnabled) { - await AttachCachedResourceAsync(imageUri); + var img = await ProvideCachedResourceAsync(imageUri, token); + + if (!_tokenSource.IsCancellationRequested) + { + // Only attach our image if we still have a valid request. + AttachSource(img); + } } else if (string.Equals(imageUri.Scheme, "data", StringComparison.OrdinalIgnoreCase)) { @@ -154,8 +173,12 @@ private async Task LoadImageAsync(Uri imageUri) { var bytes = Convert.FromBase64String(source.Substring(index + base64Head.Length)); var bitmap = new BitmapImage(); - AttachSource(bitmap); await bitmap.SetSourceAsync(new MemoryStream(bytes).AsRandomAccessStream()); + + if (!_tokenSource.IsCancellationRequested) + { + AttachSource(bitmap); + } } } else @@ -171,85 +194,42 @@ private async Task LoadImageAsync(Uri imageUri) /// /// This method is provided in case a developer would like their own custom caching strategy for . /// By default it uses the built-in UWP cache provided by and - /// the control itself. This method should call - /// to set the retrieved cache value to the image. may be checked - /// after retrieving a cached image to ensure that the current resource requested matches the one - /// requested by the parameter. - /// may be used in order to signal any cancellation events - /// using a to the call to the cache, for instance like the Toolkit's - /// own in . + /// the control itself. This method should return an + /// value of the image specified by the provided uri parameter. + /// A is provided in case the current request is invalidated + /// (e.g. the container is recycled before the original image is loaded). + /// The Toolkit also has an image cache helper which can be used as well: + /// in . /// /// /// - /// try - /// { /// var propValues = new List<KeyValuePair<string, object>>(); /// /// if (DecodePixelHeight > 0) /// { - /// propValues.Add(new KeyValuePair<string, object>(nameof(DecodePixelHeight), D ecodePixelHeight)); + /// propValues.Add(new KeyValuePair<string, object>(nameof(DecodePixelHeight), DecodePixelHeight)); /// } /// if (DecodePixelWidth > 0) /// { - /// propValues.Add(new KeyValuePair<string, object>(nameof(DecodePixelWidth), D ecodePixelWidth)); + /// propValues.Add(new KeyValuePair<string, object>(nameof(DecodePixelWidth), DecodePixelWidth)); /// } /// if (propValues.Count > 0) /// { /// propValues.Add(new KeyValuePair<string, object>(nameof(DecodePixelType), DecodePixelType)); /// } /// - /// // A token could be provided here as well to cancel the request to the cache, - /// // if a new image is requested. That token can be canceled in the OnNewSourceRequested method. - /// var img = await ImageCache.Instance.GetFromCacheAsync(imageUri, true, initializerKeyValues: propValues); - /// - /// lock (LockObj) - /// { - /// // If you have many imageEx in a virtualized ListView for instance - /// // controls will be recycled and the uri will change while waiting for the previous one to load - /// if (CurrentSourceUri == imageUri) - /// { - /// AttachSource(img); - /// ImageExOpened?.Invoke(this, new ImageExOpenedEventArgs()); - /// VisualStateManager.GoToState(this, LoadedState, true); - /// } - /// } - /// } - /// catch (OperationCanceledException) - /// { - /// // nothing to do as cancellation has been requested. - /// } - /// catch (Exception e) - /// { - /// lock (LockObj) - /// { - /// if (CurrentSourceUri == imageUri) - /// { - /// ImageExFailed?.Invoke(this, new ImageExFailedEventArgs(e)); - /// VisualStateManager.GoToState(this, FailedState, true); - /// } - /// } - /// } + /// // A token is provided here as well to cancel the request to the cache, + /// // if a new image is requested. + /// return await ImageCache.Instance.GetFromCacheAsync(imageUri, true, token, propValues); /// /// /// of the image to load from the cache. + /// A which is used to signal when the current request is outdated. /// - protected virtual Task AttachCachedResourceAsync(Uri imageUri) + protected virtual Task ProvideCachedResourceAsync(Uri imageUri, CancellationToken token) { // By default we just use the built-in UWP image cache provided within the Image control. - AttachSource(new BitmapImage(imageUri)); - - return Task.CompletedTask; - } - - /// - /// This method is called when a new source is requested by the control. This can be useful when - /// implementing a custom caching strategy to cancel any open request on the cache if a new - /// request comes in due to container recycling before the previous one has completed. - /// Be default, this method does nothing. - /// - /// Incoming requested source. - protected virtual void OnNewSourceRequested(object source) - { + return Task.FromResult((ImageSource)new BitmapImage(imageUri)); } } } \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.cs index 91b03d4950d..e4f62537a46 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.cs @@ -73,14 +73,11 @@ public ImageExBase() /// Routed Event Handler protected void AttachImageOpened(RoutedEventHandler handler) { - var image = Image as Image; - var brush = Image as ImageBrush; - - if (image != null) + if (Image is Image image) { image.ImageOpened += handler; } - else if (brush != null) + else if (Image is ImageBrush brush) { brush.ImageOpened += handler; } @@ -92,14 +89,11 @@ protected void AttachImageOpened(RoutedEventHandler handler) /// RoutedEventHandler protected void RemoveImageOpened(RoutedEventHandler handler) { - var image = Image as Image; - var brush = Image as ImageBrush; - - if (image != null) + if (Image is Image image) { image.ImageOpened -= handler; } - else if (brush != null) + else if (Image is ImageBrush brush) { brush.ImageOpened -= handler; } @@ -111,14 +105,11 @@ protected void RemoveImageOpened(RoutedEventHandler handler) /// Exception Routed Event Handler protected void AttachImageFailed(ExceptionRoutedEventHandler handler) { - var image = Image as Image; - var brush = Image as ImageBrush; - - if (image != null) + if (Image is Image image) { image.ImageFailed += handler; } - else if (brush != null) + else if (Image is ImageBrush brush) { brush.ImageFailed += handler; } @@ -130,14 +121,11 @@ protected void AttachImageFailed(ExceptionRoutedEventHandler handler) /// Exception Routed Event Handler protected void RemoveImageFailed(ExceptionRoutedEventHandler handler) { - var image = Image as Image; - var brush = Image as ImageBrush; - - if (image != null) + if (Image is Image image) { image.ImageFailed -= handler; } - else if (brush != null) + else if (Image is ImageBrush brush) { brush.ImageFailed -= handler; }