A Coder
A Coder

Reputation: 3046

Image edit and save Out of Memory Exception C#

I'm working in a WPF application where I show my images in two places which means the same image gets loaded in two places. In one of the place the image will be shown along with other few images in a slider where it will be able to edit and save. If there is no image available in the location I should be showing a separate image Image not found which is not editable.

When I started working on the functionality I got the Used by another process exception during edit and save. So after searching I came up with a solution and now at a rare time I get the Out of memory exception when I click the Next or Previous or First or Last in slider. The slider is just an Image control with 4 buttons. When the buttons are clicked the below method is called. I'm not sure if there is any memory leaks.

bool NoImage = true;
private static readonly object _syncRoot = new object();
private BitmapSource LoadImage(string path)
{
    lock (_syncRoot) //lock the object so it doesn't get executed more than once at a time.
    {
        BitmapDecoder decoder = null;

        try
        {
            //If the image is not found in the folder, then show the image not found.
            if (!File.Exists(path) && (path != null))
            {
                System.Drawing.Bitmap ss = XXXX.Resources.ImageNotFound;
                var stream = new System.IO.MemoryStream();
                if (!File.Exists(Path.GetTempPath() + "ImageNotFound.jpg"))
                {
                    FileStream file = new FileStream(Path.GetTempPath() + "ImageNotFound.jpg", FileMode.Create, FileAccess.Write);
                    ss.Save(stream, ImageFormat.Jpeg);
                    stream.Position = 0;
                    stream.WriteTo(file);
                    file.Close();
                    stream.Close();
                }

                path = Path.Combine(Path.GetTempPath(), "ImageNotFound.jpg");
                NoImage = false;
            }
            else
            {
                if (!EnableForEdit)
                    NoImage = false;
                else
                    NoImage = true;
            }

            if (!string.IsNullOrEmpty(path) && (!NoImage || File.Exists(path)))
            {
                using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read))
                {
                    decoder = BitmapDecoder.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);

                }
                return decoder.Frames.FirstOrDefault();

            }
            else
                return null;
        }
        catch (OutOfMemoryException ex)
        {
            MessageBox.Show("Insufficient memory to handle the process. Please try again later.", "Application alert");                  

            return null;
        }
        catch (Exception ex)
        {
            // Error handling.
            throw new ApplicationException(ex.Message);
        }
        finally
        {
            decoder = null;
            GC.WaitForFullGCComplete(1000);
            GC.Collect(0, GCCollectionMode.Forced);
        }
    }
}


<Image x:Name="viewImage" Grid.Row="2" Height="100" Width="135" Source="{Binding DisplayImage, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, NotifyOnSourceUpdated=True}" />

If my approach is wrong, let me know where should I do the change or if there is any simpler way to do. Kindly help.

Note: The images which are loaded is above 5Mb

Upvotes: 0

Views: 798

Answers (1)

MikeT
MikeT

Reputation: 5500

Firstly when ever you create a stream you need to dispose of it once you are finished with it (Note Close does not Dispose, but Dispose does close), if not then the stream stays in memory consuming resources

so your code should look as follows

using(var stream = new System.IO.MemoryStream())
{
    if (!File.Exists(Path.GetTempPath() + "ImageNotFound.jpg"))
    {
        using(FileStream file = new FileStream(Path.GetTempPath() + "ImageNotFound.jpg", FileMode.Create, FileAccess.Write))
        {
            ss.Save(stream, ImageFormat.Jpeg);
            stream.Position = 0;
            stream.WriteTo(file);
        }
    }
}

Second you need to reduce your apps memory impact

to do that i would suggest leveraging the functionality already in WPF here is a quick example of how you should do this

Your Model

public class ImageItem
{
    public Uri URI{ get; set; }

    private BitmapSource _Source;

    public BitmapSource Source
    {
        get
        {
            try
            {
                if (_Source == null) _Source = new BitmapImage(URI);//lazy loading

            }
            catch (Exception)
            {
                _Source = null;
            }
            return _Source;
        }
    }
    public void Save(string filename)
    {
        var img = BitmapFrame.Create(Source);
        var encoder = new JpegBitmapEncoder();

        encoder.Frames.Add(img);
        using(var saveStream = System.IO.File.OpenWrite(filename))
            encoder.Save(saveStream)

    }


}

Your View Model

public class ImageList : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public ObservableCollection<ImageItem> Images { get; } = new ObservableCollection<ImageItem>();

    private int _SelectedIndex;
//  c# >= 6
    public static readonly PropertyChangedEventArgs SelectedIndexProperty = new PropertyChangedEventArgs(nameof(SelectedIndex));
//  c# < 6
//  public static readonly PropertyChangedEventArgs SelectedIndexProperty = new PropertyChangedEventArgs("SelectedIndex");

    public int SelectedIndex
    {
        get { return _SelectedIndex; }
        set
        {
            _SelectedIndex = value;
//          c# >= 6
            PropertyChanged?.Invoke(this, SelectedIndexProperty);
            PropertyChanged?.Invoke(this, CurrentImageProperty);
//          c# < 6
//          var handler = PropertyChanged;
//          if(handler !=null)
//          {
//              handler (this, SelectedIndexProperty);
//              handler (this, CurrentImageProperty);
//          }
        }
    }

//  c# >= 6
    public static readonly PropertyChangedEventArgs CurrentImageProperty = new PropertyChangedEventArgs(nameof(CurrentImage)); 
//  c# < 6
//  public static readonly PropertyChangedEventArgs CurrentImageProperty = new PropertyChangedEventArgs("CurrentImage"); 

    public ImageItem CurrentImage => Images.Count>0 ?  Images[SelectedIndex] : null;

    public void Next()
    {
        if (SelectedIndex < Images.Count - 1)
            SelectedIndex++;
        else
            SelectedIndex = 0;
    }
    public void Back()
    {
        if (SelectedIndex == 0)
            SelectedIndex = Images.Count - 1;
        else
            SelectedIndex--;
    }

    public void Add(string Filename)
    {
        Images.Add(new ImageItem() { URI= new Uri(Filename) });
//      c# >= 6
        PropertyChanged?.Invoke(this, CurrentImageProperty);
//      c# < 6
//      var handler = PropertyChanged;
//      if(handler !=null)
//      {
//          handler (this, CurrentImageProperty);
//      }
    }
}

and Finally your View

<Window x:Class="ImageDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ImageDemo"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <BitmapImage x:Key="NotFound" UriSource="C:\...\NotFound.png"/>
    </Window.Resources>
    <Window.DataContext>
        <local:ImageList/>
    </Window.DataContext>
    <DockPanel>
        <Button Content="&lt;" Click="Back_Click"/>
        <Button DockPanel.Dock="Right" Content="&gt;" Click="Next_Click"/>
        <Image Source="{Binding CurrentImage.Source, Mode=OneWay, 
               TargetNullValue={StaticResource NotFound},
               FallbackValue={StaticResource NotFound}}"/>
    </DockPanel>
</Window>

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    //c# >= 6
    private ImageList list => DataContext as ImageList;
    //c# < 6
    //private ImageList list {get{ return DataContext as ImageList;}}

    private void Next_Click(object sender, RoutedEventArgs e)
    {
        list.Next();
    }

    private void Back_Click(object sender, RoutedEventArgs e)
    {
        list.Back();
    }
}

note: because the Model is separate to the View you can show the same image in several places with no issue at all

also System.Drawing.Bitmap is not WPF compatible so you should use the WPF classes in System.Windows.Media.Imaging

Upvotes: 4

Related Questions