UI freezes at the moment Image control loads BitmapImage [duplicate] - c#

This question already has an answer here:
Load a large BitmapImage asynchronously
(1 answer)
Closed 2 years ago.
My WPF MVVM application loads an image from the given URL asynchronously, through Webclient.DownloadFileAsync(url, fileLocation). That process goes fine and smooth, no freezes at all when downloading a picture. But the problem occurs when I present the image file to the user - an application becomes unresponsive.
After file is downloaded, I assign the image file to the BitmapImage:
public async void LoadFileToBitmapImage(string filePath)
{
_isDownloading = false;
await FileToBitmapImage(filePath);
}
public Task FileToBitmapImage(string filePath)
{
return Task.Run(() =>
{
var executableLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
var imageLocation = Path.Combine(executableLocation, filePath);
var bi = new BitmapImage();
bi.BeginInit();
bi.UriSource = new Uri(imageLocation);
bi.EndInit();
bi.Freeze();
Image = bi;
});
}
Image.cs:
private BitmapImage _image;
public BitmapImage Image
{
get => _image;
set
{
_image = value;
NotifyOfPropertyChange("Image");
}
}
XAML Image Binding:
<Image Source="{Binding Image, IsAsync=True}" Margin="3"/>
The problem occurs when the image is downloaded and presenting it to the user. The bigger an image, the more time it takes to present an image to the user and the more time an application is unresponsive.
I tried clicking pause at that very time when the application freezes to check threads and get the following info and unfortunately it doesn't provide me with any information.
Any help will be much appreciated!
Edit
Worth noting that application becomes unresponsive after PropertyChanged event is raised, not before. Maybe it's something to do with rendering an image to the UI?

first, if you save the image, change the binding to a string/uri directly, no BitmapImage, no nned to create it, Wpf handle that for you
public BitmapImage Image ===> public Uri Image
and remove FileToBitmapImage.

I spent a few days to find a simple solution to this problem. I needed to display over a hundred images in high quality without UI freezing.
I tried various modifications of the binding and so on in the end only the creation of the Image control through the code and set of the Source property worked before Image appeared in the tree of interface elements.
In XAML only empty ContentControl:
<ContentControl x:Name="ImageContent"/>
In code:
static readonly ConcurrentExclusiveSchedulerPair _pair = new ConcurrentExclusiveSchedulerPair();
// Works for very big images
public void LoadImage(Uri imageUri)
{
var image = new System.Windows.Controls.Image(); // On UI thread
RenderOptions.SetBitmapScalingMode(image, BitmapScalingMode.HighQuality);
Task.Factory.StartNew(() =>
{
var source = new BitmapImage(imageUri); // load ImageSource
Dispatcher.RunOnMainThread(() =>
{
image.Source = source; // Set source while not in UI
// Add image in UI tree
ImageContent.Content = image; // ImageContent is empty ContentControl
ImageContent.InvalidateVisual();
});
}, default, TaskCreationOptions.LongRunning, _pair.ExclusiveScheduler);
}
Works better with loading image with CacheOption OnLoad.
public static ImageSource BitmapFromUri(Uri source)
{
if (source == null)
return new BitmapImage(source);
using (var fs = new FileStream(source.LocalPath, FileMode.Open))
{
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.StreamSource = fs;
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.EndInit();
bitmap.Freeze();
return bitmap;
}
}

Related

BitmapImage created on separate thread and frozen does not show

