diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ImageEx/ImageExLazyLoadingControl.xaml b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ImageEx/ImageExLazyLoadingControl.xaml index a8f8b498d76..da20eed89e0 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ImageEx/ImageExLazyLoadingControl.xaml +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ImageEx/ImageExLazyLoadingControl.xaml @@ -11,7 +11,7 @@ - + + Click="CloseButton_Click" + Foreground="Black"> diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ImageEx/ImageExPage.xaml.cs b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ImageEx/ImageExPage.xaml.cs index f9acfd9f44e..5d9eebfa33a 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ImageEx/ImageExPage.xaml.cs +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ImageEx/ImageExPage.xaml.cs @@ -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(); } diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/CachingStrategy.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/CachingStrategy.cs deleted file mode 100644 index 9db1d3e1ed6..00000000000 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/CachingStrategy.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -namespace Microsoft.Toolkit.Uwp.UI.Controls -{ - /// - /// The type of caching to be applied to . - /// Default is - /// - public enum ImageExCachingStrategy - { - /// - /// Caching is handled by 's custom caching system. - /// - Custom, - - /// - /// Caching is handled internally by UWP. - /// - Internal - } -} diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageEx.Members.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageEx.Members.cs index 48d12972809..b0b63f55403 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageEx.Members.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageEx.Members.cs @@ -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 { @@ -33,7 +34,12 @@ public Thickness NineGrid /// public override CompositionBrush GetAlphaMask() { - return IsInitialized ? (Image as Image).GetAlphaMask() : null; + if (IsInitialized && Image is Image image) + { + return image.GetAlphaMask(); + } + + return null; } /// @@ -42,7 +48,12 @@ public override CompositionBrush GetAlphaMask() /// The image as a . public CastingSource GetAsCastingSource() { - return IsInitialized ? (Image as Image).GetAsCastingSource() : null; + if (IsInitialized && Image is Image image) + { + return image.GetAsCastingSource(); + } + + return null; } } } \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.Members.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.Members.cs index 55db3a5f391..abc687227e8 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.Members.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.Members.cs @@ -40,11 +40,6 @@ public partial class ImageExBase /// public static readonly DependencyProperty IsCacheEnabledProperty = DependencyProperty.Register(nameof(IsCacheEnabled), typeof(bool), typeof(ImageExBase), new PropertyMetadata(false)); - /// - /// Identifies the dependency property. - /// - public static readonly DependencyProperty CachingStrategyProperty = DependencyProperty.Register(nameof(CachingStrategy), typeof(ImageExCachingStrategy), typeof(ImageExBase), new PropertyMetadata(ImageExCachingStrategy.Custom)); - /// /// Identifies the dependency property. /// @@ -126,15 +121,6 @@ public bool IsCacheEnabled set { SetValue(IsCacheEnabledProperty, value); } } - /// - /// Gets or sets a value indicating how the will be cached. - /// - public ImageExCachingStrategy CachingStrategy - { - get { return (ImageExCachingStrategy)GetValue(CachingStrategyProperty); } - set { SetValue(CachingStrategyProperty, value); } - } - /// /// Gets or sets a value indicating whether gets or sets is lazy loading enable. (17763 or higher supported) /// diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.Source.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.Source.cs index 2b13c681121..da35c09ac7a 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.Source.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.Source.cs @@ -24,9 +24,9 @@ public partial class ImageExBase /// 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; /// @@ -66,19 +66,28 @@ private static bool IsHttpUri(Uri uri) return uri.IsAbsoluteUri && (uri.Scheme == "http" || uri.Scheme == "https"); } + /// + /// Method to call to assign an value to the underlying powering . + /// + /// to assign to the image. 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) @@ -88,15 +97,14 @@ private async void SetSource(object source) return; } - this._tokenSource?.Cancel(); + _tokenSource?.Cancel(); - this._tokenSource = new CancellationTokenSource(); + _tokenSource = new CancellationTokenSource(); AttachSource(null); if (source == null) { - VisualStateManager.GoToState(this, UnloadedState, true); return; } @@ -107,65 +115,75 @@ 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 }); @@ -173,56 +191,45 @@ private async Task LoadImageAsync(Uri imageUri) } } - private async Task SetHttpSourceCustomCached(Uri imageUri) + /// + /// This method is provided in case a developer would like their own custom caching strategy for . + /// By default it uses the built-in UWP cache provided by and + /// the control itself. This method should return an + /// value of the image specified by the provided uri parameter. + /// A 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: + /// in . + /// + /// + /// + /// 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)); + /// } + /// + /// // 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); + /// + /// + /// of the image to load from the cache. + /// A which is used to signal when the current request is outdated. + /// + protected virtual Task ProvideCachedResourceAsync(Uri imageUri, CancellationToken token) { - try - { - var propValues = new List>(); - - if (DecodePixelHeight > 0) - { - propValues.Add(new KeyValuePair(nameof(DecodePixelHeight), DecodePixelHeight)); - } - - if (DecodePixelWidth > 0) - { - propValues.Add(new KeyValuePair(nameof(DecodePixelWidth), DecodePixelWidth)); - } - - if (propValues.Count > 0) - { - propValues.Add(new KeyValuePair(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)); } } } \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.cs index f3f749a97d0..e4f62537a46 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.cs @@ -21,7 +21,6 @@ namespace Microsoft.Toolkit.Uwp.UI.Controls [TemplateVisualState(Name = UnloadedState, GroupName = CommonGroup)] [TemplateVisualState(Name = FailedState, GroupName = CommonGroup)] [TemplatePart(Name = PartImage, Type = typeof(object))] - [TemplatePart(Name = PartProgress, Type = typeof(ProgressRing))] public abstract partial class ImageExBase : Control { private bool _isInViewport; @@ -31,11 +30,6 @@ public abstract partial class ImageExBase : Control /// protected const string PartImage = "Image"; - /// - /// ProgressRing name in template - /// - protected const string PartProgress = "Progress"; - /// /// VisualStates name in template /// @@ -66,22 +60,11 @@ public abstract partial class ImageExBase : Control /// protected object Image { get; private set; } - /// - /// Gets backing object for the ProgressRing - /// - protected ProgressRing Progress { get; private set; } - - /// - /// Gets object used for lock - /// - protected object LockObj { get; private set; } - /// /// Initializes a new instance of the class. /// public ImageExBase() { - LockObj = new object(); } /// @@ -90,14 +73,11 @@ public ImageExBase() /// Routed Event Handler protected void AttachImageOpened(RoutedEventHandler handler) { - var image = Image as Image; - var brush = Image as ImageBrush; - - if (image != null) + if (Image is Image image) { image.ImageOpened += handler; } - else if (brush != null) + else if (Image is ImageBrush brush) { brush.ImageOpened += handler; } @@ -109,14 +89,11 @@ protected void AttachImageOpened(RoutedEventHandler handler) /// RoutedEventHandler protected void RemoveImageOpened(RoutedEventHandler handler) { - var image = Image as Image; - var brush = Image as ImageBrush; - - if (image != null) + if (Image is Image image) { image.ImageOpened -= handler; } - else if (brush != null) + else if (Image is ImageBrush brush) { brush.ImageOpened -= handler; } @@ -128,14 +105,11 @@ protected void RemoveImageOpened(RoutedEventHandler handler) /// Exception Routed Event Handler protected void AttachImageFailed(ExceptionRoutedEventHandler handler) { - var image = Image as Image; - var brush = Image as ImageBrush; - - if (image != null) + if (Image is Image image) { image.ImageFailed += handler; } - else if (brush != null) + else if (Image is ImageBrush brush) { brush.ImageFailed += handler; } @@ -147,14 +121,11 @@ protected void AttachImageFailed(ExceptionRoutedEventHandler handler) /// Exception Routed Event Handler protected void RemoveImageFailed(ExceptionRoutedEventHandler handler) { - var image = Image as Image; - var brush = Image as ImageBrush; - - if (image != null) + if (Image is Image image) { image.ImageFailed -= handler; } - else if (brush != null) + else if (Image is ImageBrush brush) { brush.ImageFailed -= handler; } @@ -169,7 +140,6 @@ protected override void OnApplyTemplate() RemoveImageFailed(OnImageFailed); Image = GetTemplateChild(PartImage) as object; - Progress = GetTemplateChild(PartProgress) as ProgressRing; IsInitialized = true; @@ -191,26 +161,23 @@ protected override void OnApplyTemplate() base.OnApplyTemplate(); } - /// - protected override Size ArrangeOverride(Size finalSize) - { - var newSquareSize = Math.Min(finalSize.Width, finalSize.Height) / 8.0; - - if (Progress?.Width == newSquareSize) - { - Progress.Height = newSquareSize; - } - - return base.ArrangeOverride(finalSize); - } - - private void OnImageOpened(object sender, RoutedEventArgs e) + /// + /// Underlying event handler. + /// + /// Image + /// Event Arguments + protected virtual void OnImageOpened(object sender, RoutedEventArgs e) { ImageExOpened?.Invoke(this, new ImageExOpenedEventArgs()); VisualStateManager.GoToState(this, LoadedState, true); } - private void OnImageFailed(object sender, ExceptionRoutedEventArgs e) + /// + /// Underlying event handler. + /// + /// Image + /// Event Arguments + protected virtual void OnImageFailed(object sender, ExceptionRoutedEventArgs e) { ImageExFailed?.Invoke(this, new ImageExFailedEventArgs(new Exception(e.ErrorMessage))); VisualStateManager.GoToState(this, FailedState, true); diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Core/Microsoft.Toolkit.Uwp.UI.Controls.Core.csproj b/Microsoft.Toolkit.Uwp.UI.Controls.Core/Microsoft.Toolkit.Uwp.UI.Controls.Core.csproj index b604c5a7219..973416dd091 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Core/Microsoft.Toolkit.Uwp.UI.Controls.Core.csproj +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Core/Microsoft.Toolkit.Uwp.UI.Controls.Core.csproj @@ -20,7 +20,7 @@ UWP Toolkit Windows Controls XAML Markdown CameraPreview Camera DropShadow ImageEx InAppNotification InfiniteCanvas Radial RadialProgressBar Scroll ScrollHeader Tile false - 8.0 + 9.0