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;
}