Skip to content

Commit 628d1d5

Browse files
Update ImageEx to support custom implementation of Caching by subclassing ImageExBase/ImageEx
Removed class level fields which were really local variables. Made new virtual methods. Still works fine in Sample App.
1 parent dbd379c commit 628d1d5

File tree

3 files changed

+115
-28
lines changed

3 files changed

+115
-28
lines changed

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

Lines changed: 102 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@ 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+
/// <summary>
28+
/// Gets value tracking the currently requested source Uri. This can be helpful to use when implementing <see cref="ProvideCachedResourceAsync(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; }
31+
3032
private object _lazyLoadingSource;
3133

3234
/// <summary>
@@ -66,7 +68,11 @@ private static bool IsHttpUri(Uri uri)
6668
return uri.IsAbsoluteUri && (uri.Scheme == "http" || uri.Scheme == "https");
6769
}
6870

69-
private void AttachSource(ImageSource source)
71+
/// <summary>
72+
/// Method to call to assign an <see cref="ImageSource"/> value to the underlying <see cref="Image"/> powering <see cref="ImageExBase"/>.
73+
/// </summary>
74+
/// <param name="source"><see cref="ImageSource"/> to assign to the image.</param>
75+
protected void AttachSource(ImageSource source)
7076
{
7177
var image = Image as Image;
7278
var brush = Image as ImageBrush;
@@ -88,9 +94,7 @@ private async void SetSource(object source)
8894
return;
8995
}
9096

91-
this._tokenSource?.Cancel();
92-
93-
this._tokenSource = new CancellationTokenSource();
97+
OnNewSourceRequested(source);
9498

9599
AttachSource(null);
96100

@@ -112,37 +116,38 @@ private async void SetSource(object source)
112116
return;
113117
}
114118

115-
_uri = source as Uri;
116-
if (_uri == null)
119+
CurrentSourceUri = source as Uri;
120+
if (CurrentSourceUri == null)
117121
{
118122
var url = source as string ?? source.ToString();
119-
if (!Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out _uri))
123+
if (!Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out Uri uri))
120124
{
121125
VisualStateManager.GoToState(this, FailedState, true);
122126
return;
123127
}
128+
129+
CurrentSourceUri = uri;
124130
}
125131

126-
_isHttpSource = IsHttpUri(_uri);
127-
if (!_isHttpSource && !_uri.IsAbsoluteUri)
132+
if (!IsHttpUri(CurrentSourceUri) && !CurrentSourceUri.IsAbsoluteUri)
128133
{
129-
_uri = new Uri("ms-appx:///" + _uri.OriginalString.TrimStart('/'));
134+
CurrentSourceUri = new Uri("ms-appx:///" + CurrentSourceUri.OriginalString.TrimStart('/'));
130135
}
131136

132-
await LoadImageAsync(_uri);
137+
await LoadImageAsync(CurrentSourceUri);
133138
}
134139