I have an Image with Source bound to a view model BitmapImage property. Its an icon to show for each tree view item in a tree view. I want to load the images in a separate thread to reduce blocking of the UI thread during initialisation of the tree.
I have read various answers to similar questions but I have not yet found the solution to this particular problem. I am aware that to make a BitmapImage available across threads you have to Freeze the image but it is this freezing that stops it rendering.
I load the image as follows:
private void LoadImage()
{
Task.Run(() =>
{
if (string.IsNullOrWhiteSpace(_imagePath))
return ;
string[] resources = GetResourceNames();
var result = resources?.FirstOrDefault(x => String.Equals(x, _image, StringComparison.CurrentCultureIgnoreCase)) ??
resources?.FirstOrDefault(x => String.Equals(Path.GetFileName(x), _imagePath, StringComparison.CurrentCultureIgnoreCase));
var image = result == null ? null : new BitmapImage(GetUri(ImageResourceCache.ImagesAssembly.FullName, result));
if (image == null) return;
image.CacheOption = BitmapCacheOption.OnLoad; //<== a suggested solution that does not make a difference
image.Freeze();//<== freezing stops the cross thread exception but stops rendering
DispatcherHelp.CheckInvokeOnUI(() => Image = image);
});
}
private static string[] GetResourceNames()
{
var asm = ImageResourceCache.ImagesAssembly;
var resName = asm.GetName().Name + ".g.resources";
using (var stream = asm.GetManifestResourceStream(resName))
{
if (stream == null) return null;
using (var reader = new ResourceReader(stream))
return reader.Cast<DictionaryEntry>().Select(entry => (string)entry.Key).ToArray();
}
}
private static Uri GetUri(string dllName, string relativeFilePath)
{
return new Uri($"/{dllName};component/{relativeFilePath}", UriKind.RelativeOrAbsolute);
}
LoadImage is called in the view model Constructor. The _imagePath is passed to the constructor.
If I remove the Task.Run and the freeze it renders. Put the freeze back and it no longer renders.
Binding is as follows:
<Image Source="{Binding Image}" Stretch="Uniform" Margin="0 0 3 0" />
viewmodel:
public BitmapImage Image
{
get => _image;
set
{
_image = value;
RaisePropertyChanged();
}
}
You should use a pack URI to identify the image resouce. This should work provided that there is an image/pic.jpeg resource:
Task.Run(() =>
{
BitmapImage image = new BitmapImage(new Uri("pack://application:,,,/images/pic.jpeg", UriKind.RelativeOrAbsolute));
image.Freeze();
Dispatcher.BeginInvoke(new Action(() => Image = image));
});
Your GetUri method seems to be missing the pack://application:,,,/ part. There is no reason to look up the resources like you are currently doing when you can use a pack URI.

Can't delete a file after using it in a custom window (WPF)

I have a custom window named ImageWindow, which has a Image control in it.
public void DownloadImageWithExtension(Uri url, ImageFormat format)
{
if (File.Exists(path))
{
File.Delete(path));
}
using (WebClient client = new WebClient())
{
client.DownloadFile(url, path);
}
ImageWindow iw = new ImageWindow(path);
iw.ShowDialog();
}
This is my main windows code, which calls ImageWindow (iw) passing the file to open, which has been downloaded.
public partial class ImageWindow : Window
{
public ImageWindow(string fileName)
{
InitializeComponent();
BitmapImage image = new BitmapImage(new Uri(fileName));
image_Image.Source = image; //Set Image controls image source to BitmapImage
}
}
Here i get the image, save it to BitmapImage and add to Source of the Image control inside the newly opened window.
After i close the window and try to open the window again, i get this I/O exception.
The process cannot access the file '\bin\Debug\10supercomfywallpapers!.Jpeg' because it is being used by another process.
I have found some fixes mentioning using dispose and such, but BitmapImage doesn't inherit IDisposable.
EDIT: The deletion will be moved to after the OpenDialog() returns, but i need to fix the window not disposing first to do that.
Try to set the CacheOption to BitmapCacheOption.OnLoad:
public partial class ImageWindow : Window
{
public ImageWindow(string fileName)
{
InitializeComponent();
BitmapImage image = new BitmapImage();
image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad;
image.UriSource = new Uri(fileName);
image.EndInit();
image.Freeze();
image_Image.Source = image; //Set Image controls image source to BitmapImage
}
}
This should prevent the file from being locked.

Xaml Image.Source is not updating in C#

