4

I have read afew different posts and I really can't get my head around this!

I have got the following class, when the user has selected a drive and set DriveInfo sel the DoUpload method runs, and converts a load of images to byte format.

Now this takes seconds on my machine, however with more images and slower machines it could take longer, so I want to do this on another thread, but each time an image is converted, I want to notify the UI, in my case I want to update the following Properties: Thumbnails, Filenames, Steps, UploadProgress.

Again, I want to update the above Properties each time an image is converted, how do I do this using the Task API?

Here is the class:

public class UploadInitiation : Common.NotifyUIBase
    {
        #region Public Properties
        /// <summary>
        /// File lists
        /// </summary>
        /// public ObservableCollection<BitmapImage> Thumbnail_Bitmaps { get; set; } = new ObservableCollection<BitmapImage>();
        public ObservableCollection<ByteImage> Thumbnails { get; set; } = new ObservableCollection<ByteImage>();
        public ObservableCollection<NFile> Filenames { get; set; } = new ObservableCollection<NFile>();

        /// <summary>
        /// Updates
        /// </summary>
        public ObservableCollection<UploadStep> Steps { get; set; } = new ObservableCollection<UploadStep>();
        public int UploadProgress { get; set; } = 45;
        public string UploadTask { get; set; } = "Idle...";
        public bool UploadEnabled { get; set; } = false;

        private bool _uploadBegin;
        public bool UploadBegin
        {
            set { _uploadBegin = value; RaisePropertyChanged(); }
            get { return _uploadBegin; }
        }
        #endregion END Public Properties

        public UploadInitiation()
        {
            // Populate steps required, ensure upload returns UI updates
            Steps.Add(new UploadStep { Message = "First task...", Complete = true, Error = null });
            Steps.Add(new UploadStep { Message = "Error testing task...", Complete = false, Error = "testing error" });
            Steps.Add(new UploadStep { Message = "Seperate upload to new thread...", Complete = false, Error = null });
            Steps.Add(new UploadStep { Message = "Generate new file names...", Complete = false, Error = null });
            Steps.Add(new UploadStep { Message = "Render Thumbnails, add to database...", Complete = false, Error = null });
            Steps.Add(new UploadStep { Message = "Move images ready for print...", Complete = false, Error = null });
        }

        /// <summary>
        /// This Method will perform the upload on a seperate thread.
        /// Report progress back by updating public properties of this class.
        /// </summary>
        /// <param name="sel"></param>
        public void DoUpload(DriveInfo sel)
        {
            // Check that there is a device selected
            if (sel != null)
            {
                // Generate List of images to upload
                var files = Directory.EnumerateFiles(sel.Name, "*.*", SearchOption.AllDirectories)
                    .Where(s => s.EndsWith(".jpeg") || s.EndsWith(".jpg") || s.EndsWith(".png"));

                if (files.Count() > 0)
                {
                    // Manage each image
                    foreach (string item in files)
                    {
                        // Generate thumbnail byte array
                        Thumbnails.Add(new ByteImage { Image = GenerateThumbnailBinary(item) });
                    }
                    foreach (string item in files)
                    {
                        // Generate new name
                        Filenames.Add(
                            new NFile
                            {
                                OldName = Path.GetFileNameWithoutExtension(item),
                                NewName = Common.Security.KeyGenerator.GetUniqueKey(32)
                            });
                    }
                }
            }
        }

        public byte[] GenerateThumbnailBinary(string loc)
        {
            BitmapImage image = new BitmapImage(new Uri(loc));

            Stream stream = File.OpenRead(loc);
            byte[] binaryImage = new byte[stream.Length];
            stream.Read(binaryImage,0,(int)stream.Length);

            return binaryImage;
        }
Martyn Ball
  • 4,679
  • 8
  • 56
  • 126
  • You can't update the UI from a Task, are you launching the processing of the images as tasks and then wanting to update the UI after that task completes? Your code doesn't demonstrate any of that. – CodingGorilla Mar 09 '16 at 18:09
  • @CodingGorilla You can do it using some kind of Threading, I have done it before, not sure how to do it using Tasks, I want to update the UI/Property my UI has access to each time I call `Thumbnails.Add()`. – Martyn Ball Mar 09 '16 at 18:15
  • https://msdn.microsoft.com/en-us/magazine/dn605875.aspx – BitTickler Mar 09 '16 at 18:18
  • Change your Thumbnails.Add function to wrap it like this: Application.Current.Dispatcher.BeginInvoke(new Action(() => Thumbnails.Add(...))); This will fire on the main thread's message queue. – Dean Mar 10 '16 at 14:42

2 Answers2

4

There's a built-in class Progress which would report the progress to UI thread.

public async void StartProcessingButton_Click(object sender, EventArgs e)
{
  // The Progress<T> constructor captures our UI context,
  //  so the lambda will be run on the UI thread.
  var progress = new Progress<int>(percent =>
  {
    textBox1.Text = percent + "%";
  });

  // DoProcessing is run on the thread pool.
  await Task.Run(() => DoProcessing(progress));
  textBox1.Text = "Done!";
}

public void DoProcessing(IProgress<int> progress)
{
  for (int i = 0; i != 100; ++i)
  {
    Thread.Sleep(100); // CPU-bound work
    if (progress != null)
      progress.Report(i);
  }
}

Read more about the Progress Reporter here with Async/Await.

Update -

Here's how you can do it in your code to report the progress.

Define the progress object with it's progresschanged handler. Below is a sample with anonymous handler.

  IProgress<UploadStep> progress = new Progress<UploadStep>(step =>
  {
      // sample consumption of progress changed event.
      if (step.Complete)
      {
          // Do what ever you want at the end.
      }
      else
      {
          statusTxtBox.Text = step.Message;
      }
  }); 

Invoke your DoUpload with this object of Progress

 DoUpload(driveInfo, progress);

Now following change in your DoUpload method:

public void DoUpload(DriveInfo sel, IProgress<UploadStep> progress)
{
    // Check that there is a device selected
    if (sel != null)
    {
        progress.Report(new UploadStep { Message = "First Task..." });

        // Generate List of images to upload
        var files = Directory.EnumerateFiles(sel.Name, "*.*", SearchOption.AllDirectories)
            .Where(s => s.EndsWith(".jpeg") || s.EndsWith(".jpg") || s.EndsWith(".png"));

        if (files.Count() > 0)
        {
            // Manage each image
            foreach (string item in files)
            {
                // Generate thumbnail byte array
                Thumbnails.Add(new ByteImage { Image = GenerateThumbnailBinary(item) });
            }

            progress.Report(new UploadStep { Message = "Generated Thumnails..." });

            foreach (string item in files)
            {
                // Generate new name
                Filenames.Add(
                    new NFile
                    {
                        OldName = Path.GetFileNameWithoutExtension(item),
                        NewName = Common.Security.KeyGenerator.GetUniqueKey(32)
                    });
            }

            progress.Report(new UploadStep { Message = "Uplaoding to database..." });
        }
    }
}
vendettamit
  • 14,315
  • 2
  • 32
  • 54
  • https://blogs.msdn.microsoft.com/dotnet/2012/06/06/async-in-4-5-enabling-progress-and-cancellation-in-async-apis/ – BitTickler Mar 09 '16 at 19:07
3

I would create a separate Thread and pass the list and the Dispatcher to it. Every time an item is ready, you should Invoke the Dispatcher and set the results.

var list<UploadStep> items = new List<UploadStep>();

// queue workitems
var currentDispatcher = Dispatcher.CurrentDispatcher;
var thread = new Thread(() =>
    {
        foreach(var item in items)
        {
            // do you thing with downloading..

            currentDispatcher.Invoke(() =>
            {
                // Add to observable class or other gui items..
            });
        }
    });

thread.Start(); 

If you want to do it with the Task libary, you should await each item.

var list<UploadStep> items = new List<UploadStep>();

foreach(var step in items)
{
    var myResults = await DoYourDownload(step);
    // Change some gui stuff.
}

All these items will be processed in serial.

Here is an article on how to process them parallel: Starting Tasks In foreach Loop Uses Value of Last Item

Community
  • 1
  • 1
Jeroen van Langen
  • 21,446
  • 3
  • 42
  • 57
  • I think this is a bit too low level. There might be other approaches, too. – BitTickler Mar 09 '16 at 18:34
  • One other way which is nearly as low level is to queue user work items to a thread pool. This way, multiple images can be processed in parallel. – BitTickler Mar 09 '16 at 18:36
  • @BitTickler , so I could pass the convert task to the treadpool and then receive the result when each one finishes? So potentially 2 images could convert at the same time on different threads and return and update the Properties at the same time? – Martyn Ball Mar 09 '16 at 18:52
  • 1
    @MartynBall Thats the idea. In your wait callback handler, you could do what this answer here shows. Use the dispatcher and manipulate your UI. – BitTickler Mar 09 '16 at 19:00
  • 1
    @BitTickler. Using the threadpool is useful. But creating a reference to the dispatcher and invoke on it, will still be the case. One problem is, you must create something to check when all items are processed. A solution would be using `Parallel.ForEach();` but this will block the current thread. So when processing multithreaded. `Interlocked.Decrease (ref counter);` will be the key. – Jeroen van Langen Mar 09 '16 at 19:10
  • @BitTickler I agree, just pass a method to a `ThreadPool` and invoke dispatcher in each `foreach` loop, this would be so much easier. and very little change to the code. – XAMlMAX Mar 09 '16 at 22:06