135140
private async Task LoadImageAsync(Uri imageUri)
136141
{
137-
if (_uri != null)
142+
if (imageUri != null)
138143
{
139144
if (IsCacheEnabled)
140145
{
141-
AttachSource(new BitmapImage(imageUri));
146+
await ProvideCachedResourceAsync(imageUri);
142147
}
143-
else if (string.Equals(_uri.Scheme, "data", StringComparison.OrdinalIgnoreCase))
148+
else if (string.Equals(imageUri.Scheme, "data", StringComparison.OrdinalIgnoreCase))
144149
{
145-
var source = _uri.OriginalString;
150+
var source = imageUri.OriginalString;
146151
const string base64Head = "base64,";
147152
var index = source.IndexOf(base64Head);
148153
if (index >= 0)
@@ -155,12 +160,90 @@ private async Task LoadImageAsync(Uri imageUri)
155160
}
156161
else
157162
{
158-
AttachSource(new BitmapImage(_uri)
163+
AttachSource(new BitmapImage(imageUri)
159164
{
160165
CreateOptions = BitmapCreateOptions.IgnoreImageCache
161166
});
162167
}
163168
}
164169
}
170+
171+
/// <summary>
172+
/// This method is provided in case a developer would like their own custom caching strategy for <see cref="ImageExBase"/>.
173+
/// By default it uses the built-in UWP cache provided by <see cref="BitmapImage"/> and
174+
/// the <see cref="Image"/> control itself. Call <see cref="AttachSource(ImageSource)"/> to set
175+
/// the retrieved cache value to the image.
176+
/// </summary>
177+
/// <example>
178+
/// <code>
179+
/// try
180+
/// {
181+
/// var propValues = new List&lt;KeyValuePair&lt;string, object>>();
182+
///
183+
/// if (DecodePixelHeight > 0)
184+
/// {
185+
/// propValues.Add(new KeyValuePair&lt;string, object>(nameof(DecodePixelHeight), D ecodePixelHeight));
186+
/// }
187+
/// if (DecodePixelWidth > 0)
188+
/// {
189+
/// propValues.Add(new KeyValuePair&lt;string, object>(nameof(DecodePixelWidth), D ecodePixelWidth));
190+
/// }
191+
/// if (propValues.Count > 0)
192+
/// {
193+
/// propValues.Add(new KeyValuePair&lt;string, object>(nameof(DecodePixelType), DecodePixelType));
194+
/// }
195+
///
196+
/// // A token could be provided here as well to cancel the request to the cache,
197+
/// // if a new image is requested. That token can be canceled in the OnNewSourceRequested method.
198+
/// var img = await ImageCache.Instance.GetFromCacheAsync(imageUri, true, initializerKeyValues: propValues);
199+
///
200+
/// lock (LockObj)
201+
/// {
202+
/// // If you have many imageEx in a virtualized ListView for instance
203+
/// // controls will be recycled and the uri will change while waiting for the previous one to load
204+
/// if (_currentSourceUri == imageUri)
205+
/// {
206+
/// AttachSource(img);
207+
/// ImageExOpened?.Invoke(this, new ImageExOpenedEventArgs());
208+
/// VisualStateManager.GoToState(this, LoadedState, true);
209+
/// }
210+
/// }
211+
/// }
212+
/// catch (OperationCanceledException)
213+
/// {
214+
/// // nothing to do as cancellation has been requested.
215+
/// }
216+
/// catch (Exception e)
217+
/// {
218+
/// lock (LockObj)
219+
/// {
220+
/// if (_currentSourceUri == imageUri)
221+
/// {
222+
/// ImageExFailed?.Invoke(this, new ImageExFailedEventArgs(e));
223+
/// VisualStateManager.GoToState(this, FailedState, true);
224+
/// }
225+
/// }
226+
/// }
227+
/// </code>
228+
/// </example>
229+
/// <param name="imageUri"><see cref="Uri"/> of the image to load from the cache.</param>
230+
/// <returns><see cref="Task"/></returns>
231+
protected virtual Task ProvideCachedResourceAsync(Uri imageUri)
232+
{
233+
AttachSource(new BitmapImage(imageUri));
234+
235+
return Task.CompletedTask;
236+
}
237+
238+
/// <summary>
239+
/// This method is called when a new source is requested by the control. This can be useful when
240+
/// implementing a custom caching strategy to cancel any open request on the cache if a new
241+
/// request comes in due to container recycling before the previous one has completed.
242+
/// Be default, this method does nothing.
243+
/// </summary>
244+
/// <param name="source">Incoming requested source.</param>
245+
protected virtual void OnNewSourceRequested(object source)
246+
{
247+
}
165248
}
166249
}

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

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,17 +60,11 @@ public abstract partial class ImageExBase : Control
6060
/// </summary>
6161
protected object Image { get; private set; }
6262

63-
/// <summary>
64-
/// Gets object used for lock
65-
/// </summary>
66-
protected object LockObj { get; private set; }
67-
6863
/// <summary>
6964
/// Initializes a new instance of the <see cref="ImageExBase"/> class.
7065
/// </summary>
7166
public ImageExBase()
7267
{
73-
LockObj = new object();
7468
}
7569

7670
/// <summary>
@@ -179,13 +173,23 @@ protected override void OnApplyTemplate()
179173
base.OnApplyTemplate();
180174
}
181175

182-
private void OnImageOpened(object sender, RoutedEventArgs e)
176+
/// <summary>
177+
/// Underlying <see cref="Image.ImageOpened"/> event handler.
178+
/// </summary>
179+
/// <param name="sender">Image</param>
180+
/// <param name="e">Event Arguments</param>
181+
protected virtual void OnImageOpened(object sender, RoutedEventArgs e)
183182
{
184183
ImageExOpened?.Invoke(this, new ImageExOpenedEventArgs());
185184
VisualStateManager.GoToState(this, LoadedState, true);
186185
}
187186

188-
private void OnImageFailed(object sender, ExceptionRoutedEventArgs e)
187+
/// <summary>
188+
/// Underlying <see cref="Image.ImageFailed"/> event handler.
189+
/// </summary>
190+
/// <param name="sender">Image</param>
191+
/// <param name="e">Event Arguments</param>
192+
protected virtual void OnImageFailed(object sender, ExceptionRoutedEventArgs e)
189193
{
190194
ImageExFailed?.Invoke(this, new ImageExFailedEventArgs(new Exception(e.ErrorMessage)));
191195
VisualStateManager.GoToState(this, FailedState, true);

Microsoft.Toolkit.Uwp.UI.Controls.Core/Microsoft.Toolkit.Uwp.UI.Controls.Core.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
<PackageTags>UWP Toolkit Windows Controls XAML Markdown CameraPreview Camera DropShadow ImageEx InAppNotification InfiniteCanvas Radial RadialProgressBar Scroll ScrollHeader Tile</PackageTags>
2121
<!-- ARM64 builds for managed apps use .NET Native. We can't use the Reflection Provider for that. -->
2222
<EnableTypeInfoReflection Condition="'$(Configuration)' == 'Debug'">false</EnableTypeInfoReflection>
23-
<LangVersion>8.0</LangVersion>
23+
<LangVersion>9.0</LangVersion>
2424
</PropertyGroup>
2525

2626
<ItemGroup>

0 commit comments

Comments
 (0)