Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<Grid Margin="40"
Background="White">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<Grid Height="3000">
<Grid Height="6000">
<Border Width="200"
Height="200"
HorizontalAlignment="Center"
Expand All @@ -35,7 +35,8 @@
VerticalAlignment="Top"
Background="Transparent"
BorderThickness="0"
Click="CloseButton_Click">
Click="CloseButton_Click"
Foreground="Black">
<SymbolIcon Symbol="Cancel" />
</Button>
</Grid>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,16 +82,19 @@ private async void Load()

if (lazyLoadingControlHost != null)
{
lazyLoadingControlHost.Child = imageExLazyLoadingControl;
// Allow this to act as a toggle.
if (lazyLoadingControlHost.Child == null)
{
lazyLoadingControlHost.Child = imageExLazyLoadingControl;
}
else
{
lazyLoadingControlHost.Child = null;
}
}
});

SampleController.Current.RegisterNewCommand("Clear image cache", async (sender, args) =>
{
container?.Children?.Clear();
GC.Collect(); // Force GC to free file locks
await ImageCache.Instance.ClearAsync();
});
SampleController.Current.RegisterNewCommand("Remove images", (sender, args) => container?.Children?.Clear());

await LoadDataAsync();
}
Expand Down
23 changes: 0 additions & 23 deletions Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/CachingStrategy.cs

This file was deleted.

15 changes: 13 additions & 2 deletions Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageEx.Members.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Windows.UI.Composition;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;

namespace Microsoft.Toolkit.Uwp.UI.Controls
{
Expand Down Expand Up @@ -33,7 +34,12 @@ public Thickness NineGrid
/// <inheritdoc/>
public override CompositionBrush GetAlphaMask()
{
return IsInitialized ? (Image as Image).GetAlphaMask() : null;
if (IsInitialized && Image is Image image)
{
return image.GetAlphaMask();
}

return null;
}

/// <summary>
Expand All @@ -42,7 +48,12 @@ public override CompositionBrush GetAlphaMask()
/// <returns>The image as a <see cref="CastingSource"/>.</returns>
public CastingSource GetAsCastingSource()
{
return IsInitialized ? (Image as Image).GetAsCastingSource() : null;
if (IsInitialized && Image is Image image)
{
return image.GetAsCastingSource();
}

return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,6 @@ public partial class ImageExBase
/// </summary>
public static readonly DependencyProperty IsCacheEnabledProperty = DependencyProperty.Register(nameof(IsCacheEnabled), typeof(bool), typeof(ImageExBase), new PropertyMetadata(false));

/// <summary>
/// Identifies the <see cref="CachingStrategy"/> dependency property.
/// </summary>
public static readonly DependencyProperty CachingStrategyProperty = DependencyProperty.Register(nameof(CachingStrategy), typeof(ImageExCachingStrategy), typeof(ImageExBase), new PropertyMetadata(ImageExCachingStrategy.Custom));

/// <summary>
/// Identifies the <see cref="EnableLazyLoading"/> dependency property.
/// </summary>
Expand Down Expand Up @@ -126,15 +121,6 @@ public bool IsCacheEnabled
set { SetValue(IsCacheEnabledProperty, value); }
}

/// <summary>
/// Gets or sets a value indicating how the <see cref="ImageEx"/> will be cached.
/// </summary>
public ImageExCachingStrategy CachingStrategy
{
get { return (ImageExCachingStrategy)GetValue(CachingStrategyProperty); }
set { SetValue(CachingStrategyProperty, value); }
}

/// <summary>
/// Gets or sets a value indicating whether gets or sets is lazy loading enable. (17763 or higher supported)
/// </summary>
Expand Down
175 changes: 91 additions & 84 deletions Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.Source.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ public partial class ImageExBase
/// </summary>
public static readonly DependencyProperty SourceProperty = DependencyProperty.Register(nameof(Source), typeof(object), typeof(ImageExBase), new PropertyMetadata(null, SourceChanged));

private Uri _uri;
private bool _isHttpSource;
private CancellationTokenSource _tokenSource = null;
//// Used to track if we get a new request, so we can cancel any potential custom cache loading.
private CancellationTokenSource _tokenSource;

private object _lazyLoadingSource;

/// <summary>
Expand Down Expand Up @@ -66,19 +66,28 @@ private static bool IsHttpUri(Uri uri)
return uri.IsAbsoluteUri && (uri.Scheme == "http" || uri.Scheme == "https");
}

