@@ -24,9 +24,9 @@ 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- private Uri _uri ;
28- private bool _isHttpSource ;
29- private CancellationTokenSource _tokenSource = null ;
27+ //// Used to track if we get a new request, so we can cancel any potential custom cache loading.
28+ private CancellationTokenSource _tokenSource ;
29+
3030 private object _lazyLoadingSource ;
3131
3232 /// <summary>
@@ -66,19 +66,28 @@ private static bool IsHttpUri(Uri uri)
6666 return uri . IsAbsoluteUri && ( uri . Scheme == "http" || uri . Scheme == "https" ) ;
6767 }
6868
69+ /// <summary>
70+ /// Method to call to assign an <see cref="ImageSource"/> value to the underlying <see cref="Image"/> powering <see cref="ImageExBase"/>.
71+ /// </summary>
72+ /// <param name="source"><see cref="ImageSource"/> to assign to the image.</param>
6973 private void AttachSource ( ImageSource source )
7074 {
71- var image = Image as Image ;
72- var brush = Image as ImageBrush ;
73-
74- 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 )
7579 {
7680 image . Source = source ;
7781 }
78- else if ( brush != null )
82+ else if ( Image is ImageBrush brush )
7983 {
8084 brush . ImageSource = source ;
8185 }
86+
87+ if ( source == null )
88+ {
89+ VisualStateManager . GoToState ( this , UnloadedState , true ) ;
90+ }
8291 }
8392
8493 private async void SetSource ( object source )
@@ -88,15 +97,14 @@ private async void SetSource(object source)
8897 return ;
8998 }
9099
91- this . _tokenSource ? . Cancel ( ) ;
100+ _tokenSource ? . Cancel ( ) ;
92101
93- this . _tokenSource = new CancellationTokenSource ( ) ;
102+ _tokenSource = new CancellationTokenSource ( ) ;
94103
95104 AttachSource ( null ) ;
96105
97106 if ( source == null )
98107 {
99- VisualStateManager . GoToState ( this , UnloadedState , true ) ;
100108 return ;
101109 }
102110
@@ -107,122 +115,121 @@ private async void SetSource(object source)
107115 {
108116 AttachSource ( imageSource ) ;
109117
110- ImageExOpened ? . Invoke ( this , new ImageExOpenedEventArgs ( ) ) ;
111- VisualStateManager . GoToState ( this , LoadedState , true ) ;
112118 return ;
113119 }
114120
115- _uri = source as Uri ;
116- if ( _uri == null )
121+ var uri = source as Uri ;
122+ if ( uri == null )
117123 {
118124 var url = source as string ?? source . ToString ( ) ;
119- if ( ! Uri . TryCreate ( url , UriKind . RelativeOrAbsolute , out _uri ) )
125+ if ( ! Uri . TryCreate ( url , UriKind . RelativeOrAbsolute , out uri ) )
120126 {
127+ ImageExFailed ? . Invoke ( this , new ImageExFailedEventArgs ( new UriFormatException ( "Invalid uri specified." ) ) ) ;
121128 VisualStateManager . GoToState ( this , FailedState , true ) ;
122129 return ;
123130 }
124131 }
125132
126- _isHttpSource = IsHttpUri ( _uri ) ;
127- if ( ! _isHttpSource && ! _uri . IsAbsoluteUri )
133+ if ( ! IsHttpUri ( uri ) && ! uri . IsAbsoluteUri )
128134 {
129- _uri = new Uri ( "ms-appx:///" + _uri . OriginalString . TrimStart ( '/' ) ) ;
135+ uri = new Uri ( "ms-appx:///" + uri . OriginalString . TrimStart ( '/' ) ) ;
130136 }
131137
132- await LoadImageAsync ( _uri ) ;
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+ }
133151 }
134152
135- private async Task LoadImageAsync ( Uri imageUri )
153+ private async Task LoadImageAsync ( Uri imageUri , CancellationToken token )
136154 {
137- if ( _uri != null )
155+ if ( imageUri != null )
138156 {
139157 if ( IsCacheEnabled )
140158 {
141- switch ( CachingStrategy )
159+ var img = await ProvideCachedResourceAsync ( imageUri , token ) ;
160+
161+ if ( ! _tokenSource . IsCancellationRequested )
142162 {
143- case ImageExCachingStrategy . Custom when _isHttpSource :
144- await SetHttpSourceCustomCached ( imageUri ) ;
145- break ;
146- case ImageExCachingStrategy . Custom :
147- case ImageExCachingStrategy . Internal :
148- default :
149- AttachSource ( new BitmapImage ( imageUri ) ) ;
150- break ;
163+ // Only attach our image if we still have a valid request.
164+ AttachSource ( img ) ;
151165 }
152166 }
153- else if ( string . Equals ( _uri . Scheme , "data" , StringComparison . OrdinalIgnoreCase ) )
167+ else if ( string . Equals ( imageUri . Scheme , "data" , StringComparison . OrdinalIgnoreCase ) )
154168 {
155- var source = _uri . OriginalString ;
169+ var source = imageUri . OriginalString ;
156170 const string base64Head = "base64," ;
157171 var index = source . IndexOf ( base64Head ) ;
158172 if ( index >= 0 )
159173 {
160174 var bytes = Convert . FromBase64String ( source . Substring ( index + base64Head . Length ) ) ;
161175 var bitmap = new BitmapImage ( ) ;
162- AttachSource ( bitmap ) ;
163176 await bitmap . SetSourceAsync ( new MemoryStream ( bytes ) . AsRandomAccessStream ( ) ) ;
177+
178+ if ( ! _tokenSource . IsCancellationRequested )
179+ {
180+ AttachSource ( bitmap ) ;
181+ }
164182 }
165183 }
166184 else
167185 {
168- AttachSource ( new BitmapImage ( _uri )
186+ AttachSource ( new BitmapImage ( imageUri )
169187 {
170188 CreateOptions = BitmapCreateOptions . IgnoreImageCache
171189 } ) ;
172190 }
173191 }
174192 }
175193
176- private async Task SetHttpSourceCustomCached ( Uri imageUri )
194+ /// <summary>
195+ /// This method is provided in case a developer would like their own custom caching strategy for <see cref="ImageExBase"/>.
196+ /// By default it uses the built-in UWP cache provided by <see cref="BitmapImage"/> and
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"/>.
203+ /// </summary>
204+ /// <example>
205+ /// <code>
206+ /// var propValues = new List<KeyValuePair<string, object>>();
207+ ///
208+ /// if (DecodePixelHeight > 0)
209+ /// {
210+ /// propValues.Add(new KeyValuePair<string, object>(nameof(DecodePixelHeight), DecodePixelHeight));
211+ /// }
212+ /// if (DecodePixelWidth > 0)
213+ /// {
214+ /// propValues.Add(new KeyValuePair<string, object>(nameof(DecodePixelWidth), DecodePixelWidth));
215+ /// }
216+ /// if (propValues.Count > 0)
217+ /// {
218+ /// propValues.Add(new KeyValuePair<string, object>(nameof(DecodePixelType), DecodePixelType));
219+ /// }
220+ ///
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);
224+ /// </code>
225+ /// </example>
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>
228+ /// <returns><see cref="Task"/></returns>
229+ protected virtual Task < ImageSource > ProvideCachedResourceAsync ( Uri imageUri , CancellationToken token )
177230 {
178- try
179- {
180- var propValues = new List < KeyValuePair < string , object > > ( ) ;
181-
182- if ( DecodePixelHeight > 0 )
183- {
184- propValues . Add ( new KeyValuePair < string , object > ( nameof ( DecodePixelHeight ) , DecodePixelHeight ) ) ;
185- }
186-
187- if ( DecodePixelWidth > 0 )
188- {
189- propValues . Add ( new KeyValuePair < string , object > ( nameof ( DecodePixelWidth ) , DecodePixelWidth ) ) ;
190- }
191-
192- if ( propValues . Count > 0 )
193- {
194- propValues . Add ( new KeyValuePair < string , object > ( nameof ( DecodePixelType ) , DecodePixelType ) ) ;
195- }
196-
197- var img = await ImageCache . Instance . GetFromCacheAsync ( imageUri , true , _tokenSource . Token , propValues ) ;
198-
199- lock ( LockObj )
200- {
201- // If you have many imageEx in a virtualized ListView for instance
202- // controls will be recycled and the uri will change while waiting for the previous one to load
203- if ( _uri == imageUri )
204- {
205- AttachSource ( img ) ;
206- ImageExOpened ? . Invoke ( this , new ImageExOpenedEventArgs ( ) ) ;
207- VisualStateManager . GoToState ( this , LoadedState , true ) ;
208- }
209- }
210- }
211- catch ( OperationCanceledException )
212- {
213- // nothing to do as cancellation has been requested.
214- }
215- catch ( Exception e )
216- {
217- lock ( LockObj )
218- {
219- if ( _uri == imageUri )
220- {
221- ImageExFailed ? . Invoke ( this , new ImageExFailedEventArgs ( e ) ) ;
222- VisualStateManager . GoToState ( this , FailedState , true ) ;
223- }
224- }
225- }
231+ // By default we just use the built-in UWP image cache provided within the Image control.
232+ return Task . FromResult ( ( ImageSource ) new BitmapImage ( imageUri ) ) ;
226233 }
227234 }
228235}
0 commit comments