Skip to content

Commit b386021

Browse files
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
1 parent c2fc4ad commit b386021

File tree

4 files changed

+84
-110
lines changed

4 files changed

+84
-110
lines changed

Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ImageEx/ImageExPage.xaml.cs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,7 @@ private async void Load()
9494
}
9595
});
9696

97-
SampleController.Current.RegisterNewCommand("Clear image cache", async (sender, args) =>
98-
{
99-
container?.Children?.Clear();
100-
GC.Collect(); // Force GC to free file locks
101-
await ImageCache.Instance.ClearAsync();
102-
});
97+
SampleController.Current.RegisterNewCommand("Remove images", (sender, args) => container?.Children?.Clear());
10398

10499
await LoadDataAsync();
105100
}

Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageEx.Members.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Windows.UI.Composition;
77
using Windows.UI.Xaml;
88
using Windows.UI.Xaml.Controls;
9+
using Windows.UI.Xaml.Media;
910

1011
namespace Microsoft.Toolkit.Uwp.UI.Controls
1112
{
@@ -33,7 +34,12 @@ public Thickness NineGrid
3334
/// <inheritdoc/>
3435
public override CompositionBrush GetAlphaMask()
3536
{
36-
return IsInitialized ? (Image as Image).GetAlphaMask() : null;
37+
if (IsInitialized && Image is Image image)
38+
{
39+
return image.GetAlphaMask();
40+
}
41+
42+
return null;
3743
}
3844

3945
/// <summary>
@@ -42,7 +48,12 @@ public override CompositionBrush GetAlphaMask()
4248
/// <returns>The image as a <see cref="CastingSource"/>.</returns>
4349
public CastingSource GetAsCastingSource()
4450
{
45-
return IsInitialized ? (Image as Image).GetAsCastingSource() : null;
51+
if (IsInitialized && Image is Image image)
52+
{
53+
return image.GetAsCastingSource();
54+
}
55+
56+
return null;
4657
}
4758
}
4859
}

Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.Source.cs

Lines changed: 62 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,8 @@ public partial class ImageExBase
2424
/// </summary>
2525
public static readonly DependencyProperty SourceProperty = DependencyProperty.Register(nameof(Source), typeof(object), typeof(ImageExBase), new PropertyMetadata(null, SourceChanged));
2626

27-
/// <summary>
28-
/// Gets value tracking the currently requested source Uri. This can be helpful to use when implementing <see cref="AttachCachedResourceAsync(Uri)"/> 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.
29-
/// </summary>
30-
protected Uri CurrentSourceUri { get; private set; }
27+
//// Used to track if we get a new request, so we can cancel any potential custom cache loading.
28+
private CancellationTokenSource _tokenSource;
3129

3230
private object _lazyLoadingSource;
3331

@@ -72,19 +70,24 @@ private static bool IsHttpUri(Uri uri)
7270
/// Method to call to assign an <see cref="ImageSource"/> value to the underlying <see cref="Image"/> powering <see cref="ImageExBase"/>.
7371
/// </summary>
7472
/// <param name="source"><see cref="ImageSource"/> to assign to the image.</param>
75-
protected void AttachSource(ImageSource source)
73+
private void AttachSource(ImageSource source)
7674
{
77-
var image = Image as Image;
78-
var brush = Image as ImageBrush;
79-
80-
if (image != null)
75+
// Setting the source at this point should call ImageExOpened/VisualStateManager.GoToState
76+
// as we register to both the ImageOpened/ImageFailed events of the underlying control.
77+
// We only need to call those methods if we fail in other cases before we get here.
78+
if (Image is Image image)
8179
{
8280
image.Source = source;
8381
}
84-
else if (brush != null)
82+
else if (Image is ImageBrush brush)
8583
{
8684
brush.ImageSource = source;
8785
}
86+
87+
if (source == null)
88+
{
89+
VisualStateManager.GoToState(this, UnloadedState, true);
90+
}
8891
}
8992

