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.
Related
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;
}
}
I was searching for around 4 months in order to solve image-flicker problem for my 180 controls, (similar to image-boxes) which are inserted into a flow-layout-panel.
I nearly tried everything (Enabling double flicker and so on...) and nothing worked for me. But, today I figured a solution and that's by setting image size-mode to normal instead of zoom or to resize my images to fit exactly into those controls similar to picture-boxes. So I chose the second option and here is my code:
public MovieControl(Movies M1, bool show, MainForm current)
{
InitializeComponent();
Current = current;
if (M1.movie_Banner == "") bunifuImageButton1.Image = Properties.Resources.no_image_available;
else
{
WebClient client = new WebClient();
Stream stream = client.OpenRead(M1.movie_Banner);
Bitmap bitmap; bitmap = new Bitmap(stream);
if (bitmap != null)
bunifuImageButton1.Image = new Bitmap((Image)bitmap, new Size(139, 208));
stream.Flush();
stream.Close();
client.Dispose();
}
bunifuImageButton1.Tag = M1;
M1 = null;
}
Also, I want to mention that this code is being called by a thread using the following lines:
private void RunMoviesThread()
{
ClearMovies();
LoadMoviesThread = new Thread(() => LoadMoviesControls());
LoadMoviesThread.Start();
}
private void LoadMoviesControls()
{
int x = 1;
if (MPageDropDown.InvokeRequired)
this.Invoke((MethodInvoker)delegate { if (MPageDropDown.SelectedItem != null) x = int.Parse(MPageDropDown.SelectedItem.ToString()); });
else if (MPageDropDown.SelectedItem != null) x = int.Parse(MPageDropDown.SelectedItem.ToString());
for (int i = (x - 1) * Max; i < MoviesList.Count() && i < x * Max; i++)
{
MovieControl tmp = new MovieControl(MoviesList[i], ShowMError, this);
if (tmp.InvokeRequired || MoviesFlowPanel.InvokeRequired)
{
MovieControl m1 = null;
try
{
m1= new MovieControl(MoviesList[i], ShowMError, this);
}
catch { }
this.Invoke((MethodInvoker)delegate { MoviesFlowPanel.Controls.Add(m1); });
}
else
MoviesFlowPanel.Controls.Add(tmp);
tmp = null;
}
}
It worked very well, instead of the fact that it took a very long time to process. for example; and image could take about half a second and up to 20 seconds in some rare cases! could you help me figure a more efficient way...
Please Note: my images are previously uploaded on the internet and this code is written in C#. Also, without the resizing process it took only few seconds to load.
**Max value is set to 180
Thanks in advance.
Well I don't know how much this will help you, but it's worth a shot. I am not entirely confident that WebClient can be parallelized like this, but a similar example appears on page 187 of Pro .NET Performance: Optimize Your C# Applications so I am going to take a leap and say that you can use WebClient from multiple threads. I have added a new method which you could just call from your UI, although it is making a check to be sure it doesn't need to be invoked. This method reads the page number and then launches the background process which will download and resize the images and then after it does all of that it will create the controls in the UI thread.
private void GetPageAndLoadControls()
{
if (MPageDropDown.InvokeRequired)
{
MPageDropDown.Invoke((MethodInvoker)(() => GetPageAndLoadControls();));
return;
}
var page = MPageDropDown.SelectedItem != null ?
int.Parse(MPageDropDown.SelectedItem.ToString()) - 1 : 0;
Task.Run(() => LoadMoviesControls(page));
}
private void LoadMoviesControls(int page)
{
var dict = new ConcurrentDictionary<string, Bitmap>();
var movies = MovieList.Skip(page * Max).Take(Max).ToList();
using (var client = new WebClient())
{
Parallel.ForEach(movies, (m) =>
{
Stream stream = null;
Bitmap bmp = null;
try
{
if (!string.IsNullOrWhitespace(m.movie_Banner)
{
stream = client.OpenRead(s);
bmp = new Bitmap(stream);
// Note: I am guessing on a key here, that maybe there is a title
// use whatever key is going to be best for your class
dict.TryAdd(m.movie_Title, new Bitmap(bmp, 139, 208));
}
else dict.TryAdd(m.movie_Title, Properties.Resources.no_image_available);
}
finally
{
bmp?.Dispose();
stream?.Dispose();
}
});
}
// Here we have to invoke because the controls have to be created on
// the UI thread. All of the other work has already been done in background
// threads using the thread pool.
MoviesFlowPanel.Invoke((MethodInvoker)() =>
{
foreach(var movie in movies)
{
Bitmap image = null;
dict.TryGetValue(movie.movie_Title, out image);
MoviesFlowPanel.Controls.Add(
new MovieControl(movie, image, ShowMError, this);
}
});
}
// Changed the constructor to now take an image as well, so you can pass in
// the already resized image
public MovieControl(Movies M1, Bitmap image, bool show, MainForm current)
{
InitializeComponent();
Current = current;
bunifuImageButton1.Image = image ?? Properties.Resources.no_image_available; // Sanity check
bunifuImageButton1.Tag = M1;
}
Edit
Something that occurred to me after writing this and thinking more about it, you did not post the code for ClearMovies(), but from the posted code I am assuming that you are disposing the previous 180 controls and creating 180 new ones. What would be better would be to use the same approach I have shown in the code above and then instead of "creating" new controls you just update the existing ones. You could add an update method to your user control that just changes the image and the Movie item that is being stored in the control. This would save the overhead of creating new controls each time which should improve the performance. You may need to call Invalidate at the end of the Update method. Not sure and since I can't really test this all out, just wanted to mention it. Also, instead of using the Tag property to hold the Movie object, I would just add a member variable to your UserControl. This would add type safety and make it clearer what is being held where.
public class MovieControl : Control
{
public Movies Movie { get; protected set; }
// Changed the constructor to now take an image as well, so you can
// pass in the already resized image
public MovieControl(Movies M1, Bitmap image, bool show, MainForm current)
{
InitializeComponent();
Current = current;
bunifuImageButton1.Image = image ?? Properties.Resources.no_image_available; // Sanity check
Movie = M1;
}
public void UpdateMovieAndImage(Movies movie, Image image)
{
// Only dispose if it isn't null and isn't the "No Image" image
if (bunifuImageButton1.Image != null
&& bunifuImageButton1.Image != Properties.Resources.no_image_available)
{
binifuImageButton1.Image.Dispose();
}
bunifuImageButton1.Image = image;
Movie = movie;
}
}
We are developing an app which have to attach an image at the runtime and display the same to the UI. We are using following set of code file for it:
private void Btn_Attach_File_Click(object sender, RoutedEventArgs e)
{
FileOpenPicker fileOpenPicker = new FileOpenPicker();
fileOpenPicker.FileTypeFilter.Add(".jpg");
fileOpenPicker.FileTypeFilter.Add(".jpeg");
fileOpenPicker.FileTypeFilter.Add(".png");
fileOpenPicker.ContinuationData["Operate"] = "OpenImage";
fileOpenPicker.PickSingleFileAndContinue();
}
Also, to continue the app after picking file from storage, here is the code :
public async void ContinueFileOpenPicker(FileOpenPickerContinuationEventArgs args)
{
if ((args.Files != null && args.Files.Count > 0))
{
IReadOnlyList<StorageFile> files = args.Files;
Image myImage = new Image();
foreach (StorageFile file in files)
{
if (args.ContinuationData["Operate"] as string == "OpenImage" && args.Files != null && args.Files.Count > 0)
{
IRandomAccessStream fileStream = await file.OpenAsync(Windows.Storage.FileAccessMode.Read);
System.Diagnostics.Debug.WriteLine(file.Name);
System.Diagnostics.Debug.WriteLine(file.Path);
using (StorageItemThumbnail thumbnail = await file.GetThumbnailAsync(ThumbnailMode.SingleItem, 190)) //, ThumbnailOptions.UseCurrentScale);
{
if (thumbnail != null && thumbnail.Type == ThumbnailType.Image)
{
BitmapImage bitmapImage = new BitmapImage();
bitmapImage.SetSource(thumbnail);
myImage.Source = bitmapImage;
}
}
}
}
}
}
Above code is in xaml.cs file.
But the problem is, even though I'm loading the file, there is no attachment on UI.
Do we need to make any changes to the corresponding xaml file? we've searched a lot on it but couldn't find any solution.
Thanks in advance,
First, make an image control in xaml:
<Image Name="img"/>
In codebehind after the loop:
this.img.Source = myImage.Source;
//For better perf according to the docs, specify this
//this.img.Source.DecodePixelWidth = 200
Certainly it's possible to make a binding instead of using the Name property, but this is the easiest way.
Read the docs: MSDN
One other thing, the loop foreach (StorageFile file in files) isn't that useful if you have one file, and if you have more than one file it will take the last file.
Feel free to ask for more info!
Happy coding :)!
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
};
}
I have a collection of custom objects called DataItems which contain URIs of images that I want to load and put in a collection for my Silverlight application to use.
As I process each DataItem, I get its SourceUri (e.g. "http://..../picture001.png") and start it loading:
void LoadNext()
{
WebClient webClientImgDownloader = new WebClient();
if (loadedItemIndex < RegisteredDataEntries.Count())
{
DataItem dataItem = RegisteredDataEntries[registeredIdCodes[loadedItemIndex]];
if (dataItem.Kind == DataItemKind.Image)
{
webClientImgDownloader.OpenReadCompleted +=
new OpenReadCompletedEventHandler(webClientImgDownloader_OpenReadCompleted);
webClientImgDownloader.OpenReadAsync(new Uri(dataItem.SourceUri,
UriKind.Absolute));
webClientImgDownloader.AddObject(dataItem); //????????????????????
webClientImgDownloader.Headers["idCode"] = dataItem.IdCode; //?????????????
webClientImgDownloader.ResponseHeaders["idCode"] = dataItem.IdCode; //?????????????
}
}
else
{
OnLoadingComplete(this, null);
}
}
Then when the loading of that image has completed, I save the image in a collection:
void webClientImgDownloader_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
{
dataItemIdCode = e.DataItem.IdCode; //???????????????????
dataitemIdCode = ((DataItem)sender).IdCode; //?????????????????????
BitmapImage bitmap = new BitmapImage();
bitmap.SetSource(e.Result);
Image image = new Image();
image.Source = bitmap;
Images.Add(dataItemIdCode, image);
}
But how do I pass the IdCode of the current DataItem through to my OpenReadCompleted method so that when that image has completed loading, I can also IDENTIFY it according to its IdCode?
ANSWER:
Franci's suggestion works, here are the lines for anyone else looking for this::
webClientImgDownloader.OpenReadAsync(new Uri(dataItem.SourceUri,
UriKind.Absolute), dataItem);
...
DataItem dataItem = e.UserState as DataItem;
There's an OpenDataAsync overload that takes a user token object. You should get this object in the OpenReadCompletedEventArgs, in the UserState property (inherited from AsyncCompletedEventArgs).