tags:

views:

632

answers:

4

I am writing an Image Manager WPF application. I have a ListBox with the following ItemsTemplate:

        <Grid x:Name="grid" Width="150" Height="150" Background="{x:Null}">
            <Grid.RowDefinitions>
                <RowDefinition Height="*"/>
                <RowDefinition Height="27.45"/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="150"/>
            </Grid.ColumnDefinitions>
            <Border Margin="5,5,5,5.745" Grid.RowSpan="2" Background="#FF828282" BorderBrush="{DynamicResource ListBorder}" CornerRadius="5,5,5,5" BorderThickness="1,1,2,2" x:Name="border">
                <Grid>
                    <Viewbox Margin="0,0,0,21.705">
                        <Image Width="Auto" Height="Auto" x:Name="picture" Source="{Binding Path=FullName}" />
                    </Viewbox>
                    <TextBlock Height="Auto" Text="{Binding Path=Name}" TextWrapping="Wrap" x:Name="PictureText" HorizontalAlignment="Left" Margin="70,0,0,0" VerticalAlignment="Bottom" />
                </Grid>
            </Border>
        </Grid>

Note that the "Image" control is bound to the "FullName" property, which is a string representing the absolute path to a JPG.

Several application features require that I alter the JPG file (move, rename, or delete). When I try to do so (currently trying to Move the file) I receive an IOException: "The process cannot access the file because it is being used by another process." The process locking the file is my WPF application.

I did some searching online and found several postings indicating that Images in particular have trouble letting go of their resources. I have tried the following:

  1. Setting the ListBox.Source to null
  2. Adding a 10 second wait time before attempting the move.
  3. Issuing GC.Collect().
  4. Moving the operation to a different thread.

What else can I try? I thought about finding a reference to the Image object in the ItemsTemplate and trying to dispose of the Image, but I can't figure out how to get the reference.

One possible solution I read about was to create copies of the Images rather than the actual images, but since the Binding is to the filename and not the actual Image I don't know if I could make this work.

Any help or suggestions would be most appreciated.

+2  A: 

Check out this post here.

http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/dee7cb68-aca3-402b-b159-2de933f933f1/

Sample

Basically you'll have to preload the image using a stream. I would create a PreLoadImageConverter, something like this, I haven't tested it.

<Grid>
  <Grid.Resources>
    <local:PreLoadImageConverter x:Key="imageLoadingConverter" />
  </Grid.Resources>
  <Image Width="Auto" Height="Auto" x:Name="picture" Source="{Binding Path=FullName, Converter={StaticResource imageLoadingConverter}}" />
</Grid>

PreLoadImageConverter.cs

public class PreLoadImageConverter : IValueConverter
{
  public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
  {
    if (value == null) return null;
    string imagePath = value.ToString();

    ImageSource imageSource;
    using (var stream = new MemoryStream())
    {
      Bitmap bitmap = new Bitmap(imagePath);
      bitmap.Save(stream, System.Drawing.Imaging.ImageFormat.Png);   
      PngBitmapDecoder bitmapDecoder = new PngBitmapDecoder(stream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad);
      imageSource = bitmapDecoder.Frames[0];
      imageSource.Freeze();
    }
    return imageSource;
  }

  public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
  {
    throw new Exception("The method or operation is not implemented.");
  }
}
bendewey
I've tried this, but still cannot move the file. I added a using() around the Bitmap as well, thinking maybe it hasn't been disposed yet, but same results.
Joel Cochran
That seems odd, Can you move or delete the file when the app isn't running? Is it possible that the file is still lock from some previous instance?
bendewey
Yes, I can move it manually.
Joel Cochran
+3  A: 

My Intuipic application allows users to delete images, too. I had to write this converter to achieve it. Relevant code:

//create new stream and create bitmap frame
BitmapImage bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.StreamSource = new FileStream(path, FileMode.Open, FileAccess.Read);
bitmapImage.DecodePixelWidth = (int) _decodePixelWidth;
bitmapImage.DecodePixelHeight = (int) _decodePixelHeight;
//load the image now so we can immediately dispose of the stream
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.EndInit();

//clean up the stream to avoid file access exceptions when attempting to delete images
bitmapImage.StreamSource.Dispose();

HTH, Kent

Kent Boogaart
I tried this (without the DecodePixelWidth/Height), but still the file is locked. There must be something else going on - I'll have to start looking for other issues. Would a FileInfo object create a lock?
Joel Cochran
I think you'll have to eliminate potential causes one by one.
Kent Boogaart
I exhausted all I could find. I'll be rebuilding the app next, checking step by step for the culprit.
Joel Cochran
A: 

Thanks Kent,

Works perfectly for me.

Vsaratkar

Vsaratkar
+1  A: 

I marked Kent's response as an answer, and I would have marked bendewey's as well, because I used both of them in the final solution.

The file was definitely locked because the file name was all that was being bound, so the Image control opened the actual file to produce the image.

To Solve this, I created a Value Converter like bendewey suggested, and then I used (most of) the code form Kent's suggestion to return a new BitmapImage:

    [ValueConversion(typeof(string), typeof(BitmapImage))]
public class PathToBitmapImage : IValueConverter
{
    public static BitmapImage ConvertToImage(string path)
    {
        if (!File.Exists(path))
            return null;

        BitmapImage bitmapImage = null;
        try
        {
            bitmapImage = new BitmapImage();
            bitmapImage.BeginInit();
            bitmapImage.StreamSource = new FileStream(path, FileMode.Open, FileAccess.Read);
            bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
            bitmapImage.EndInit();
            bitmapImage.StreamSource.Dispose();
        }
        catch (IOException ioex)
        {
        }
        return bitmapImage;
    }

    #region IValueConverter Members

    public virtual object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (value == null || !(value is string))
            return null;

        var path = value as string;

        return ConvertToImage(path);
    }

    public virtual object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }

    #endregion
}

As the comments suggest above, however, this did not solve the problem. I have been away on other projects and recently returned to this one reinvigorated to find the solution.

I created another project that only tested this code, and of course it worked. This told me that there was more amiss in the original program.

Long story short, the Image was being generated in three places, which I thought had been addressed:

1) The ImageList, now bound using the Converter. 2) The main Image which was bound to the ImageList SelectedItem property. 3) The DeleteImage popup, which was bound using the Converter.

It turns out the problem was in #2. By binding to the SelectedItem, I mistakenly assumed I was binding to the newly rendered Image (based on the Converter). In reality, the SelectedItem object was in fact the file name. This meant that the main Image was again being built by directly accessing the file.

So the solution was to bind the main Image control to the SelectedItem property AND employ the Converter.

Joel Cochran