Search code examples
c#wpfimagemvvmlarge-files

Bringing large image into view at runtime


Here is the problem. I have a view, that should display an image and some controls. User add new images, changes some options and click "finish". Images are large and very large (400-1500 MB Tiff) User should see the preview of image, but it is ok if it loading for 10-15 sec or even more, he have a job for this time. Image is binding through MVVM pattern like simple string (file will be always in local folder)

<Image Name="ImagePreview" Source="{Binding SFilePathForPreview,
         FallbackValue={StaticResource DefaultImage},
         TargetNullValue={StaticResource DefaultImage}}"
         HorizontalAlignment="Center" Width="200" Height="200"
         VerticalAlignment="Center" />

Problem is that all is hangs when user try to add a file for loading time. I understand that this case should be solved through multithreading - but have no idea how to implement this.

I tryed to update image from view in different thread like this:

Thread newThread = new Thread(LazyLoad);
newThread.Name = "LazyLoad";
newThread.Start(SFilePathForPreview);

public void LazyLoad(object SFilePath)
{            
    try
    {
        string path = (string)SFilePath;

        BitmapImage t_source = new BitmapImage();

        t_source.BeginInit();
        t_source.UriSource = new Uri(path);
        t_source.DecodePixelWidth = 200;
        t_source.EndInit();

        t_source.Freeze();
        this.Dispatcher.Invoke(new Action(delegate
        {
            ImagePreview.Source = t_source;
        }));
    }
    catch
    {
        //...
    }
}

But anyway at point

ImagePreview.Source = t_source;

everything hangs up until image fully loaded.

Is there a way to load a preview in the background and show it without those terrible hangs?


Solution

  • The probably most simple way of asynchronously loading an image is via an asynchronous Binding. You would not have to deal with Threads or Tasks at all.

    <Image Source="{Binding Image, IsAsync=True}"/>
    

    A possible view model could look like shown below, where you must make sure that the Image property getter can be called from a background thread.

    public class ViewModel : ViewModelBase
    {
        private string imagePath;
        private BitmapImage image;
    
        public string ImagePath
        {
            get { return imagePath; }
            set
            {
                imagePath = value;
                image = null;
                OnPropertyChanged(nameof(ImagePath));
                OnPropertyChanged(nameof(Image));
            }
        }
    
        public BitmapImage Image
        {
            get
            {
                lock (this)
                {
                    if (image == null &&
                        !string.IsNullOrEmpty(imagePath) &&
                        File.Exists(imagePath))
                    {
                        using (var stream = File.OpenRead(imagePath))
                        {
                            image = new BitmapImage();
                            image.BeginInit();
                            image.CacheOption = BitmapCacheOption.OnLoad;
                            image.DecodePixelWidth = 200;
                            image.StreamSource = stream;
                            image.EndInit();
                            image.Freeze();
                        }
                    }
                }
    
                return image;
            }
        }
    }