I have some trouble with updating the source of an Image in Xaml. Im making a Windows Store App, and Im trying to set the source in my C# code. Basically, what my small program is doing is to let the user select a JPG file, and then copy this over to AppData folder. In my App, I want the picture the user have uploaded to show. Everything is working except the part where I show the image, this image does not want to change even if I provide a new source.
C# Code:
public sealed partial class MainPage : Page
{
FileOpenPicker pickerSelect;
FileSavePicker pickerSave;
StorageFolder folder;
StorageFile pic;
public MainPage()
{
this.InitializeComponent();
InitializePickers();
InitializeProfilePicture();
}
private async void InitializeProfilePicture()
{
folder = Windows.Storage.ApplicationData.Current.LocalFolder;
pic = await folder.GetFileAsync("profile.jpg");
BitmapImage uri = new BitmapImage(new Uri(pic.Path, UriKind.Absolute));
ProfilePic.Source = uri;
}
private void InitializePickers()
{
pickerSelect = new FileOpenPicker();
pickerSave = new FileSavePicker();
pickerSelect.FileTypeFilter.Add(".jpg");
}
private async void Upload_Click(object sender, RoutedEventArgs e)
{
StorageFile pictureSelect = await pickerSelect.PickSingleFileAsync();
StorageFolder folder = Windows.Storage.ApplicationData.Current.LocalFolder;
await pictureSelect.CopyAsync(folder, "profile.jpg", NameCollisionOption.ReplaceExisting);
InitializeProfilePicture();
}
}
In the method "InitializeProfilePicture" I create a new BitmapImage and I set ProfilePic to this. This code is just working once, if I run the InitializeProfilePicture in the start as I do now, and the user selects a new picture and uploads to the AppData folder, the image does not change (even though the picture is indeed uploaded). If I remove the method from the start and just keep it in the Button_Click method, the new uploaded picture will be the one showing. But uploading a new picture after ProfilePic have set it's source, it will not change.
Image in Xaml is just like this
Image Width="480" Height="640" x:Name="ProfilePic" Grid.Row="1" Grid.RowSpan="2"
.. And there is also a button here to run Upload_Click method, but thats it.
Any idea why this is happening?? Should it not be updated??
You could load a BitmapImage directly from file like this:
var imageFile = await picker.PickSingleFileAsync();
var bitmap = new BitmapImage();
using (var stream = await imageFile.OpenReadAsync())
{
await bitmap.SetSourceAsync(stream);
}
ProfilePic.Source = bitmap;

Why does a listbox take forever to load when I override its ItemsPanel to wrappanel?