9093
private async void SetSource(object source)
@@ -94,13 +97,14 @@ private async void SetSource(object source)
9497
return;
9598
}
9699

97-
OnNewSourceRequested(source);
100+
_tokenSource?.Cancel();
101+
102+
_tokenSource = new CancellationTokenSource();
98103

99104
AttachSource(null);
100105

101106
if (source == null)
102107
{
103-
VisualStateManager.GoToState(this, UnloadedState, true);
104108
return;
105109
}
106110

@@ -111,39 +115,54 @@ private async void SetSource(object source)
111115
{
112116
AttachSource(imageSource);
113117

114-
ImageExOpened?.Invoke(this, new ImageExOpenedEventArgs());
115-
VisualStateManager.GoToState(this, LoadedState, true);
116118
return;
117119
}
118120

119-
CurrentSourceUri = source as Uri;
120-
if (CurrentSourceUri == null)
121+
var uri = source as Uri;
122+
if (uri == null)
121123
{
122124
var url = source as string ?? source.ToString();
123-
if (!Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out Uri uri))
125+
if (!Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out uri))
124126
{
127+
ImageExFailed?.Invoke(this, new ImageExFailedEventArgs(new UriFormatException("Invalid uri specified.")));
125128
VisualStateManager.GoToState(this, FailedState, true);
126129
return;
127130
}
128-
129-
CurrentSourceUri = uri;
130131
}
131132

132-
if (!IsHttpUri(CurrentSourceUri) && !CurrentSourceUri.IsAbsoluteUri)
133+
if (!IsHttpUri(uri) && !uri.IsAbsoluteUri)
133134
{
134-
CurrentSourceUri = new Uri("ms-appx:///" + CurrentSourceUri.OriginalString.TrimStart('/'));
135+
uri = new Uri("ms-appx:///" + uri.OriginalString.TrimStart('/'));
135136
}
136137

137-
await LoadImageAsync(CurrentSourceUri);
138+
try
139+
{
140+
await LoadImageAsync(uri, _tokenSource.Token);
141+
}
142+
catch (OperationCanceledException)
143+
{
144+
// nothing to do as cancellation has been requested.
145+
}
146+
catch (Exception e)
147+
{
148+
ImageExFailed?.Invoke(this, new ImageExFailedEventArgs(e));
149+
VisualStateManager.GoToState(this, FailedState, true);
150+
}
138151
}
139152

