11

I expect I'll be able to make a work around but I can't for the life of me understand why this code is not functioning correctly and allowing duplicate entries to be added to the List.

The if statement condition is never met, even when I drag identical files in from the same location. I don't understand why the "Contains" method isn't matching them up.

public class Form1:Form {
    private List<FileInfo> dragDropFiles = new List<FileInfo>();

    private void Form1_DragDrop(object sender, DragEventArgs e) {
        try {
            if (e.Data.GetDataPresent(DataFormats.FileDrop)) {
                string[] files =
                    (string[])e.Data.GetData(DataFormats.FileDrop);

                OutputDragDrop(files);
            }
        }
        catch { }
    }

    private void Form1_DragEnter(object sender, DragEventArgs e) {
        if (e.Data.GetDataPresent(DataFormats.FileDrop))
            e.Effect = DragDropEffects.Copy;
        else
            e.Effect = DragDropEffects.None;
    }

    private void OutputDragDrop(string[] files) {
        try {
            foreach (string file in files) {
                FileInfo fileInfo = new FileInfo(file);

                if (dragDropFiles.Contains(fileInfo)) {
                    dragDropFiles.Remove(fileInfo);
                }
                dragDropFiles.Add(fileInfo);
            }
            PopulateContextMenu();
        }
        catch { }
    }
}

I thought I had found another method in which to achieve this using "Distinct"

However, it appears checkedDragDropFiles & dragDropFiles have the same amount of entries, including duplicates, except when dragDropFiles is displayed in a ListBox it doesn't show them. Why does it do this?

I need to prevent any duplicated list entries, as I would be programmatically creating a menu based off of the list data.

private void OutputDragDrop(string[] files)
{
    try
    {
        foreach (string file in files)
        {
            FileInfo fileInfo = new FileInfo(file);

            //if (dragDropFiles.Contains(fileInfo))
            //{
            //    dragDropFiles.Remove(fileInfo);
            //}
            dragDropFiles.Add(fileInfo);
        }

        List<FileInfo> checkedDragDropFiles = dragDropFiles.Distinct().ToList();

        debugList.DataSource = checkedDragDropFiles;
        debugList2.DataSource = dragDropFiles;
        //PopulateContextMenu();
    }
    catch { }
}
Cody Gray - on strike
  • 239,200
  • 50
  • 490
  • 574
negligible
  • 350
  • 1
  • 3
  • 16
  • 2
    what makes `FileInfo`s the same, perhaps you should implement a `IEqualityComparer` to pass to `Distinct` – Jodrell Jan 11 '12 at 12:21
  • Just a note: If `Contains` returns _true_, why remove and add? Do a negative check and only add if the list doesn't _not_ contain the value. – Oded Jan 11 '12 at 13:07
  • Oded: Good point, that is kinda a wasted action. – negligible Jan 11 '12 at 13:41

4 Answers4

18

List<T> does indeed allow duplicates.

In the case of FileInfo, the Contains method will be checking whether the references are the same, but as you are fetching a completely new set of FileInfo, the references are different.

You need to use the overload of Contains that takes an IEqualityComparer - see here.

You can also use HashSet<T> instead - it is a data structure that does not allow duplicates (though with different references, you will still have this issue).

Oded
  • 489,969
  • 99
  • 883
  • 1,009
  • In this case that wouldn't help right? (FileInfo is compared by reference, not value). – Jeff Foster Jan 11 '12 at 12:21
  • 3
    `HashSet` is nice, as it does not throw an exception if the element is already present ... like that! –  Jan 11 '12 at 12:21
  • 1
    @JeffFoster i am shure that you can use a wrapper, enhance this wrapper with `IEquatable`, override `.Equals` and `.GetHashCode` and `HashSet` should work –  Jan 11 '12 at 12:22
  • 1
    @JeffFoster - quite right. Was already updating my answer to that effect. – Oded Jan 11 '12 at 12:25
  • 1
    for performance issue i would not use `List`. why? in each iteration you do a `.Contains` or `.Any` (which lacks hash-based-comparison) - "best" case: each iteration adds an item... why not create a `Dictionary`, where the key is an (invariant & case-ignore) representation of the eg `FullName` property. This will give you a performance boost, as `.ContainsKey` will do a hash-based-check (whilst the `List`-solution lacks this kinda-check). Afterwards you just use the `.Values`-property... –  Jan 11 '12 at 12:37
  • Thanks, I believe this method may be beneficial for me to later as I will need a unique reference to allow the user to remove specific ToolStripMenuItem's that will be generated based on the list. – negligible Jan 11 '12 at 14:05
  • @AndreasNiedermair it is not necessary to use a wrapper; you can supply your own implementation of `IEqualityComparer` that tests based on the full path. – phoog Jan 11 '12 at 22:01
  • @phoog ah, did not think of the other ctor ... thanks! anyway, i wouldn't use `HashSet` nor `List` for this ... –  Jan 12 '12 at 06:40