/// <summary>
/// Method to call to assign an <see cref="ImageSource"/> value to the underlying <see cref="Image"/> powering <see cref="ImageExBase"/>.
/// </summary>
/// <param name="source"><see cref="ImageSource"/> to assign to the image.</param>
private void AttachSource(ImageSource source)
{
var image = Image as Image;
var brush = Image as ImageBrush;

if (image != null)
// Setting the source at this point should call ImageExOpened/VisualStateManager.GoToState
// as we register to both the ImageOpened/ImageFailed events of the underlying control.
// We only need to call those methods if we fail in other cases before we get here.
if (Image is Image image)
{
image.Source = source;
}
else if (brush != null)
else if (Image is ImageBrush brush)
{
brush.ImageSource = source;
}

if (source == null)
{
VisualStateManager.GoToState(this, UnloadedState, true);
}
}

private async void SetSource(object source)
Expand All @@ -88,15 +97,14 @@ private async void SetSource(object source)
return;
}

this._tokenSource?.Cancel();
_tokenSource?.Cancel();

this._tokenSource = new CancellationTokenSource();
_tokenSource = new CancellationTokenSource();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: we could move the initialization of this instance down below after our synchronous paths are taken, so that in case the image is loaded synchronously instead we can skip the allocation here entirely. Basically we really only need to create this if ProvideCachedResourceAsync is called, otherwise we don't use the token anywhere else 🙂


AttachSource(null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: is toggling this visual state unconditionally by design? Because if the input is not null, doing this here no matter what means we're basically always cycling between loaded -> unloaded -> loading -> loaded whenever a new image is displayed.

I'm thinking maybe we could skip a couple of visual states here and:

  1. Only switch to the unloaded state when we're actually setting a null source
  2. Only set the loading state if we're loading an image asynchronously (that is, in the first two paths of our LoadImageAsync method.

It's a minor change but in theory it might help with performance a tiny bit in cases where we have a lot of controls being displayed eg. in a virtualized list, as those would be toggling the target image all the time?
Just an idea 😄


if (source == null)
{
VisualStateManager.GoToState(this, UnloadedState, true);
return;
}

Expand All @@ -107,122 +115,121 @@ private async void SetSource(object source)
{
AttachSource(imageSource);

ImageExOpened?.Invoke(this, new ImageExOpenedEventArgs());
VisualStateManager.GoToState(this, LoadedState, true);
return;
}

_uri = source as Uri;
if (_uri == null)
var uri = source as Uri;
if (uri == null)
{
var url = source as string ?? source.ToString();
if (!Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out _uri))
if (!Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out uri))
{
ImageExFailed?.Invoke(this, new ImageExFailedEventArgs(new UriFormatException("Invalid uri specified.")));
VisualStateManager.GoToState(this, FailedState, true);
return;
}
}

_isHttpSource = IsHttpUri(_uri);
if (!_isHttpSource && !_uri.IsAbsoluteUri)
if (!IsHttpUri(uri) && !uri.IsAbsoluteUri)
{
_uri = new Uri("ms-appx:///" + _uri.OriginalString.TrimStart('/'));
uri = new Uri("ms-appx:///" + uri.OriginalString.TrimStart('/'));
}

await LoadImageAsync(_uri);
try
{
await LoadImageAsync(uri, _tokenSource.Token);
}
catch (OperationCanceledException)
{
// nothing to do as cancellation has been requested.
}
catch (Exception e)
{
ImageExFailed?.Invoke(this, new ImageExFailedEventArgs(e));
VisualStateManager.GoToState(this, FailedState, true);
}
}

