Skip to content

Commit 72bcb6b

Browse files
Merge branch 'master' into deprecate-radial
2 parents c80a78b + 189e91a commit 72bcb6b

File tree

8 files changed

+138
-186
lines changed

8 files changed

+138
-186
lines changed

Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ImageEx/ImageExLazyLoadingControl.xaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<Grid Margin="40"
1212
Background="White">
1313
<ScrollViewer VerticalScrollBarVisibility="Auto">
14-
<Grid Height="3000">
14+
<Grid Height="6000">
1515
<Border Width="200"
1616
Height="200"
1717
HorizontalAlignment="Center"
@@ -35,7 +35,8 @@
3535
VerticalAlignment="Top"
3636
Background="Transparent"
3737
BorderThickness="0"
38-
Click="CloseButton_Click">
38+
Click="CloseButton_Click"
39+
Foreground="Black">
3940
<SymbolIcon Symbol="Cancel" />
4041
</Button>
4142
</Grid>

Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ImageEx/ImageExPage.xaml.cs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -82,16 +82,19 @@ private async void Load()
8282

8383
if (lazyLoadingControlHost != null)
8484
{
85-
lazyLoadingControlHost.Child = imageExLazyLoadingControl;
85+
// Allow this to act as a toggle.
86+
if (lazyLoadingControlHost.Child == null)
87+
{
88+
lazyLoadingControlHost.Child = imageExLazyLoadingControl;
89+
}
90+
else
91+
{
92+
lazyLoadingControlHost.Child = null;
93+
}
8694
}
8795
});
8896

89-
SampleController.Current.RegisterNewCommand("Clear image cache", async (sender, args) =>
90-
{
91-
container?.Children?.Clear();
92-
GC.Collect(); // Force GC to free file locks
93-
await ImageCache.Instance.ClearAsync();
94-
});
97+
SampleController.Current.RegisterNewCommand("Remove images", (sender, args) => container?.Children?.Clear());
9598

9699
await LoadDataAsync();
97100
}

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

Lines changed: 0 additions & 23 deletions
This file was deleted.

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Windows.UI.Composition;
77
using Windows.UI.Xaml;
88
using Windows.UI.Xaml.Controls;
9+
using Windows.UI.Xaml.Media;
910

1011
namespace Microsoft.Toolkit.Uwp.UI.Controls
1112
{
@@ -33,7 +34,12 @@ public Thickness NineGrid
3334
/// <inheritdoc/>
3435
public override CompositionBrush GetAlphaMask()
3536
{
36-
return IsInitialized ? (Image as Image).GetAlphaMask() : null;
37+
if (IsInitialized && Image is Image image)
38+
{
39+
return image.GetAlphaMask();
40+
}
41+
42+
return null;
3743
}
3844

3945
/// <summary>
@@ -42,7 +48,12 @@ public override CompositionBrush GetAlphaMask()
4248
/// <returns>The image as a <see cref="CastingSource"/>.</returns>
4349
public CastingSource GetAsCastingSource()
4450
{
45-
return IsInitialized ? (Image as Image).GetAsCastingSource() : null;
51+
if (IsInitialized && Image is Image image)
52+
{
53+
return image.GetAsCastingSource();
54+
}
55+
56+
return null;
4657
}
4758
}
4859
}

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

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,6 @@ public partial class ImageExBase
4040
/// </summary>
4141
public static readonly DependencyProperty IsCacheEnabledProperty = DependencyProperty.Register(nameof(IsCacheEnabled), typeof(bool), typeof(ImageExBase), new PropertyMetadata(false));
4242

43-
/// <summary>
44-
/// Identifies the <see cref="CachingStrategy"/> dependency property.
45-
/// </summary>
46-
public static readonly DependencyProperty CachingStrategyProperty = DependencyProperty.Register(nameof(CachingStrategy), typeof(ImageExCachingStrategy), typeof(ImageExBase), new PropertyMetadata(ImageExCachingStrategy.Custom));
47-
4843
/// <summary>
4944
/// Identifies the <see cref="EnableLazyLoading"/> dependency property.
5045
/// </summary>
@@ -126,15 +121,6 @@ public bool IsCacheEnabled
126121
set { SetValue(IsCacheEnabledProperty, value); }
127122
}
128123

129-
/// <summary>
130-
/// Gets or sets a value indicating how the <see cref="ImageEx"/> will be cached.
131-
/// </summary>
132-
public ImageExCachingStrategy CachingStrategy
133-
{
134-
get { return (ImageExCachingStrategy)GetValue(CachingStrategyProperty); }
135-
set { SetValue(CachingStrategyProperty, value); }
136-
}
137-
138124
/// <summary>
139125
/// Gets or sets a value indicating whether gets or sets is lazy loading enable. (17763 or higher supported)
140126
/// </summary>

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

Lines changed: 91 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -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&lt;KeyValuePair&lt;string, object>>();
207+
///
208+
/// if (DecodePixelHeight > 0)
209+
/// {
210+
/// propValues.Add(new KeyValuePair&lt;string, object>(nameof(DecodePixelHeight), DecodePixelHeight));
211+
/// }
212+
/// if (DecodePixelWidth > 0)
213+
/// {
214+
/// propValues.Add(new KeyValuePair&lt;string, object>(nameof(DecodePixelWidth), DecodePixelWidth));
215+
/// }
216+
/// if (propValues.Count > 0)
217+
/// {
218+
/// propValues.Add(new KeyValuePair&lt;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

Comments
 (0)