@@ -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<KeyValuePair<string, object>>();
187207 ///
188208 /// if (DecodePixelHeight > 0)
189209 /// {
190- /// propValues.Add(new KeyValuePair<string, object>(nameof(DecodePixelHeight), D ecodePixelHeight ));
210+ /// propValues.Add(new KeyValuePair<string, object>(nameof(DecodePixelHeight), DecodePixelHeight ));
191211 /// }
192212 /// if (DecodePixelWidth > 0)
193213 /// {
194- /// propValues.Add(new KeyValuePair<string, object>(nameof(DecodePixelWidth), D ecodePixelWidth ));
214+ /// propValues.Add(new KeyValuePair<string, object>(nameof(DecodePixelWidth), DecodePixelWidth ));
195215 /// }
196216 /// if (propValues.Count > 0)
197217 /// {
198218 /// propValues.Add(new KeyValuePair<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}
0 commit comments