6

Because the default Object.Equals implementation compares objects by reference, not by value. Each FileInfo instance you create is a different object, as far as .NET is concerned.

You can use LINQ to specify your custom comparison predicate in order to compare objects by different property:

if (dragDropFiles.Any(f => f.Name == file) == false)
{
    dragDropFiles.Add(fileInfo);
}

[Edit]

Since strings are compared by value, you might as well filter the list before you project it to FileInfo, like this:

private void OutputDragDrop(string[] files)
{
    dragDropFiles = files.Distinct().Select(f => new FileInfo(f)).ToList();
    debugList.DataSource = checkedDragDropFiles;
    debugList2.DataSource = dragDropFiles;
}
vgru
  • 49,838
  • 16
  • 120
  • 201
  • long time since i have seen `if (dragDropFiles.Any(f => f.Name == file) == false)` ... :) `if (!dragDropFiles.Any(f => f.Name == file))` –  Jan 11 '12 at 12:24
  • @Andreas: This is the way we did it in the old days. :-D Just kidding. Actually I only did it to emphasize that I inverted OP's logic (adding to list is now done inside the conditional). – vgru Jan 11 '12 at 12:27
  • :) well ... a `!` might easily be missed –  Jan 11 '12 at 12:29
  • Attention: this might work for this special case - but in other secenarios i would rather go for an invariant & case-ignore comparison. This can be easily done by passing `System.StringComparer.CurrentCultureIgnoreCase` or `System.StringComparer.InvariantCultureIgnoreCase` into `.Distinct` –  Jan 11 '12 at 12:39
  • @AndreasNiedermair: AFAIK, two filenames cannot sit in a same folder and differ only by casing, but if there is a potential that file names could get different casing, then I agree, it would be safer. – vgru Jan 11 '12 at 13:27
  • I believe that you can do this on *NIX systems - not shure though (as i am not a pengiun guy ...) ... besides that: that's why i've said "but in other scenarios" –  Jan 11 '12 at 13:28
  • Thanks for the reply but could you explain where the variable "f" comes from? How is it being declared as the right type? – negligible Jan 11 '12 at 14:01
  • Edit: Ok, so on testing I discovered that this solution doesn't work. Although it doesn't display the duplicate items when I output the list to a listbox, it still actually adds the duplicate entry to my list. I took a screenshot to demonstrate this: http://i42.tinypic.com/5znrd.png Program running: http://i42.tinypic.com/xc7rpi.png – negligible Jan 11 '12 at 14:23
  • @negligible: do you clear the list before adding items (`dragDropFiles.Clear()`), if you are repeating this action? I don't see how it could add duplicates if it is initially empty. There could be a possibility that they had different casing, but they seem equal based on your screenshot. If you don't want to clear it, instead of `AddRange`, just change the entire list **(I've updated my answer)**. – vgru Jan 11 '12 at 14:47
  • @Groo: This is dynamically generating a context menu item for each list item. So if I cleared the dragDropFiles every time the user would have to drag them back into the application for them to appear on the menu again. (I have long finished this actual project now) – negligible Feb 03 '12 at 15:34
0

You can easily create multiple FileInfo instances for the same file - so your list will contain every FileInfo only once, but it may have multiple FileInfos for the smae file.

So your best bet might be to use a Hashtable and use FileInfo.FullName as criterion.

Eugen Rieck
  • 64,175
  • 10
  • 70
  • 92
0

If you want an implementation of ICollection<T> that does not allow duplicates, whilst still retaining an ordering, consider using SortedSet<T> rather than List<T>.

Ian Nelson
  • 57,123
  • 20
  • 76
  • 103