140-
private async Task LoadImageAsync(Uri imageUri)
153+
private async Task LoadImageAsync(Uri imageUri, CancellationToken token)
141154
{
142155
if (imageUri != null)
143156
{
144157
if (IsCacheEnabled)
145158
{
146-
await AttachCachedResourceAsync(imageUri);
159+
var img = await ProvideCachedResourceAsync(imageUri, token);
160+
161+
if (!_tokenSource.IsCancellationRequested)
162+
{
163+
// Only attach our image if we still have a valid request.
164+
AttachSource(img);
165+
}
147166
}
148167
else if (string.Equals(imageUri.Scheme, "data", StringComparison.OrdinalIgnoreCase))
149168
{
@@ -154,8 +173,12 @@ private async Task LoadImageAsync(Uri imageUri)
154173
{
155174
var bytes = Convert.FromBase64String(source.Substring(index + base64Head.Length));
156175
var bitmap = new BitmapImage();
157-
AttachSource(bitmap);
158176
await bitmap.SetSourceAsync(new MemoryStream(bytes).AsRandomAccessStream());
177+
178+
if (!_tokenSource.IsCancellationRequested)
179+
{
180+
AttachSource(bitmap);
181+
}
159182
}
160183
}
161184
else
@@ -171,85 +194,42 @@ private async Task LoadImageAsync(Uri imageUri)
171194
/// <summary>
172195
/// This method is provided in case a developer would like their own custom caching strategy for <see cref="ImageExBase"/>.
173196
/// By default it uses the built-in UWP cache provided by <see cref="BitmapImage"/> and
174-
/// the <see cref="Image"/> control itself. This method should call <see cref="AttachSource(ImageSource)"/>
175-
/// to set the retrieved cache value to the image. <see cref="CurrentSourceUri"/> may be checked
176-
/// after retrieving a cached image to ensure that the current resource requested matches the one
177-
/// requested by the <see cref="AttachCachedResourceAsync(Uri)"/> parameter.
178-
/// <see cref="OnNewSourceRequested(object)"/> may be used in order to signal any cancellation events
179-
/// using a <see cref="CancellationToken"/> to the call to the cache, for instance like the Toolkit's
180-
/// own <see cref="CacheBase{T}.GetFromCacheAsync(Uri, bool, CancellationToken, List{KeyValuePair{string, object}})"/> in <see cref="ImageCache"/>.
197+
/// the <see cref="Image"/> control itself. This method should return an <see cref="ImageSource"/>
198+
/// value of the image specified by the provided uri parameter.
199+
/// A <see cref="CancellationToken"/> is provided in case the current request is invalidated
200+
/// (e.g. the container is recycled before the original image is loaded).
201+
/// The Toolkit also has an image cache helper which can be used as well:
202+
/// <see cref="CacheBase{T}.GetFromCacheAsync(Uri, bool, CancellationToken, List{KeyValuePair{string, object}})"/> in <see cref="ImageCache"/>.
181203
/// </summary>
182204
/// <example>
183205
/// <code>
184-
/// try
185-
/// {
186206
/// var propValues = new List&lt;KeyValuePair&lt;string, object>>();
187207
///
188208
/// if (DecodePixelHeight > 0)
189209
/// {
190-
/// propValues.Add(new KeyValuePair&lt;string, object>(nameof(DecodePixelHeight), D ecodePixelHeight));
210+
/// propValues.Add(new KeyValuePair&lt;string, object>(nameof(DecodePixelHeight), DecodePixelHeight));
191211
/// }
192212
/// if (DecodePixelWidth > 0)
193213
/// {
194-
/// propValues.Add(new KeyValuePair&lt;string, object>(nameof(DecodePixelWidth), D ecodePixelWidth));
214+
/// propValues.Add(new KeyValuePair&lt;string, object>(nameof(DecodePixelWidth), DecodePixelWidth));
195215
/// }
196216
/// if (propValues.Count > 0)
197217
/// {
198218
/// propValues.Add(new KeyValuePair&lt;string, object>(nameof(DecodePixelType), DecodePixelType));
199219
/// }
200220
///
201-
/// // A token could be provided here as well to cancel the request to the cache,
202-
/// // if a new image is requested. That token can be canceled in the OnNewSourceRequested method.
203-
/// var img = await ImageCache.Instance.GetFromCacheAsync(imageUri, true, initializerKeyValues: propValues);
204-
///
205-
/// lock (LockObj)
206-
/// {
207-
/// // If you have many imageEx in a virtualized ListView for instance
208-
/// // controls will be recycled and the uri will change while waiting for the previous one to load
209-
/// if (CurrentSourceUri == imageUri)
210-
/// {
211-
/// AttachSource(img);
212-
/// ImageExOpened?.Invoke(this, new ImageExOpenedEventArgs());
213-
/// VisualStateManager.GoToState(this, LoadedState, true);
214-
/// }
215-
/// }
216-
/// }
217-
/// catch (OperationCanceledException)
218-
/// {
219-
/// // nothing to do as cancellation has been requested.
220-
/// }
221-
/// catch (Exception e)
222-
/// {
223-
/// lock (LockObj)
224-
/// {
225-
/// if (CurrentSourceUri == imageUri)
226-
/// {
227-
/// ImageExFailed?.Invoke(this, new ImageExFailedEventArgs(e));
228-
/// VisualStateManager.GoToState(this, FailedState, true);
229-
/// }
230-
/// }
231-
/// }
221+
/// // A token is provided here as well to cancel the request to the cache,
222+
/// // if a new image is requested.
223+
/// return await ImageCache.Instance.GetFromCacheAsync(imageUri, true, token, propValues);
232224
/// </code>
233225
/// </example>
234226
/// <param name="imageUri"><see cref="Uri"/> of the image to load from the cache.</param>
227+
/// <param name="token">A <see cref="CancellationToken"/> which is used to signal when the current request is outdated.</param>
235228
/// <returns><see cref="Task"/></returns>
236-
protected virtual Task AttachCachedResourceAsync(Uri imageUri)
229+
protected virtual Task<ImageSource> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
237230
{
238231
// By default we just use the built-in UWP image cache provided within the Image control.
239-
AttachSource(new BitmapImage(imageUri));
240-
241-
return Task.CompletedTask;
242-
}
243-
244-
/// <summary>
245-
/// This method is called when a new source is requested by the control. This can be useful when
246-
/// implementing a custom caching strategy to cancel any open request on the cache if a new
247-
/// request comes in due to container recycling before the previous one has completed.
248-
/// Be default, this method does nothing.
249-
/// </summary>
250-
/// <param name="source">Incoming requested source.</param>
251-
protected virtual void OnNewSourceRequested(object source)
252-
{
232+
return Task.FromResult((ImageSource)new BitmapImage(imageUri));
253233
}
254234
}
255235
}

Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.cs

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,11 @@ public ImageExBase()
7373
/// <param name="handler">Routed Event Handler</param>
7474
protected void AttachImageOpened(RoutedEventHandler handler)
7575
{
76-
var image = Image as Image;
77-
var brush = Image as ImageBrush;
78-
79-
if (image != null)
76+
if (Image is Image image)
8077
{
8178
image.ImageOpened += handler;
8279
}
83-
else if (brush != null)
80+
else if (Image is ImageBrush brush)
8481
{
8582
brush.ImageOpened += handler;
8683
}
@@ -92,14 +89,11 @@ protected void AttachImageOpened(RoutedEventHandler handler)
9289
/// <param name="handler">RoutedEventHandler</param>
9390
protected void RemoveImageOpened(RoutedEventHandler handler)
9491
{
95-
var image = Image as Image;
96-
var brush = Image as ImageBrush;
97-
98-
if (image != null)
92+
if (Image is Image image)
9993
{
10094
image.ImageOpened -= handler;
10195
}
102-
else if (brush != null)
96+
else if (Image is ImageBrush brush)
10397
{
10498
brush.ImageOpened -= handler;
10599
}
@@ -111,14 +105,11 @@ protected void RemoveImageOpened(RoutedEventHandler handler)
111105
/// <param name="handler">Exception Routed Event Handler</param>
112106
protected void AttachImageFailed(ExceptionRoutedEventHandler handler)
113107
{
114-
var image = Image as Image;
115-
var brush = Image as ImageBrush;
116-
117-
if (image != null)
108+
if (Image is Image image)
118109
{
119110
image.ImageFailed += handler;
120111
}
121-
else if (brush != null)
112+
else if (Image is ImageBrush brush)
122113
{
123114
brush.ImageFailed += handler;
124115
}
@@ -130,14 +121,11 @@ protected void AttachImageFailed(ExceptionRoutedEventHandler handler)
130121
/// <param name="handler">Exception Routed Event Handler</param>
131122
protected void RemoveImageFailed(ExceptionRoutedEventHandler handler)
132123
{
133-
var image = Image as Image;
134-
var brush = Image as ImageBrush;
135-
136-
if (image != null)
124+
if (Image is Image image)
137125
{
138126
image.ImageFailed -= handler;
139127
}
140-
else if (brush != null)
128+
else if (Image is ImageBrush brush)
141129
{
142130
brush.ImageFailed -= handler;
143131
}

0 commit comments

Comments
 (0)