@@ -24,9 +24,9 @@ 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
- 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
+
30
30
private object _lazyLoadingSource ;
31
31
32
32
/// <summary>
@@ -66,19 +66,28 @@ private static bool IsHttpUri(Uri uri)
66
66
return uri . IsAbsoluteUri && ( uri . Scheme == "http" || uri . Scheme == "https" ) ;
67
67
}
68
68
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>
69
73
private void AttachSource ( ImageSource source )
70
74
{
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 )
75
79
{
76
80
image . Source = source ;
77
81
}
78
- else if ( brush != null )
82
+ else if ( Image is ImageBrush brush )
79
83
{
80
84
brush . ImageSource = source ;
81
85
}
86
+
87
+ if ( source == null )
88
+ {
89
+ VisualStateManager . GoToState ( this , UnloadedState , true ) ;
90
+ }
82
91
}
83
92
84
93
private async void SetSource ( object source )
@@ -88,15 +97,14 @@ private async void SetSource(object source)
88
97
return ;
89
98
}
90
99
91
- this . _tokenSource ? . Cancel ( ) ;
100
+ _tokenSource ? . Cancel ( ) ;
92
101
93
- this . _tokenSource = new CancellationTokenSource ( ) ;
102
+ _tokenSource = new CancellationTokenSource ( ) ;
94
103
95
104
AttachSource ( null ) ;
96
105
97
106
if ( source == null )
98
107
{
99
- VisualStateManager . GoToState ( this , UnloadedState , true ) ;
100
108
return ;
101
109
}
102
110
@@ -107,122 +115,121 @@ private async void SetSource(object source)
107
115
{
108
116
AttachSource ( imageSource ) ;
109
117
110
- ImageExOpened ? . Invoke ( this , new ImageExOpenedEventArgs ( ) ) ;
111
- VisualStateManager . GoToState ( this , LoadedState , true ) ;
112
118
return ;
113
119
}
114
120
115
- _uri = source as Uri ;
116
- if ( _uri == null )
121
+ var uri = source as Uri ;
122
+ if ( uri == null )
117
123
{
118
124
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 ) )
120
126
{
127
+ ImageExFailed ? . Invoke ( this , new ImageExFailedEventArgs ( new UriFormatException ( "Invalid uri specified." ) ) ) ;
121
128
VisualStateManager . GoToState ( this , FailedState , true ) ;
122
129
return ;
123
130
}
124
131
}
125
132
126
- _isHttpSource = IsHttpUri ( _uri ) ;
127
- if ( ! _isHttpSource && ! _uri . IsAbsoluteUri )
133
+ if ( ! IsHttpUri ( uri ) && ! uri . IsAbsoluteUri )
128
134
{
129
- _uri = new Uri ( "ms-appx:///" + _uri . OriginalString . TrimStart ( '/' ) ) ;
135
+ uri = new Uri ( "ms-appx:///" + uri . OriginalString . TrimStart ( '/' ) ) ;
130
136
}
131
137
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
+ }
133
151
}
134
152
135
- private async Task LoadImageAsync ( Uri imageUri )
153
+ private async Task LoadImageAsync ( Uri imageUri , CancellationToken token )
136
154
{
137
- if ( _uri != null )
155
+ if ( imageUri != null )
138
156
{
139
157
if ( IsCacheEnabled )
140
158
{
141
- switch ( CachingStrategy )
159
+ var img = await ProvideCachedResourceAsync ( imageUri , token ) ;
160
+
161
+ if ( ! _tokenSource . IsCancellationRequested )
142
162
{
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 ) ;
151
165
}
152
166
}
153
- else if ( string . Equals ( _uri . Scheme , "data" , StringComparison . OrdinalIgnoreCase ) )
167
+ else if ( string . Equals ( imageUri . Scheme , "data" , StringComparison . OrdinalIgnoreCase ) )
154
168
{
155
- var source = _uri . OriginalString ;
169
+ var source = imageUri . OriginalString ;
156
170
const string base64Head = "base64," ;
157
171
var index = source . IndexOf ( base64Head ) ;
158
172
if ( index >= 0 )
159
173
{
160
174
var bytes = Convert . FromBase64String ( source . Substring ( index + base64Head . Length ) ) ;
161
175
var bitmap = new BitmapImage ( ) ;
162
- AttachSource ( bitmap ) ;
163
176
await bitmap . SetSourceAsync ( new MemoryStream ( bytes ) . AsRandomAccessStream ( ) ) ;
177
+
178
+ if ( ! _tokenSource . IsCancellationRequested )
179
+ {
180
+ AttachSource ( bitmap ) ;
181
+ }
164
182
}
165
183
}
166
184
else
167
185
{
168
- AttachSource ( new BitmapImage ( _uri )
186
+ AttachSource ( new BitmapImage ( imageUri )
169
187
{
170
188
CreateOptions = BitmapCreateOptions . IgnoreImageCache
171
189
} ) ;
172
190
}
173
191
}
174
192
}
175
193
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 )
177
230
{
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 ) ) ;
226
233
}
227
234
}
228
235
}
0 commit comments