@@ -24,10 +24,8 @@ public partial class ImageExBase
24
24
/// </summary>
25
25
public static readonly DependencyProperty SourceProperty = DependencyProperty . Register ( nameof ( Source ) , typeof ( object ) , typeof ( ImageExBase ) , new PropertyMetadata ( null , SourceChanged ) ) ;
26
26
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 ;
31
29
32
30
private object _lazyLoadingSource ;
33
31
@@ -72,19 +70,24 @@ private static bool IsHttpUri(Uri uri)
72
70
/// Method to call to assign an <see cref="ImageSource"/> value to the underlying <see cref="Image"/> powering <see cref="ImageExBase"/>.
73
71
/// </summary>
74
72
/// <param name="source"><see cref="ImageSource"/> to assign to the image.</param>
75
- protected void AttachSource ( ImageSource source )
73
+ private void AttachSource ( ImageSource source )
76
74
{
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 )
81
79
{
82
80
image . Source = source ;
83
81
}
84
- else if ( brush != null )
82
+ else if ( Image is ImageBrush brush )
85
83
{
86
84
brush . ImageSource = source ;
87
85
}
86
+
87
+ if ( source == null )
88
+ {
89
+ VisualStateManager . GoToState ( this , UnloadedState , true ) ;
90
+ }
88
91
}
89
92
90
93
private async void SetSource ( object source )
@@ -94,13 +97,14 @@ private async void SetSource(object source)
94
97
return ;
95
98
}
96
99
97
- OnNewSourceRequested ( source ) ;
100
+ _tokenSource ? . Cancel ( ) ;
101
+
102
+ _tokenSource = new CancellationTokenSource ( ) ;
98
103
99
104
AttachSource ( null ) ;
100
105
101
106
if ( source == null )
102
107
{
103
- VisualStateManager . GoToState ( this , UnloadedState , true ) ;
104
108
return ;
105
109
}
106
110
@@ -111,39 +115,54 @@ private async void SetSource(object source)
111
115
{
112
116
AttachSource ( imageSource ) ;
113
117
114
- ImageExOpened ? . Invoke ( this , new ImageExOpenedEventArgs ( ) ) ;
115
- VisualStateManager . GoToState ( this , LoadedState , true ) ;
116
118
return ;
117
119
}
118
120
119
- CurrentSourceUri = source as Uri ;
120
- if ( CurrentSourceUri == null )
121
+ var uri = source as Uri ;
122
+ if ( uri == null )
121
123
{
122
124
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 ) )
124
126
{
127
+ ImageExFailed ? . Invoke ( this , new ImageExFailedEventArgs ( new UriFormatException ( "Invalid uri specified." ) ) ) ;
125
128
VisualStateManager . GoToState ( this , FailedState , true ) ;
126
129
return ;
127
130
}
128
-
129
- CurrentSourceUri = uri ;
130
131
}
131
132
132
- if ( ! IsHttpUri ( CurrentSourceUri ) && ! CurrentSourceUri . IsAbsoluteUri )
133
+ if ( ! IsHttpUri ( uri ) && ! uri . IsAbsoluteUri )
133
134
{
134
- CurrentSourceUri = new Uri ( "ms-appx:///" + CurrentSourceUri . OriginalString . TrimStart ( '/' ) ) ;
135
+ uri = new Uri ( "ms-appx:///" + uri . OriginalString . TrimStart ( '/' ) ) ;
135
136
}
136
137
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
+ }
138
151
}
139
152
140
- private async Task LoadImageAsync ( Uri imageUri )
153
+ private async Task LoadImageAsync ( Uri imageUri , CancellationToken token )
141
154
{
142
155
if ( imageUri != null )
143
156
{
144
157
if ( IsCacheEnabled )
145
158
{
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
+ }
147
166
}
148
167
else if ( string . Equals ( imageUri . Scheme , "data" , StringComparison . OrdinalIgnoreCase ) )
149
168
{
@@ -154,8 +173,12 @@ private async Task LoadImageAsync(Uri imageUri)
154
173
{
155
174
var bytes = Convert . FromBase64String ( source . Substring ( index + base64Head . Length ) ) ;
156
175
var bitmap = new BitmapImage ( ) ;
157
- AttachSource ( bitmap ) ;
158
176
await bitmap . SetSourceAsync ( new MemoryStream ( bytes ) . AsRandomAccessStream ( ) ) ;
177
+
178
+ if ( ! _tokenSource . IsCancellationRequested )
179
+ {
180
+ AttachSource ( bitmap ) ;
181
+ }
159
182
}
160
183
}
161
184
else
@@ -171,85 +194,42 @@ private async Task LoadImageAsync(Uri imageUri)
171
194
/// <summary>
172
195
/// This method is provided in case a developer would like their own custom caching strategy for <see cref="ImageExBase"/>.
173
196
/// 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"/>.
181
203
/// </summary>
182
204
/// <example>
183
205
/// <code>
184
- /// try
185
- /// {
186
206
/// var propValues = new List<KeyValuePair<string, object>>();
187
207
///
188
208
/// if (DecodePixelHeight > 0)
189
209
/// {
190
- /// propValues.Add(new KeyValuePair<string, object>(nameof(DecodePixelHeight), D ecodePixelHeight ));
210
+ /// propValues.Add(new KeyValuePair<string, object>(nameof(DecodePixelHeight), DecodePixelHeight ));
191
211
/// }
192
212
/// if (DecodePixelWidth > 0)
193
213
/// {
194
- /// propValues.Add(new KeyValuePair<string, object>(nameof(DecodePixelWidth), D ecodePixelWidth ));
214
+ /// propValues.Add(new KeyValuePair<string, object>(nameof(DecodePixelWidth), DecodePixelWidth ));
195
215
/// }
196
216
/// if (propValues.Count > 0)
197
217
/// {
198
218
/// propValues.Add(new KeyValuePair<string, object>(nameof(DecodePixelType), DecodePixelType));
199
219
/// }
200
220
///
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);
232
224
/// </code>
233
225
/// </example>
234
226
/// <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>
235
228
/// <returns><see cref="Task"/></returns>
236
- protected virtual Task AttachCachedResourceAsync ( Uri imageUri )
229
+ protected virtual Task < ImageSource > ProvideCachedResourceAsync ( Uri imageUri , CancellationToken token )
237
230
{
238
231
// 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 ) ) ;
253
233
}
254
234
}
255
235
}
0 commit comments