I'm trying to add items (images) to a ListBox, preferably with a WrapPanel type layout. Pretty simple.
The issue I have is that when I define the ListBox.ItemsPanel template to use a wrap panel, the time spent loading the images becomes unbearable. I am adding about 70 images currently, but I'd like this to support thousands.
If I don't customize the listbox, things load pretty darn fast. I've mentioned below in the XAML exactly what part I am changing.
What I don't understand is
Why such a seemingly small xaml change makes the app choke
How I can still get wrappanel type layout with decent perf.
<Grid>
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<Button Grid.Row=0 Name="ImageButton" Click="ImageButton_Click">GET IMAGES</Button>
<ListBox Grid.Row="1" Name="ImageCollection" ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<!-- Items added to list panel using Wrap Panel Template. -->
<!-- By removing the below, things go pretty fast -->
<!-- When the ListBox.ItemsPanel uses a wrappanel as below, loading up the images is SLOW. If I delete the below, loading is FAST, but isn't what I want. -->
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
</Grid>
My Code behind looks like this. I don't change anything here, but just including it as informational.
public AsyncGet()
{
InitializeComponent();
}
private List<String> images = new List<string>(
Directory.GetFiles(#"C:\Users\me\Pictures\wallpapers")
);
private void ImageButton_Click(object sender, RoutedEventArgs e)
{
foreach (string s in images)
{
Image image = new Image();
BitmapImage bi = new BitmapImage();
bi.BeginInit();
bi.UriSource = new Uri(s);
bi.EndInit();
image.Source = bi;
image.Height = 200;
image.Width = 200;
ImageCollection.Items.Add(image);
}
}
EDIT
After trying out a number of VirtualWrapPanel implementations that I was not really happy with, I decided to stick with WrapPanel. I did a few things that seem to keep the main thread responsive:
Keep the BitMapImage Decode dimensions and Image dimensions to 200
Begin the loading by adding a default image for each item. The intended path is stored in the Images tag.
Go through each item update the source, using async/await to generate the bitmapImage.
I don't know I'm using async/await the way it was intended. But it seems to allow the UI to remain responsive. The images are cycled over and eventually all defaults are replaced, it may take a little longer than otherwise but I prefer the ability to still take user input.
I wish I could find a way to databind this, I may try it later.
private List<String> images = new List<string>(
Directory.GetFiles(#"C:\Users\me\Pictures\wallpapers")
);
private Image defaultImage;
public AsyncGet()
{
InitializeComponent();
DataContext = this;
defaultImage = new Image();
defaultImage.Source = MakeBitmapImage( #"C:\Users\me\Pictures\MeowShawn_Lynch.jpg");
defaultImage.Height = 200;
defaultImage.Width = 200;
}
private async void ImageButton_Click(object sender, RoutedEventArgs e)
{
ImageCollection.Items.Clear();
foreach(string s in images)
{
Image image = new Image();
image.Source = defaultImage.Source;
image.Height = defaultImage.Height;
image.Width = defaultImage.Width;
ImageCollection.Items.Add(image);
image.Tag = s;
}
foreach (Image image in ImageCollection.Items)
{
string path = image.Tag.ToString();
Task<BitmapImage> task = new Task<BitmapImage>(() => MakeBitmapImage(path, true));
task.Start();
image.Source = await task;
}
}
private BitmapImage MakeBitmapImage(string path, Boolean freeze = false)
{
BitmapImage bi = new BitmapImage();
bi.CacheOption = BitmapCacheOption.OnLoad;
bi.BeginInit();
bi.UriSource = new Uri(path);
bi.DecodePixelHeight = 200;
bi.EndInit();
if (freeze)
{
bi.Freeze();
}
return bi;
}
By default ListBox will use VirtualizingStackPanel which does not render elements that aren't in the view at the moment.
The word "virtualize" refers to a technique by which a subset of user interface (UI) elements are generated from a larger number of data items based on which items are visible on-screen. Generating many UI elements when only a few elements might be on the screen can adversely affect the performance of your application. The VirtualizingStackPanel calculates the number of visible items and works with the ItemContainerGenerator from an ItemsControl (such as ListBox or ListView) to create UI elements only for visible items.
WrapPanel does not have that functionality and all items are treated as if they are visible. You can try using this Virtualizing WrapPanel control.
For normal ListBox, with default ItemsPanel, you can control virtualization by setting VirtualizingStackPanel.IsVirtualizing attached property:
<ListBox VirtualizingStackPanel.IsVirtualizing="True"
Suggestion: Instead of populating the images collection using the UIElement's name (ImageCollection), implement INotifyPropertyChanged and expose the List of Images as property of the Form, than use Binding (that is the WPF - Way of doing such things) and last but not least check if enabling/diabling virtualization has any impact on your issue...
If you want to support thousands then consider a class that only loads the BitmapImage on demand
This example only loads the image when Virtualization asks for it
The ListBox has Image with itemsource bound to BitmapImage
Showing items as images in a WPF ListView
public class ImageDefer
{
private string source;
private BitmapImage bi;
public BitmapImage BI
{
get
{
if (bi is null)
{
bi = new BitmapImage();
bi.BeginInit();
bi.UriSource = new Uri(source);
bi.EndInit();
}
return bi;
}
}
public ImageDefer (string Source) { source = Source; }
}

Display loading while Caching image from URL

I have a code in WPF C# that i use to load images from the web as such:
if (myImgURL != "")
{
var imgBitmap = new BitmapImage();
imgBitmap.BeginInit();
imgBitmap.UriSource = new Uri(myImgURL, UriKind.RelativeOrAbsolute);
imgBitmap.CacheOption = BitmapCacheOption.OnLoad;
imgBitmap.EndInit();
myImgControl.Source = imgBitmap;
}
It works perfectly but sometimes it takes a while before the images get displayed (if internet is slow and all). How can I have a ProgressRing (from the Mahapps.Metro toolkit) display and be enabled while the image loads and then disappear when the image is displayed?
I am not aware of any event trigger for when an image is being downloaded and when it is fully loaded.
Take a look at the following events in class BitmapSource (the base class of BitmapImage):
DownloadProgress
DownloadCompleted
DownloadFailed
And just a note. You are creating a BitmapImage from an Uri and showing it immediately. Hence there is no need to set BitmapCacheOption.OnLoad (which afaik is necessary only if you load from a stream that should be closed immediately after EndInit). So you could shorten your code like this:
if (!string.IsNullOrEmpty(myImgURL))
{
var imgBitmap = new BitmapImage(new Uri(myImgURL));
myImgControl.Source = imgBitmap;
if (imgBitmap.IsDownloading)
{
// start download animation here
imgBitmap.DownloadCompleted += (o, e) =>
{
// stop download animation here
};
imgBitmap.DownloadFailed += (o, e) =>
{
// stop download animation here
};
}

Categories