// out parameters can't be used in anonymous methods, so a separate pointer to backgroundWorker is required for return to the caller
        private async Task<bool> TryBeginFolderLoadAsync(FolderLoad folderLoad)
        {
            List<FileInfo> filesToAdd = folderLoad.GetFiles();
            if (filesToAdd.Count == 0)
            {
                // no images were found in folder; see if user wants to try again
                MessageBox messageBox = new MessageBox("Select a folder containing images or videos?", this, MessageBoxButton.YesNo);
                messageBox.Message.Problem = "There aren't any images or videos in the folder '" + this.FolderPath + "' so your image set is currentl empty.";
                messageBox.Message.Reason = "\u2022 This folder has no images in it (files ending in .jpg)." + Environment.NewLine;
                messageBox.Message.Reason += "\u2022 This folder has no videos in it (files ending in .avi or .mp4).";
                messageBox.Message.Solution = "Choose Yes and select a folder containing images and/or videos or choose No and add files later via the File menu.";
                messageBox.Message.Hint = "\u2022 The files may be in a subfolder of this folder." + Environment.NewLine;
                messageBox.Message.Hint += "\u2022 If you need to set the image set's time zone before adding files choose No." + Environment.NewLine;
                messageBox.Message.StatusImage = MessageBoxImage.Question;
                if (messageBox.ShowDialog() == false)
                {
                    return false;
                }

                IEnumerable<string> folderPaths;
                if (this.ShowFolderSelectionDialog(out folderPaths))
                {
                    folderLoad.FolderPaths.Clear();
                    folderLoad.FolderPaths.AddRange(folderPaths);
                    return await this.TryBeginFolderLoadAsync(folderLoad);
                }

                // exit if user changed their mind about trying again
                return false;
            }

            // update UI for import (visibility is inverse of RunWorkerCompleted)
            this.FeedbackControl.Visibility = Visibility.Visible;
            this.FileNavigatorSlider.Visibility = Visibility.Collapsed;
            IProgress<FolderLoadProgress> folderLoadStatus = new Progress<FolderLoadProgress>(this.UpdateFolderLoadProgress);
            FolderLoadProgress folderLoadProgress = new FolderLoadProgress(filesToAdd.Count, this.MarkableCanvas.Width > 0 ? (int)this.MarkableCanvas.Width : (int)this.Width);
            folderLoadStatus.Report(folderLoadProgress);
            if (this.state.SkipDarkImagesCheck)
            {
                this.statusBar.SetMessage("Loading folders...");
            }
            else
            {
                this.statusBar.SetMessage("Loading folders (if this is slower than you like and dark image detection isn't needed you can select Skip dark check in the Options menu right now)...");
            }
            this.FileViewPane.IsActive = true;

            // Load all files found
            // First pass: Examine files to extract their basic properties and build a list of files not already in the database
            //
            // With dark calculations enabled:
            // Profiling of a 1000 image load on quad core, single 80+MB/s capable SSD shows the following:
            // - one thread:   100% normalized execution time, 35% CPU, 16MB/s disk (100% normalized time = 1 minute 58 seconds)
            // - two threads:   55% normalized execution time, 50% CPU, 17MB/s disk (6.3% normalized time with dark checking skipped)
            // - three threads: 46% normalized execution time, 70% CPU, 20MB/s disk
            // This suggests memory bound operation due to image quality calculation.  The overhead of displaying preview images is fairly low; 
            // normalized time is about 5% with both dark checking and previewing skipped.
            //
            // For now, try to get at least two threads as that captures most of the benefit from parallel operation.  Video loading may be more CPU bound 
            // due to initial frame rendering and benefit from additional threads.  This requires further investigation.  It may also be desirable to reduce 
            // the pixel stride in image quality calculation, which would increase CPU load.
            //
            // With dark calculations disabled:
            // The bottleneck's the SQL insert though using more than four threads (or possibly more threads than the number of physical processors as the 
            // test machine was quad core) results in slow progress on the first 20 files or so, making the optimum number of loading threads complex as it
            // depends on amortizing startup lag across faster progress on the remaining import.  As this is comparatively minor relative to SQL (at least
            // for O(10,000) files for now just default to four threads in the disabled case.
            //
            // Note: the UI thread is free during loading.  So if loading's going slow the user can switch off dark checking asynchronously to speed up 
            // loading.
            //
            // A sequential partitioner is used as this keeps the preview images displayed to the user in pretty much the same order as they're named,
            // which is less confusing than TPL's default partitioning where the displayed image jumps back and forth through the image set.  Pulling files
            // nearly sequentially may also offer some minor disk performance benefit.
            List<ImageRow> filesToInsert = new List<ImageRow>();
            TimeZoneInfo imageSetTimeZone = this.dataHandler.FileDatabase.ImageSet.GetTimeZone();
            await Task.Run(() => Parallel.ForEach(
                new SequentialPartitioner<FileInfo>(filesToAdd),
                Utilities.GetParallelOptions(this.state.SkipDarkImagesCheck ? Environment.ProcessorCount : 2), 
                (FileInfo fileInfo) =>
                {
                    ImageRow file;
                    if (this.dataHandler.FileDatabase.GetOrCreateFile(fileInfo, imageSetTimeZone, out file))
                    {
                        // the database already has an entry for this file so skip it
                        // if needed, a separate list of files to update could be generated
                        return;
                    }

                    BitmapSource bitmapSource = null;
                    try
                    {
                        if (this.state.SkipDarkImagesCheck)
                        {
                            file.ImageQuality = FileSelection.Ok;
                        }
                        else
                        {
                            // load bitmap and determine its quality
                            // Parallel doesn't await async bodies so awaiting the load results in Parallel prematurely concluding the body task completed and
                            // dispatching another, resulting in system overload.  The simplest solution is to block this worker thread, which is OK as there
                            // are many workers and they're decoupled from the UI thread.
                            bitmapSource = file.LoadBitmapAsync(this.FolderPath, folderLoadProgress.RenderWidthBestEstimate).GetAwaiter().GetResult();
                            if (bitmapSource == Constant.Images.CorruptFile.Value)
                            {
                                file.ImageQuality = FileSelection.Corrupt;
                            }
                            else
                            {
                                file.ImageQuality = bitmapSource.AsWriteable().IsDark(this.state.DarkPixelThreshold, this.state.DarkPixelRatioThreshold);
                            }
                        }

                        // see if the datetime can be updated from the metadata
                        file.TryReadDateTimeOriginalFromMetadata(this.FolderPath, imageSetTimeZone);
                    }
                    catch (Exception exception)
                    {
                        Debug.Fail(String.Format("Load of {0} failed as it's likely corrupted.", file.FileName), exception.ToString());
                        bitmapSource = Constant.Images.CorruptFile.Value;
                        file.ImageQuality = FileSelection.Corrupt;
                    }

                    lock (filesToInsert)
                    {
                        filesToInsert.Add(file);
                    }

                    DateTime utcNow = DateTime.UtcNow;
                    if (utcNow - folderLoadProgress.MostRecentStatusDispatch > this.state.Throttles.DesiredIntervalBetweenRenders)
                    {
                        lock (folderLoadProgress)
                        {
                            if (utcNow - folderLoadProgress.MostRecentStatusDispatch > this.state.Throttles.DesiredIntervalBetweenRenders)
                            {
                                // if file was already loaded for dark checking use the resulting bitmap
                                // otherwise, load the file for display
                                if (bitmapSource != null)
                                {
                                    folderLoadProgress.BitmapSource = bitmapSource;
                                }
                                else
                                {
                                    folderLoadProgress.BitmapSource = null;
                                }
                                folderLoadProgress.CurrentFile = file;
                                folderLoadProgress.CurrentFileIndex = filesToInsert.Count;
                                folderLoadProgress.DisplayBitmap = true;
                                folderLoadProgress.MostRecentStatusDispatch = utcNow;
                                folderLoadStatus.Report(folderLoadProgress);
                            }
                        }
                    }
                }));

            // Second pass: Update database
            // Parallel execution above produces out of order results.  Put them back in order so the user sees images in file name order when
            // reviewing the image set.
            folderLoadProgress.DatabaseInsert = true;
            await Task.Run(() =>
                {
                    filesToInsert = filesToInsert.OrderBy(file => Path.Combine(file.RelativePath, file.FileName)).ToList();
                    this.dataHandler.FileDatabase.AddFiles(filesToInsert, (ImageRow file, int fileIndex) =>
                    {
                        // skip reloading images to display as the user's already seen them import
                        folderLoadProgress.BitmapSource = null;
                        folderLoadProgress.CurrentFile = file;
                        folderLoadProgress.CurrentFileIndex = fileIndex;
                        folderLoadProgress.DisplayBitmap = false;
                        folderLoadStatus.Report(folderLoadProgress);
                    });
                });

            // hide the feedback bar, show the file slider
            this.FeedbackControl.Visibility = Visibility.Collapsed;
            this.FileNavigatorSlider.Visibility = Visibility.Visible;

            await this.OnFolderLoadingCompleteAsync(true);

            // tell the user how many files were loaded
            this.MaybeShowFileCountsDialog(true);
            return true;
        }
        /// <summary>
        /// Load the specified database template and then the associated file database.
        /// </summary>
        /// <param name="templateDatabasePath">Fully qualified path to the template database file.</param>
        /// <returns>true only if both the template and image database file are loaded (regardless of whether any images were loaded), false otherwise</returns>
        /// <remarks>This method doesn't particularly need to be public. But making it private imposes substantial complexity in invoking it via PrivateObject
        /// in unit tests.</remarks>
        public async Task<bool> TryOpenTemplateAndBeginLoadFoldersAsync(string templateDatabasePath)
        {
            // Try to create or open the template database
            TemplateDatabase templateDatabase;
            if (TemplateDatabase.TryCreateOrOpen(templateDatabasePath, out templateDatabase) == false)
            {
                // notify the user the template couldn't be loaded
                MessageBox messageBox = new MessageBox("Carnassial could not load the template.", this);
                messageBox.Message.Problem = "Carnassial could not load " + Path.GetFileName(templateDatabasePath) + Environment.NewLine;
                messageBox.Message.Reason = "\u2022 The template was created with the Timelapse template editor instead of the Carnassial editor." + Environment.NewLine;
                messageBox.Message.Reason = "\u2022 The template may be corrupted or somehow otherwise invalid.";
                messageBox.Message.Solution = String.Format("You may have to recreate the template, restore it from the {0} folder, or use another copy of it if you have one.", Constant.File.BackupFolder);
                messageBox.Message.Result = "Carnassial won't do anything.  You can try to select another template file.";
                messageBox.Message.Hint = "If the template can't be opened in a SQLite database editor the file is corrupt.";
                messageBox.Message.StatusImage = MessageBoxImage.Error;
                messageBox.ShowDialog();

                this.state.MostRecentImageSets.TryRemove(templateDatabasePath);
                this.MenuFileRecentImageSets_Refresh();
                return false;
            }

            // Try to get the file database file path
            // importImages will be true if it's a new image database file (meaning the user will be prompted import some images)
            string fileDatabaseFilePath;
            bool addFiles;
            if (this.TrySelectDatabaseFile(templateDatabasePath, out fileDatabaseFilePath, out addFiles) == false)
            {
                // No image database file was selected
                templateDatabase.Dispose();
                return false;
            }

            // Before running from an existing file database, check the controls in the template database are compatible with those
            // of the file database.
            FileDatabase fileDatabase;
            if (FileDatabase.TryCreateOrOpen(fileDatabaseFilePath, templateDatabase, this.state.OrderFilesByDateTime, this.state.CustomSelectionTermCombiningOperator, out fileDatabase) == false)
            {
                // notify the user the database couldn't be loaded
                MessageBox messageBox = new MessageBox("Carnassial could not load the database.", this);
                messageBox.Message.Problem = "Carnassial could not load " + Path.GetFileName(fileDatabaseFilePath) + Environment.NewLine;
                messageBox.Message.Reason = "\u2022 The database was created with Timelapse instead of Carnassial." + Environment.NewLine;
                messageBox.Message.Reason = "\u2022 The database may be corrupted or somehow otherwise invalid.";
                messageBox.Message.Solution = String.Format("You may have to recreate the database, restore it from the {0} folder, or use another copy of it if you have one.", Constant.File.BackupFolder);
                messageBox.Message.Result = "Carnassial won't do anything.  You can try to select another template or database file.";
                messageBox.Message.Hint = "If the database can't be opened in a SQLite database editor the file is corrupt.";
                messageBox.Message.StatusImage = MessageBoxImage.Error;
                messageBox.ShowDialog();
                return false;
            }
            templateDatabase.Dispose();

            if (fileDatabase.ControlSynchronizationIssues.Count > 0)
            {
                TemplateSynchronization templatesNotCompatibleDialog = new TemplateSynchronization(fileDatabase.ControlSynchronizationIssues, this);
                bool? result = templatesNotCompatibleDialog.ShowDialog();
                if (result == true)
                {
                    // user indicated not to update to the current template so exit.
                    Application.Current.Shutdown();
                    return false;
                }
                // user indicated to run with the stale copy of the template found in the image database
            }

            // valid template and file database loaded
            // generate and render the data entry controls regardless of whether there are actually any files in the file database.
            this.dataHandler = new DataEntryHandler(fileDatabase);
            this.DataEntryControls.CreateControls(fileDatabase, this.dataHandler);
            this.SetUserInterfaceCallbacks();

            this.MenuFileRecentImageSets_Refresh();
            this.state.MostRecentFileAddFolderPath = fileDatabase.FolderPath;
            this.state.MostRecentImageSets.SetMostRecent(templateDatabasePath);
            this.Title = Path.GetFileName(fileDatabase.FilePath) + " - " + Constant.MainWindowBaseTitle;

            // If this is a new file database, try to load files (if any) from the folder...  
            if (addFiles)
            {
                FolderLoad folderLoad = new FolderLoad();
                folderLoad.FolderPaths.Add(this.FolderPath);
                await this.TryBeginFolderLoadAsync(folderLoad);
            }

            await this.OnFolderLoadingCompleteAsync(false);
            return true;
        }
 private async void MenuFileAddFilesToImageSet_Click(object sender, RoutedEventArgs e)
 {
     IEnumerable<string> folderPaths;
     if (this.ShowFolderSelectionDialog(out folderPaths))
     {
         FolderLoad folderLoad = new FolderLoad();
         folderLoad.FolderPaths.AddRange(folderPaths);
         await this.TryBeginFolderLoadAsync(folderLoad);
     }
 }