/// <summary> /// If there are missing folders, search for possible matches and raise a dialog box asking the user to locate them /// </summary> /// <param name="owner"></param> /// <param name="fileDatabase"></param> /// <param name="missingFolders"></param> /// <returns>whether any folder are actually missing </returns> private static bool?CorrectForMissingFolders(Window owner, FileDatabase fileDatabase, List <string> missingRelativePaths) { // Abort if the arguments for null if (null == fileDatabase) { return(null); } if (null == missingRelativePaths) { return(null); } // We know that at least one or more folders are missing. // For each missing folder path, try to find all folders with the same name under the root folder. Dictionary <string, List <string> > matchingFolderNames = Util.FilesFolders.TryGetMissingFolders(fileDatabase.FolderPath, missingRelativePaths); Dictionary <string, string> finalFileLocations; // We want to show the normal cursor when we display dialog boxes, so save the current cursor so we can store it. Cursor cursor = Mouse.OverrideCursor; bool?result; if (matchingFolderNames != null) { Mouse.OverrideCursor = null; // Present a dialog box that asks the user to locate the missing folders. It will show possible locations for each folder (if any). // The user can then confirm correct locations, manually set the locaton of those folders, or cancel altogether. MissingFoldersLocateAllFolders dialog = new MissingFoldersLocateAllFolders(owner, fileDatabase.FolderPath, matchingFolderNames); result = dialog.ShowDialog(); if (result == true) { // Get the updated folder locations and update the database finalFileLocations = dialog.FinalFolderLocations; foreach (string key in finalFileLocations.Keys) { ColumnTuple columnToUpdate = new ColumnTuple(Constant.DatabaseColumn.RelativePath, finalFileLocations[key]); ColumnTuplesWithWhere columnToUpdateWithWhere = new ColumnTuplesWithWhere(columnToUpdate, key); fileDatabase.UpdateFiles(columnToUpdateWithWhere); } Mouse.OverrideCursor = cursor; return(true); } } Mouse.OverrideCursor = cursor; return(null); // Operaton aborted }
//Try to find a missing image private async void MenuItemEditFindMissingImage_Click(object sender, RoutedEventArgs e) { // Don't do anything if the image actually exists. This should not fire, as this menu item is only enabled // if there is a current image that doesn't exist. But just in case... if (null == this.DataHandler?.ImageCache?.Current || File.Exists(FilesFolders.GetFullPath(this.DataHandler.FileDatabase.FolderPath, this.DataHandler?.ImageCache?.Current))) { return; } string folderPath = this.DataHandler.FileDatabase.FolderPath; ImageRow currentImage = this.DataHandler?.ImageCache?.Current; // Search for - and return as list of relative path / filename tuples - all folders under the root folder for files with the same name as the fileName. List <Tuple <string, string> > matchingRelativePathFileNameList = Util.FilesFolders.SearchForFoldersContainingFileName(folderPath, currentImage.File); // Remove any of the tuples that are spoken for i.e., that are already associated with a row in the database for (int i = matchingRelativePathFileNameList.Count - 1; i >= 0; i--) { if (this.DataHandler.FileDatabase.ExistsRelativePathAndFileInDataTable(matchingRelativePathFileNameList[i].Item1, matchingRelativePathFileNameList[i].Item2)) { // We only want matching files that are not already assigned to another datafield in the database matchingRelativePathFileNameList.RemoveAt(i); } } // If there are no remaining tuples, it means no potential matches were found. // Display a message saying so and abort. if (matchingRelativePathFileNameList.Count == 0) { Dialogs.MissingFileSearchNoMatchesFoundDialog(this, currentImage.File); return; } // Now retrieve a list of all filenames located in the same folder (i.e., that have the same relative path) as the missing file. List <string> otherMissingFiles = this.DataHandler.FileDatabase.SelectFileNamesWithRelativePathFromDatabase(currentImage.RelativePath); // Remove the current missing file from the list, as well as any file that exists i.e., that is not missing. for (int i = otherMissingFiles.Count - 1; i >= 0; i--) { if (String.Equals(otherMissingFiles[i], currentImage.File) || File.Exists(Path.Combine(folderPath, currentImage.RelativePath, otherMissingFiles[i]))) { otherMissingFiles.RemoveAt(i); } } // For those that are left (if any), see if other files in the returned locations are in each path. Get their count, save them, and pass the count as a parameter e.g., a Dict with matching files, etc. // Or perhapse even better, a list of file names for each path Dict<string, List<string>> // As we did above, go through the other missing files and remove those that are spoken for i.e., that are already associated with a row in the database. // What remains will be a list of root paths, each with a list of missing (unassociated) files that could be candidates for locating Dictionary <string, List <string> > otherMissingFileCandidates = new Dictionary <string, List <string> >(); foreach (Tuple <string, string> matchingPath in matchingRelativePathFileNameList) { List <string> orphanMissingFiles = new List <string>(); foreach (string otherMissingFile in otherMissingFiles) { // Its a potential candidate if its not already referenced but it exists in that relative path folder if (false == this.DataHandler.FileDatabase.ExistsRelativePathAndFileInDataTable(matchingPath.Item1, otherMissingFile) && File.Exists(FilesFolders.GetFullPath(FolderPath, matchingPath.Item1, otherMissingFile))) { orphanMissingFiles.Add(otherMissingFile); } } otherMissingFileCandidates.Add(matchingPath.Item1, orphanMissingFiles); } Dialog.MissingImageLocateRelativePaths dialog = new Dialog.MissingImageLocateRelativePaths(this, this.DataHandler.FileDatabase, currentImage.RelativePath, currentImage.File, otherMissingFileCandidates); bool?result = dialog.ShowDialog(); if (result == true) { Tuple <string, string> locatedMissingFile = dialog.LocatedMissingFile; if (String.IsNullOrEmpty(locatedMissingFile.Item2)) { return; } // Update the original missing file List <ColumnTuplesWithWhere> columnTuplesWithWhereList = new List <ColumnTuplesWithWhere>(); ColumnTuplesWithWhere columnTuplesWithWhere = new ColumnTuplesWithWhere(); columnTuplesWithWhere.Columns.Add(new ColumnTuple(Constant.DatabaseColumn.RelativePath, locatedMissingFile.Item1)); // The new relative path columnTuplesWithWhere.SetWhere(currentImage.RelativePath, currentImage.File); // Where the original relative path/file terms are met columnTuplesWithWhereList.Add(columnTuplesWithWhere); // Update the other missing files in the database, if any foreach (string otherMissingFileName in otherMissingFileCandidates[locatedMissingFile.Item1]) { columnTuplesWithWhere = new ColumnTuplesWithWhere(); columnTuplesWithWhere.Columns.Add(new ColumnTuple(Constant.DatabaseColumn.RelativePath, locatedMissingFile.Item1)); // The new value columnTuplesWithWhere.SetWhere(currentImage.RelativePath, otherMissingFileName); // Where the original relative path/file terms are met columnTuplesWithWhereList.Add(columnTuplesWithWhere); } // Now update the database this.DataHandler.FileDatabase.UpdateFiles(columnTuplesWithWhereList); await this.FilesSelectAndShowAsync().ConfigureAwait(true); } }
// Populate the database with the metadata for the selected note field private async Task <ObservableCollection <Tuple <string, string, string> > > PopulateAsync(MetadataToolEnum metadataToolSelected) { // This list will hold key / value pairs that will be bound to the datagrid feedback, // which is the way to make those pairs appear in the data grid during background worker progress updates ObservableCollection <Tuple <string, string, string> > feedbackData = new ObservableCollection <Tuple <string, string, string> >(); // if there are no metadata / label pairs, we are done. if (this.MetadataGrid.SelectedMetadata.Count == 0) { // Catch the case where there are no selected pairs, at least for now. feedbackData.Clear(); feedbackData.Add(new Tuple <string, string, string>("Nothing was selected", "", "No changes were made")); return(feedbackData); } return(await Task.Run(() => { // For each row in the database, get the image filename and try to extract the chosen metadata value. // If we can't decide if we want to leave the data field alone or to clear it depending on the state of the isClearIfNoMetadata (set via the checkbox) // Report progress as needed. // This tuple list will hold the id, key and value that we will want to update in the database List <ColumnTuplesWithWhere> imagesToUpdate = new List <ColumnTuplesWithWhere>(); TimeZoneInfo imageSetTimeZone = DateTimeHandler.GetNeutralTimeZone(); int percentDone = 0; double totalImages = this.FileDatabase.CountAllCurrentlySelectedFiles; Dictionary <string, ImageMetadata> metadata = new Dictionary <string, ImageMetadata>(); string[] tags = this.MetadataGrid.SelectedTags;// Only needed by ExifTool, but cheap to get for (int imageIndex = 0; imageIndex < totalImages; ++imageIndex) { // Provide feedback if the operation was cancelled during the database update if (Token.IsCancellationRequested == true) { feedbackData.Clear(); feedbackData.Add(new Tuple <string, string, string>("Cancelled", "", "No changes were made")); return feedbackData; } ImageRow image = this.FileDatabase.FileTable[imageIndex]; if (metadataToolSelected == MetadataToolEnum.MetadataExtractor) { // MetadataExtractor specific code metadata = ImageMetadataDictionary.LoadMetadata(image.GetFilePath(this.FileDatabase.FolderPath)); } else // if metadataToolSelected == MetadataToolEnum.ExifTool { // ExifTool specific code - note that we transform results into the same dictionary structure used by the MetadataExtractor // Unlike MetadataExtractor, ExifTool returns TagName instad of Directory.TagName (I think - but does that mean it would break on duplicate values? metadata.Clear(); Dictionary <string, string> exifData = this.MetadataGrid.ExifToolManager.FetchExifFrom(image.GetFilePath(this.FileDatabase.FolderPath), tags); foreach (string tag in tags) { if (exifData.ContainsKey(tag)) { metadata.Add(tag, new Timelapse.Util.ImageMetadata(String.Empty, tag, exifData[tag])); } } } // At this point, the metadata Key should be the tag name, rather than Directory.TagName // (see ImageMetadataDiction.LoadDictionary to change it back so the key is the directory.name. I think Exif never returns the directory name, so thats ok too. if (this.ReadyToRefresh()) { percentDone = Convert.ToInt32(imageIndex / totalImages * 100.0); this.Progress.Report(new ProgressBarArguments(percentDone, String.Format("{0}/{1} images. Processing {2}", imageIndex, totalImages, image.File), true, false)); Thread.Sleep(Constant.ThrottleValues.RenderingBackoffTime); // Allows the UI thread to update every now and then } string dataLabelToUpdate = ""; foreach (KeyValuePair <string, string> kvp in this.MetadataGrid.SelectedMetadata) { string metadataTag = kvp.Key; dataLabelToUpdate = kvp.Value; if (false == metadata.ContainsKey(metadataTag)) { // This just skips this metadata as it was not found in the file's metadata // However, we still need to supply feedback and (if the user has asked for that option) to clear the data field if (this.clearIfNoMetadata) { List <ColumnTuple> clearField = new List <ColumnTuple>() { new ColumnTuple(dataLabelToUpdate, String.Empty) }; imagesToUpdate.Add(new ColumnTuplesWithWhere(clearField, image.ID)); feedbackData.Add(new Tuple <string, string, string>(image.File, metadataTag, "No metadata found - data field cleared")); } else { feedbackData.Add(new Tuple <string, string, string>(image.File, metadataTag, "No metadata found - data field unchanged")); } continue; } string metadataValue = metadata[metadataTag].Value; ColumnTuplesWithWhere imageUpdate; if (this.useDateMetadataOnly) { if (DateTimeHandler.TryParseMetadataDateTaken(metadataValue, imageSetTimeZone, out DateTimeOffset metadataDateTime)) { image.SetDateTimeOffset(metadataDateTime); imageUpdate = image.GetDateTimeColumnTuples(); feedbackData.Add(new Tuple <string, string, string>(image.File, metadataTag, metadataValue)); } else { feedbackData.Add(new Tuple <string, string, string>(image.File, metadataTag, String.Format("Data field unchanged - '{0}' is not a valid date/time.", metadataValue))); continue; } } else { imageUpdate = new ColumnTuplesWithWhere(new List <ColumnTuple>() { new ColumnTuple(dataLabelToUpdate, metadataValue) }, image.ID); feedbackData.Add(new Tuple <string, string, string>(image.File, metadataTag, metadataValue)); } imagesToUpdate.Add(imageUpdate); } } this.isAnyDataUpdated = true; this.Progress.Report(new ProgressBarArguments(100, String.Format("Writing metadata for {0} files. Please wait...", totalImages), false, true)); Thread.Sleep(Constant.ThrottleValues.RenderingBackoffTime); // Allows the UI thread to update every now and then this.FileDatabase.UpdateFiles(imagesToUpdate); return feedbackData; }, this.Token).ConfigureAwait(true)); }
// Reads all the data from the old ImageData.xml files into the imageData structure from the XML file in the filepath. // Note that we need to know the code controls,as we have to associate any points read in with a particular counter control public static Tuple <int, int> Read(string filePath, FileDatabase imageDatabase) { // XML Preparation - follows CA3075 pattern for loading XmlDocument xmlDoc = new XmlDocument() { XmlResolver = null }; System.IO.StringReader sreader = new System.IO.StringReader(File.ReadAllText(filePath)); XmlReader reader = XmlReader.Create(sreader, new XmlReaderSettings() { XmlResolver = null }); xmlDoc.Load(reader); // Import the old log (if any) XmlNodeList logNodes = xmlDoc.SelectNodes(Constant.ImageXml.Images + Constant.ImageXml.Slash + Constant.DatabaseColumn.Log); if (logNodes.Count > 0) { XmlNode logNode = logNodes[0]; imageDatabase.ImageSet.Log = logNode.InnerText; imageDatabase.UpdateSyncImageSetToDatabase(); } // Create three lists, each one representing the datalabels (in order found in the template) of notes, counters and choices // We will use these to find the matching ones in the xml data table. List <string> noteControlNames = new List <string>(); List <string> counterControlNames = new List <string>(); List <string> choiceControlNames = new List <string>(); foreach (ControlRow control in imageDatabase.Controls) { // Note that code should be modified to deal with flag controls switch (control.Type) { case Constant.Control.Counter: counterControlNames.Add(control.DataLabel); break; case Constant.Control.FixedChoice: choiceControlNames.Add(control.DataLabel); break; case Constant.Control.Note: noteControlNames.Add(control.DataLabel); break; default: break; } } XmlNodeList nodeList = xmlDoc.SelectNodes(Constant.ImageXml.Images + Constant.ImageXml.Slash + Constant.DatabaseColumn.Image); int imageID = 0; TimeZoneInfo imageSetTimeZone = imageDatabase.ImageSet.GetSystemTimeZone(); List <ColumnTuplesWithWhere> imagesToUpdate = new List <ColumnTuplesWithWhere>(); List <ColumnTuplesWithWhere> markersToUpdate = new List <ColumnTuplesWithWhere>(); List <Tuple <string, List <ColumnTuple> > > fileNamesMarkersList = new List <Tuple <string, List <ColumnTuple> > >(); int successCounter = 0; int skippedCounter = 0; foreach (XmlNode node in nodeList) { imageID++; // We ignore: // - Folder and Relative path, as the new template will have the correct values // - ImageQuality, as the new Timelapse version probably has a better determination of it // - DeleteFlag, as the old-style xml templates didn't have them // - Flags as the old-style xml templates didn't have them List <ColumnTuple> columnsToUpdate = new List <ColumnTuple>(); // Populate the data // File Field - We use the file name as a key into a particular database row. We don't change the database field as it is our key. string imageFileName = node[Constant.ImageXml.File].InnerText; // If the Folder Path differs from where we had previously loaded it, // warn the user that the new path will be substituted in its place // This gets the folderName in the Xml file, but we still ahve to get the folderName as it currently exists. // string folderName = node[Constant.ImageXml.Folder].InnerText; // Date - We use the original date, as the analyst may have adjusted them string date = node[Constant.ImageXml.Date].InnerText; columnsToUpdate.Add(new ColumnTuple(Constant.DatabaseColumn.Date, date)); // Date - We use the original time, although its almost certainly identical string time = node[Constant.ImageXml.Time].InnerText; columnsToUpdate.Add(new ColumnTuple(Constant.DatabaseColumn.Time, time)); // DateTime if (DateTimeHandler.TryParseLegacyDateTime(date, time, imageSetTimeZone, out DateTimeOffset dateTime)) { columnsToUpdate.Add(new ColumnTuple(Constant.DatabaseColumn.DateTime, dateTime.UtcDateTime)); columnsToUpdate.Add(new ColumnTuple(Constant.DatabaseColumn.UtcOffset, dateTime.Offset)); } // Notes: Iterate through int innerNodeIndex = 0; XmlNodeList innerNodeList = node.SelectNodes(Constant.Control.Note); foreach (XmlNode innerNode in innerNodeList) { // System.Diagnostics.Debug.Print("Note: " + noteControlNames[innerNodeIndex] + " | " + innerNode.InnerText); columnsToUpdate.Add(new ColumnTuple(noteControlNames[innerNodeIndex++], innerNode.InnerText)); } // Choices: Iterate through innerNodeIndex = 0; innerNodeList = node.SelectNodes(Constant.Control.FixedChoice); foreach (XmlNode innerNode in innerNodeList) { // System.Diagnostics.Debug.Print("Choice: " + choiceControlNames[innerNodeIndex] + " | " + innerNode.InnerText); columnsToUpdate.Add(new ColumnTuple(choiceControlNames[innerNodeIndex++], innerNode.InnerText)); } // Counters: Iterate through List <ColumnTuple> counterCoordinates = new List <ColumnTuple>(); innerNodeIndex = 0; innerNodeList = node.SelectNodes(Constant.Control.Counter); foreach (XmlNode innerNode in innerNodeList) { // Add the value of each counter to the dataline XmlNodeList dataNode = innerNode.SelectNodes(Constant.DatabaseColumn.Data); // System.Diagnostics.Debug.Print("Counter: " + counterControlNames[innerNodeIndex] + " | " + dataNode[0].InnerText); columnsToUpdate.Add(new ColumnTuple(counterControlNames[innerNodeIndex], dataNode[0].InnerText)); // For each counter, find the points associated with it and compose them together as x1,y1|x2,y2|...|xn,yn XmlNodeList pointNodeList = innerNode.SelectNodes(Constant.DatabaseColumn.Point); string countercoord = String.Empty; foreach (XmlNode pointNode in pointNodeList) { String x = pointNode.SelectSingleNode(Constant.DatabaseColumn.X).InnerText; if (x.Length > 5) { x = x.Substring(0, 5); } String y = pointNode.SelectSingleNode(Constant.DatabaseColumn.Y).InnerText; if (y.Length > 5) { y = y.Substring(0, 5); } countercoord += x + "," + y + "|"; } // Remove the last "|" from the point list if (!String.IsNullOrEmpty(countercoord)) { countercoord = countercoord.Remove(countercoord.Length - 1); // Remove the last "|" } // Countercoords will have a list of points (possibly empty) with each list entry representing a control counterCoordinates.Add(new ColumnTuple(counterControlNames[innerNodeIndex], countercoord)); innerNodeIndex++; } // add this image's updates to the update lists ColumnTuplesWithWhere imageToUpdate = new ColumnTuplesWithWhere(columnsToUpdate); // Since Timelapse1 didn't have relative paths, we only need to set Where using the image filename // imageToUpdate.SetWhere(currentFolderName, null, imageFileName); //<- replaced by the simpler SetWhere form below if (File.Exists(Path.Combine(Path.GetDirectoryName(filePath), imageFileName))) { imageToUpdate.SetWhere(imageFileName); imagesToUpdate.Add(imageToUpdate); ColumnTuple ColumnTupleFileName = new ColumnTuple(Constant.DatabaseColumn.File, imageFileName); // We have to do the markers later, as we need to get the ID of the matching filename from the data table, // and use that to set the markers. Tuple <string, List <ColumnTuple> > filenameMarkerTuple = new Tuple <string, List <ColumnTuple> >(imageFileName, counterCoordinates); fileNamesMarkersList.Add(filenameMarkerTuple); successCounter++; } else { skippedCounter++; } } // batch update the data table imageDatabase.UpdateFiles(imagesToUpdate); // Now that we have updated the data table, we can update the markers. // We retrieve the ID of the filename associated with the markers from the data table, // and use that to set the correct row in the marker table. foreach (Tuple <string, List <ColumnTuple> > tuple in fileNamesMarkersList) { long id = imageDatabase.GetIDFromDataTableByFileName(tuple.Item1); ColumnTuplesWithWhere markerToUpdate = new ColumnTuplesWithWhere(tuple.Item2, id); markersToUpdate.Add(markerToUpdate); imageDatabase.UpdateMarkers(markersToUpdate); } if (reader != null) { reader.Dispose(); } return(new Tuple <int, int>(successCounter, skippedCounter)); }
// Populate the database with the metadata for the selected note field private async Task <ObservableCollection <KeyValuePair <string, string> > > PopulateAsync(bool?metadataExtractorRBIsChecked) { // This list will hold key / value pairs that will be bound to the datagrid feedback, // which is the way to make those pairs appear in the data grid during background worker progress updates ObservableCollection <KeyValuePair <string, string> > keyValueList = new ObservableCollection <KeyValuePair <string, string> >(); return(await Task.Run(() => { // For each row in the database, get the image filename and try to extract the chosen metadata value. // If we can't decide if we want to leave the data field alone or to clear it depending on the state of the isClearIfNoMetadata (set via the checkbox) // Report progress as needed. // This tuple list will hold the id, key and value that we will want to update in the database string dataLabelToUpdate = this.dataLabelByLabel[this.dataFieldLabel]; List <ColumnTuplesWithWhere> imagesToUpdate = new List <ColumnTuplesWithWhere>(); TimeZoneInfo imageSetTimeZone = this.fileDatabase.ImageSet.GetSystemTimeZone(); int percentDone = 0; double totalImages = this.fileDatabase.CountAllCurrentlySelectedFiles; Dictionary <string, ImageMetadata> metadata = new Dictionary <string, ImageMetadata>(); for (int imageIndex = 0; imageIndex < totalImages; ++imageIndex) { // Provide feedback if the operation was cancelled during the database update if (Token.IsCancellationRequested == true) { keyValueList.Clear(); keyValueList.Add(new KeyValuePair <string, string>("Cancelled", "No changes were made")); return keyValueList; } ImageRow image = this.fileDatabase.FileTable[imageIndex]; if (metadataExtractorRBIsChecked == true) { // MetadataExtractor specific code metadata = ImageMetadataDictionary.LoadMetadata(image.GetFilePath(this.fileDatabase.FolderPath)); } else { // ExifTool specific code - note that we transform results into the same dictionary structure used by the MetadataExtractor string[] tags = { this.metadataFieldName }; metadata.Clear(); Dictionary <string, string> exifData = this.exifTool.FetchExifFrom(image.GetFilePath(this.fileDatabase.FolderPath), tags); if (exifData.ContainsKey(tags[0])) { metadata.Add(tags[0], new Timelapse.Util.ImageMetadata(String.Empty, tags[0], exifData[tags[0]])); } } if (this.ReadyToRefresh()) { percentDone = Convert.ToInt32(imageIndex / totalImages * 100.0); this.Progress.Report(new ProgressBarArguments(percentDone, String.Format("{0}/{1} images. Processing {2}", imageIndex, totalImages, image.File), true, false)); Thread.Sleep(Constant.ThrottleValues.RenderingBackoffTime); // Allows the UI thread to update every now and then } if (metadata.ContainsKey(this.metadataFieldName) == false) { if (this.clearIfNoMetadata) { // Clear the data field if there is no metadata... if (dataLabelToUpdate == Constant.DatabaseColumn.DateTime) { image.SetDateTimeOffsetFromFileInfo(this.fileDatabase.FolderPath); imagesToUpdate.Add(image.GetDateTimeColumnTuples()); keyValueList.Add(new KeyValuePair <string, string>(image.File, "No metadata found - date/time reread from file")); } else { List <ColumnTuple> clearField = new List <ColumnTuple>() { new ColumnTuple(this.dataLabelByLabel[this.dataFieldLabel], String.Empty) }; imagesToUpdate.Add(new ColumnTuplesWithWhere(clearField, image.ID)); keyValueList.Add(new KeyValuePair <string, string>(image.File, "No metadata found - data field is cleared")); } } else { keyValueList.Add(new KeyValuePair <string, string>(image.File, "No metadata found - data field remains unaltered")); } continue; } string metadataValue = metadata[this.metadataFieldName].Value; ColumnTuplesWithWhere imageUpdate; if (dataLabelToUpdate == Constant.DatabaseColumn.DateTime) { if (DateTimeHandler.TryParseMetadataDateTaken(metadataValue, imageSetTimeZone, out DateTimeOffset metadataDateTime)) { image.SetDateTimeOffset(metadataDateTime); imageUpdate = image.GetDateTimeColumnTuples(); keyValueList.Add(new KeyValuePair <string, string>(image.File, metadataValue)); } else { keyValueList.Add(new KeyValuePair <string, string>(image.File, String.Format("'{0}' - data field remains unaltered - not a valid date/time.", metadataValue))); continue; } } else { imageUpdate = new ColumnTuplesWithWhere(new List <ColumnTuple>() { new ColumnTuple(dataLabelToUpdate, metadataValue) }, image.ID); keyValueList.Add(new KeyValuePair <string, string>(image.File, metadataValue)); } imagesToUpdate.Add(imageUpdate); } this.IsAnyDataUpdated = true; this.Progress.Report(new ProgressBarArguments(100, String.Format("Writing metadata for {0} files. Please wait...", totalImages), false, true)); Thread.Sleep(Constant.ThrottleValues.RenderingBackoffTime); // Allows the UI thread to update every now and then this.fileDatabase.UpdateFiles(imagesToUpdate); return keyValueList; }, this.Token).ConfigureAwait(true)); }