3

In the question I asked Retrieve a list of filenames in folder and all subfolders quickly And a few others I have found, it seems the way to search many files is to use the EnumerateFiles Method.

The EnumerateFiles and GetFiles methods differ as follows: When you use EnumerateFiles, you can start enumerating the collection of names before the whole collection is returned; when you use GetFiles, you must wait for the whole array of names to be returned before you can access the array. Therefore, when you are working with many files and directories, EnumerateFiles can be more efficient.

This sounds Great for me, my search is taking about 10 seconds, so I can start makign my list as the information comes in. But I can't figure it out. When I run the EnumerateFiles Method, the application freezes until it completes. I could run it in a background worker, but the same thing will happen to that thread. Any help?

 DirectoryInfo dir = new DirectoryInfo(MainFolder);
 List<FileInfo> matches = new List<FileInfo>(dir.EnumerateFiles("*.docx",SearchOption.AllDirectories));

//This wont fire until after the entire collection is complete
DoSoemthingWhileWaiting();
Community
  • 1
  • 1
General Grey
  • 3,598
  • 2
  • 25
  • 32

3 Answers3

12

You can do this by pushing it into a background task.

For example, you could do:

var fileTask = Task.Factory.StartNew( () =>
{
    DirectoryInfo dir = new DirectoryInfo(MainFolder);
    return new List<FileInfo>(
           dir.EnumerateFiles("*.docx",SearchOption.AllDirectories)
           .Take(200) // In previous question, you mentioned only wanting 200 items
       );
};

// To process items:
fileTask.ContinueWith( t =>
{
     List<FileInfo> files = t.Result;

     // Use the results...
     foreach(var file in files)
     {
         this.listBox.Add(file); // Whatever you want here...
     }
}, TaskScheduler.FromCurrentSynchronizationContext()); // Make sure this runs on the UI thread

DoSomethingWhileWaiting();

You mentioned in a comment:

I want to display them in a list. and perfect send them to the main ui as they come in

In this case, you'd have to process them in the background, and add them to the list as they come in. Something like:

Task.Factory.StartNew( () =>
{
    DirectoryInfo dir = new DirectoryInfo(MainFolder);
    foreach(var tmp in dir.EnumerateFiles("*.docx",SearchOption.AllDirectories).Take(200))
    {
        string file = tmp; // Handle closure issue

        // You may want to do this in batches of >1 item...
        this.BeginInvoke( new Action(() =>
        {
             this.listBox.Add(file);
        }));
    }
});
DoSomethingWhileWaiting();
Reed Copsey
  • 554,122
  • 78
  • 1,158
  • 1,373
  • Thanks for coming in late Reed. I could have used this 5 minutes ago. This is pretty much exactly what I wanted, but I already figured it out from my past question and the answers above. Greatly appreciated you taking the time for such a complete answer. – General Grey May 15 '12 at 17:30
  • 1
    @K'Leg Faster != Better ;) Figured I'd chime in with everything you probably need, including more than one option here... – Reed Copsey May 15 '12 at 17:31
  • Reed it seems we are missing a closing bracket in your second answer. I tried adding it to the end but I am getting errors. Anyidea? – General Grey May 15 '12 at 17:40
  • @K'Leg There was a parenthesis missing - added it. Should be }); at the end of StartNew – Reed Copsey May 15 '12 at 17:41
  • Thanks I just figured it out. I was puting it behind the };) while it should have been in the middle }); – General Grey May 15 '12 at 17:42
  • I was having problems with this code still and someone suggested changing BeginInvoke to Invoke, and it solved my problem. Might want to look into that – General Grey May 15 '12 at 18:36
  • @K'Leg It depends - Invoke blocks, BeginInvoke doesn't - depending on the situation, the first may be more appropriate. – Reed Copsey May 15 '12 at 18:43
  • Ok then I needed Invoke in my case, I was getting the results all jumbled. I would get 4 results and while they shouldhave been unique they were the same – General Grey May 15 '12 at 19:06
  • @K'Leg Ahh - that's the closure - need a temporary - I'll fix. (BeginInvoke with the temporary will fix + probably give you better throughput) – Reed Copsey May 15 '12 at 19:33
7

The application freezes because you are consuming this enumerable by putting it in the constructor of the List<FileInfo>. It's as if you was calling eagerly the old method. So the proper way to do this is to run it inside a background thread and then do not immediately pass the result to something that will consume the enumerable till the end but start iterating and add items as they arrive in the loop.

I could run it in a background worker, but the same thing will happen to that thread.

Yes, during the loop you will obviously freeze the background thread but that's what background threads are meant for: to avoid freezing the main UI thread. Just make sure that when you are passing the items to the main thread to show them you are using proper synchronization with the UI. In WinForms this happens with the Control.Invoke method. Of course be careful because marshalling quite often between the background thread and the UI thread could also have negative impact give you the feeling that your application freezes. To workaround this you could pass the files as they arrive in chunks.

Here's an example:

Task.Factory.StartNew(() =>
{
    var dir = new DirectoryInfo(MainFolder);
    var files = dir.EnumerateFiles("*.docx", SearchOption.AllDirectories);
    foreach (var file in files)
    {
        Action<string> del = f => listBox1.Items.Add((string)f);
        BeginInvoke(del, file);
    }
});
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • So if I can't turn it into a list how to I store and access it? I am sorry but I am not getting this yet? – General Grey May 15 '12 at 17:19
  • What do you want to do with those results? Show them somewhere in the UI? In a ListBox or something? You should not store them in a List. You should marshal the results directly to the main UI thread for displaying as they arrive. So it will really depend on what you need to do with the results. – Darin Dimitrov May 15 '12 at 17:21
  • Yes I want to display them in a list. and perfect send them to the main ui as they come in. What I am missing is, if the background worker freezes while processing this, how can I send them to the main ui? – General Grey May 15 '12 at 17:23
  • In chunks, using the Control.Invoke method. Or you could store them in a List and once the enumeration has finished you could use Control.Invoke to bind this list to the main UI control. But in this case items won't appear as they arrive. – Darin Dimitrov May 15 '12 at 17:24
  • Thanks Darin The foreach in Gabba's answer I think pushed me to understand. I appreciate your help though – General Grey May 15 '12 at 17:28
  • I've updated my answer to show you an example of how you could launch a new thread to process the searching and then show the files as they arrive in a ListBox without freezing the UI thread. – Darin Dimitrov May 15 '12 at 17:29
4

you can do

    forach(var f in dir.EnumerateFiles("*.docx",SearchOption.AllDirectories))
    {
//.. process file
    }

in thread, and fire an event after process complete

gabba
  • 2,815
  • 2
  • 27
  • 48
  • so if I run this right away, then run it again 5 seconds later, their might be different results? – General Grey May 15 '12 at 17:17
  • I mean first file will come before all enumeration complete. If you want to process file system changing you can use filesystemwatcher: http://msdn.microsoft.com/en-us/library/system.io.filesystemwatcher.aspx – gabba May 15 '12 at 17:23
  • oh wait I think I see. if i run this everytime it finds a result it will fire another iteration of the foreach... inside the foreach I can send an update from the background worker to the ui... is that correct? – General Grey May 15 '12 at 17:24
  • Yes, but don't forget to synchronize with UI – gabba May 15 '12 at 17:25
  • The second answer of Reed Copsey looks perfect – gabba May 15 '12 at 17:28
  • @K'Leg BTW - Note the ".Take(...)" in my answer - from your previous question, I think you'll want that, too ;) – Reed Copsey May 15 '12 at 17:30