private async Task LoadImageAsync(Uri imageUri)
private async Task LoadImageAsync(Uri imageUri, CancellationToken token)
{
if (_uri != null)
if (imageUri != null)
{
if (IsCacheEnabled)
{
switch (CachingStrategy)
var img = await ProvideCachedResourceAsync(imageUri, token);

if (!_tokenSource.IsCancellationRequested)
{
case ImageExCachingStrategy.Custom when _isHttpSource:
await SetHttpSourceCustomCached(imageUri);
break;
case ImageExCachingStrategy.Custom:
case ImageExCachingStrategy.Internal:
default:
AttachSource(new BitmapImage(imageUri));
break;
// Only attach our image if we still have a valid request.
AttachSource(img);
}
}
else if (string.Equals(_uri.Scheme, "data", StringComparison.OrdinalIgnoreCase))
else if (string.Equals(imageUri.Scheme, "data", StringComparison.OrdinalIgnoreCase))
{
var source = _uri.OriginalString;
var source = imageUri.OriginalString;
const string base64Head = "base64,";
var index = source.IndexOf(base64Head);
if (index >= 0)
{
var bytes = Convert.FromBase64String(source.Substring(index + base64Head.Length));
var bitmap = new BitmapImage();
AttachSource(bitmap);
await bitmap.SetSourceAsync(new MemoryStream(bytes).AsRandomAccessStream());

if (!_tokenSource.IsCancellationRequested)
{
AttachSource(bitmap);
}
}
}
else
{
AttachSource(new BitmapImage(_uri)
AttachSource(new BitmapImage(imageUri)
{
CreateOptions = BitmapCreateOptions.IgnoreImageCache
});
}
}
}

private async Task SetHttpSourceCustomCached(Uri imageUri)
/// <summary>
/// This method is provided in case a developer would like their own custom caching strategy for <see cref="ImageExBase"/>.
/// By default it uses the built-in UWP cache provided by <see cref="BitmapImage"/> and
/// the <see cref="Image"/> control itself. This method should return an <see cref="ImageSource"/>
/// value of the image specified by the provided uri parameter.
/// A <see cref="CancellationToken"/> is provided in case the current request is invalidated
/// (e.g. the container is recycled before the original image is loaded).
/// The Toolkit also has an image cache helper which can be used as well:
/// <see cref="CacheBase{T}.GetFromCacheAsync(Uri, bool, CancellationToken, List{KeyValuePair{string, object}})"/> in <see cref="ImageCache"/>.
/// </summary>
/// <example>
/// <code>
/// var propValues = new List&lt;KeyValuePair&lt;string, object>>();
///
/// if (DecodePixelHeight > 0)
/// {
/// propValues.Add(new KeyValuePair&lt;string, object>(nameof(DecodePixelHeight), DecodePixelHeight));
/// }
/// if (DecodePixelWidth > 0)
/// {
/// propValues.Add(new KeyValuePair&lt;string, object>(nameof(DecodePixelWidth), DecodePixelWidth));
/// }
/// if (propValues.Count > 0)
/// {
/// propValues.Add(new KeyValuePair&lt;string, object>(nameof(DecodePixelType), DecodePixelType));
/// }
///
/// // A token is provided here as well to cancel the request to the cache,
/// // if a new image is requested.
/// return await ImageCache.Instance.GetFromCacheAsync(imageUri, true, token, propValues);
/// </code>
/// </example>
/// <param name="imageUri"><see cref="Uri"/> of the image to load from the cache.</param>
/// <param name="token">A <see cref="CancellationToken"/> which is used to signal when the current request is outdated.</param>
/// <returns><see cref="Task"/></returns>
protected virtual Task<ImageSource> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
{
try
{
var propValues = new List<KeyValuePair<string, object>>();

if (DecodePixelHeight > 0)
{
propValues.Add(new KeyValuePair<string, object>(nameof(DecodePixelHeight), DecodePixelHeight));
}

if (DecodePixelWidth > 0)
{
propValues.Add(new KeyValuePair<string, object>(nameof(DecodePixelWidth), DecodePixelWidth));
}

if (propValues.Count > 0)
{
propValues.Add(new KeyValuePair<string, object>(nameof(DecodePixelType), DecodePixelType));
}

var img = await ImageCache.Instance.GetFromCacheAsync(imageUri, true, _tokenSource.Token, propValues);

lock (LockObj)
{
// If you have many imageEx in a virtualized ListView for instance
// controls will be recycled and the uri will change while waiting for the previous one to load
if (_uri == imageUri)
{
AttachSource(img);
ImageExOpened?.Invoke(this, new ImageExOpenedEventArgs());
VisualStateManager.GoToState(this, LoadedState, true);
}
}
}
catch (OperationCanceledException)
{
// nothing to do as cancellation has been requested.
}
catch (Exception e)
{
lock (LockObj)
{
if (_uri == imageUri)
{
ImageExFailed?.Invoke(this, new ImageExFailedEventArgs(e));
VisualStateManager.GoToState(this, FailedState, true);
}
}
}
// By default we just use the built-in UWP image cache provided within the Image control.
return Task.FromResult((ImageSource)new BitmapImage(imageUri));
}
}
}
Loading