0

I am building a ListView to display images to the user.

In the form constructor, I load all the images then the form is displayed. Then an ArgumentException is thrown in the ShowDialog() function:

Message: The parameter is not valid
ParamName: null
InnerException: null
Source: System.Drawing
StackTrace:
System.Drawing.Image.get_Width() System.Drawing.Image.get_Size() System.Windows.Forms.ImageList.CreateBitmap(Original original, Boolean& ownsBitmap) System.Windows.Forms.ImageList.CreateHandle() System.Windows.Forms.ImageList.get_Handle() System.Windows.Forms.ListView.RealizeProperties() System.Windows.Forms.ListView.OnHandleCreated(EventArgs e) System.Windows.Forms.Control.WmCreate(Message& m) System.Windows.Forms.Control.WndProc(Message& m) System.Windows.Forms.ListView.WndProc(Message& m) System.Windows.Forms.NativeWindow.DebuggableCallback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)

I am using a solution from here in order to be able to delete images when the user requires so. If I don't use the solution and don't use the using block, it works fine. However I won't be able to delete images anymore.

Here is my code. Note that the method always execute to the end (I checked this using breakpoints). The exception is thrown only when I call ShowDialog() :

private void LoadImages()
{
    lv_Images.LargeImageList = new ImageList();
    lv_Images.LargeImageList.ImageSize = new Size(64, 64);
    DirectoryInfo di = new DirectoryInfo(initialDirectory);
    foreach (FileInfo file in di.EnumerateFiles())
    {
        if (isImage(file)) //Simply checks the file extension
        {
            using (Image img = Image.FromFile(file.FullName))
            {
                lv_Images.LargeImageList.Images.Add(file.Name, img);
            }
            int index = lv_Images.LargeImageList.Images.IndexOfKey(file.Name);
            lv_Images.Items.Add(file.Name, file.Name, index);
        }
    }
}

What I think is strange though is that it works fine when I call the LoadImages() method from the Shown event of the form.

So I'm not really stuck as I have a workaround but I am curious on why this exception is thrown.

Martin Verjans
  • 4,675
  • 1
  • 21
  • 48
  • Did you try to inspect the exception in detail. Particularly what param is invalid? – Yarl Feb 13 '19 at 09:59
  • @Ucho Yes I checked (updated my question), ParamName is null... – Martin Verjans Feb 13 '19 at 10:04
  • Did you try find out if this relates to particular image? I.e. filtering the files used? – Yarl Feb 13 '19 at 10:10
  • @Ucho Yes I just checked it happens on all my image formats (BMP, JPG, PNG). If there is no image to display the exception is not thrown. – Martin Verjans Feb 13 '19 at 10:16
  • 1
    `lv_Images.LargeImageList.Images.Add(file.Name, (Image)img.Clone());` – Jimi Feb 13 '19 at 10:18
  • 1
    @Jimi that works. Can you explain why ? – Martin Verjans Feb 13 '19 at 10:26
  • @Martin Verjans: Jimi is right. You dispose all images right in place where you create them. Documentation says Dispose() leaves images in unusable state. My bad, sorry. – Yarl Feb 13 '19 at 10:28
  • The `ImageCollection.Add()` method only clones Icon objects, not Image objects and only if you call it as: `ImageCollection.Add(Icon icon)`. Otherwise, the original Image is used. In this case, coming from a stream in a Using block, the Image is already disposed. .Net Source code: [ImageCollection.Add(string Key, Image image)](https://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/ImageList.cs,1234) – Jimi Feb 13 '19 at 10:30
  • @Jimi allright thanks a lot. If you put it as an answer I'll mark it as the solution. – Martin Verjans Feb 13 '19 at 10:34
  • It's a curious kind of *race condition* here. I'll post an answer if I find the right way to explain why the inner `Original` object (you know what that is if you have *inspected* the .Net source) used to first store the Image object passed to the outer (public) `Add()` method, can then pass an invalid Bitmap to the inner `Add()` method, which would then Clone or duplicate (create new) the Bitmap object. The *cloning* or *newing* is subject to conditions, which may or may not be true, depending on the context (the Bitmap type). – Jimi Feb 13 '19 at 11:09
  • @Jimi if it can help, the images are loaded from a network path. – Martin Verjans Feb 13 '19 at 11:33
  • Nope, it's a stream in any case. Since it's a valid source, you'll get a byte array no matter what the source. – Jimi Feb 13 '19 at 11:43

2 Answers2

0

I think that the code being executed in the constructor tries to find the size of the bitmap, as you can see on the top of the stack trace:

System.Drawing.Image.get_Width() System.Drawing.Image.get_Size() 
System.Windows.Forms.ImageList.CreateBitmap(Original original, Boolean& ownsBitmap) 

This is a problem, however, because the form has not yet been shown. That would explain why in the FormShown() it works flawlessly.

Baltasarq
  • 12,014
  • 3
  • 38
  • 57
0

After a few trials, I finally managed to understand what was going on.

Without cloning the Image, it would be disposed and the file handle released, but it would become impossible to display, because the ImageList lost the original source.

According to Jimi's comments, I could use Image.Clone() in my using block and it worked even if I want to delete the image from my code. The original image would be disposed but the cloned one wouldn't.

But then I couldn't delete an image from Windows Explorer, saying that another process was already using this file.

Image.Clone() apparently keeps a handle on the original file and does not store it internally.

So I used a MemoryStream to copy the image file content and my Image is having a handle on the MemoryStream. Now I can delete images as I want, and it's even simpler to write :

private void LoadImages()
{
    lv_Images.LargeImageList = new ImageList();
    lv_Images.LargeImageList.ImageSize = new Size(64, 64);
    DirectoryInfo di = new DirectoryInfo(initialDirectory);
    foreach (FileInfo file in di.EnumerateFiles())
    {
        if (isImage(file)) //Simply checks the file extension
        {
            //File.ReadAllBytes will release the handle when the byte array is constructed.
            lv_Images.LargeImageList.Images.Add(file.Name, Image.FromStream(new MemoryStream(File.ReadAllBytes(file.FullName))));
            int index = lv_Images.LargeImageList.Images.IndexOfKey(file.Name);
            lv_Images.Items.Add(file.Name, file.Name, index);
        }
    }
}

Thanks for your hints anyway.

Martin Verjans
  • 4,675
  • 1
  • 21
  • 48