Explorar el Código

Document cache system for data entry panel

Kenric Nugteren hace 1 año
padre
commit
f98e50ff49

+ 2 - 0
prs.desktop/MainWindow.xaml.cs

@@ -156,6 +156,8 @@ namespace PRSDesktop
             HotKeyManager.RegisterHotKey(Key.F6, ShowRecordingNotes);
             HotKeyManager.RegisterHotKey(Key.F4, ToggleRecordingAudio);
 
+            DocumentCaches.ServerName = App.Profile;
+
             var settings = App.DatabaseSettings;
             bool dbConnected;
             DatabaseType = settings.DatabaseType;

+ 339 - 183
prs.desktop/Panels/DataEntry/DataEntryGrid.cs

@@ -11,256 +11,412 @@ using System.Linq;
 using System.Windows;
 using System.Windows.Media.Imaging;
 
-namespace PRSDesktop
+namespace PRSDesktop;
+
+public class DataEntryCachedDocument : ICachedDocument
 {
-    public class DataEntryGrid : DynamicDataGrid<DataEntryDocument>
+    public DateTime TimeStamp { get; set; }
+
+    public Document? Document { get; set; }
+
+    public Guid ID => Document?.ID ?? Guid.Empty;
+
+    public DataEntryCachedDocument() { }
+
+    public DataEntryCachedDocument(Document document)
     {
-        private List<DataEntryTag>? _tags;
-        
-        public DataEntryGrid()
+        Document = document;
+        TimeStamp = document.TimeStamp;
+    }
+
+    public void DeserializeBinary(CoreBinaryReader reader, bool full)
+    {
+        TimeStamp = reader.ReadDateTime();
+        if (full)
         {
-            HiddenColumns.Add(x => x.Tag.ID);
-            HiddenColumns.Add(x => x.Tag.AppliesTo);
-            HiddenColumns.Add(x => x.Document.ID);
-            HiddenColumns.Add(x=>x.EntityID);
-            HiddenColumns.Add(x=>x.Archived);
-            
-            ActionColumns.Add(new DynamicImageColumn(LinkedImage) { Position = DynamicActionColumnPosition.Start });
+            Document = reader.ReadObject<Document>();
         }
+    }
 
-        private static readonly BitmapImage link = PRSDesktop.Resources.link.AsBitmapImage();
-        
-        private BitmapImage? LinkedImage(CoreRow? arg)
+    public void SerializeBinary(CoreBinaryWriter writer)
+    {
+        writer.Write(TimeStamp);
+        if (Document is null)
         {
-            return arg == null
-                ? link
-                : arg.Get<DataEntryDocument, Guid>(x => x.EntityID) != Guid.Empty
-                    ? link
-                    : null;
+            throw new Exception("Cannot serialize incomplete CachedDocument");
         }
+        writer.WriteObject(Document);
+    }
+}
+
+public class DataEntryCache : DocumentCache<DataEntryCachedDocument>
+{
+    private static TimeSpan _maxAge = TimeSpan.FromDays(7);
+
+    public override TimeSpan MaxAge => _maxAge;
 
-        protected override void DoReconfigure(FluentList<DynamicGridOption> options)
+    private static DataEntryCache? _cache;
+    public static DataEntryCache Cache
+    {
+        get
         {
-            base.DoReconfigure(options);
-            options.BeginUpdate()
-                .Clear()
-                .Add(DynamicGridOption.MultiSelect)
-                .Add(DynamicGridOption.DragSource)
-                .Add(DynamicGridOption.DragTarget)
-                .Add(DynamicGridOption.SelectColumns)
-                .EndUpdate();
+            _cache ??= DocumentCaches.GetOrRegister<DataEntryCache>();
+            return _cache;
         }
-        
-        public static List<DataEntryTag> GetVisibleTagList()
+    }
+
+    public DataEntryCache(): base(nameof(DataEntryCache)) { }
+
+    protected override DataEntryCachedDocument? LoadDocument(Guid id)
+    {
+        var document = Client.Query(new Filter<Document>(x => x.ID).IsEqualTo(id))
+            .ToObjects<Document>().FirstOrDefault();
+        if(document is not null)
+        {
+            return new DataEntryCachedDocument(document);
+        }
+        else
         {
-            var tags = new Client<DataEntryTag>().Query().ToObjects<DataEntryTag>().ToList();
+            return null;
+        }
+    }
+    
+    /// <summary>
+    /// Fetch a bunch of documents from the cache or the database, optionally checking against the timestamp listed in the database.
+    /// </summary>
+    /// <param name="ids"></param>
+    /// <param name="checkTimestamp">
+    /// If <see langword="true"/>, then loads <see cref="Document.TimeStamp"/> from the database for all cached documents,
+    /// and if they are older, updates the cache.
+    /// </param>
+    public IEnumerable<Document> LoadDocuments(IEnumerable<Guid> ids, bool checkTimestamp = false)
+    {
+        var cached = new List<Guid>();
+        var toLoad = new List<Guid>();
+        foreach (var docID in ids)
+        {
+            if (Has(docID))
+            {
+                cached.Add(docID);
+            }
+            else
+            {
+                toLoad.Add(docID);
+            }
+        }
 
-            var tagsList = new List<DataEntryTag>();
-            foreach (var tag in tags)
+        var loadedCached = new List<Document>();
+        if (cached.Count > 0)
+        {
+            var docs = Client.Query(
+                new Filter<Document>(x => x.ID).InList(cached.ToArray()),
+                new Columns<Document>(x => x.TimeStamp, x => x.ID));
+            foreach (var doc in docs.ToObjects<Document>())
             {
-                var entity = CoreUtils.GetEntityOrNull(tag.AppliesTo);
-                if (entity is null || Security.CanView(entity))
+                try
                 {
-                    var tagHasEmployee = new Client<DataEntryTagDistributionEmployee>()
-                        .Query(
-                            new Filter<DataEntryTagDistributionEmployee>(x => x.Tag.ID).IsEqualTo(tag.ID)
-                                .And(x => x.Employee.ID).IsEqualTo(App.EmployeeID),
-                            new Columns<DataEntryTagDistributionEmployee>(x => x.ID))
-                        .Rows.Any();
-                    if (tagHasEmployee)
+                    var timestamp = GetHeader(doc.ID).Document.TimeStamp;
+                    if (doc.TimeStamp > timestamp)
+                    {
+                        toLoad.Add(doc.ID);
+                    }
+                    else
                     {
-                        tagsList.Add(tag);
+                        loadedCached.Add(GetFull(doc.ID).Document.Document!);
                     }
                 }
+                catch (Exception e)
+                {
+                    CoreUtils.LogException("", e, "Error loading cached file");
+                    toLoad.Add(doc.ID);
+                }
             }
-            return tagsList;
         }
 
-        private List<DataEntryTag> GetVisibleTags()
+        if (toLoad.Count > 0)
+        {
+            var loaded = Client.Query(new Filter<Document>(x => x.ID).InList(toLoad.ToArray()))
+                .ToObjects<Document>().ToList();
+            foreach (var loadedDoc in loaded)
+            {
+                Add(new DataEntryCachedDocument(loadedDoc));
+            }
+            return loaded.Concat(loadedCached);
+        }
+        else
         {
-            _tags ??= GetVisibleTagList();
-            return _tags;
+            return loadedCached;
         }
+    }
+
+}
+
+public class DataEntryGrid : DynamicDataGrid<DataEntryDocument>
+{
+    private List<DataEntryTag>? _tags;
+    
+    public DataEntryGrid()
+    {
+        HiddenColumns.Add(x => x.Tag.ID);
+        HiddenColumns.Add(x => x.Tag.AppliesTo);
+        HiddenColumns.Add(x => x.Document.ID);
+        HiddenColumns.Add(x => x.EntityID);
+        HiddenColumns.Add(x => x.Archived);
         
-        public void DoExplode()
+        ActionColumns.Add(new DynamicImageColumn(LinkedImage) { Position = DynamicActionColumnPosition.Start });
+
+        var tagFilter = new Filter<DataEntryDocument>(x => x.Tag.ID).InList(GetVisibleTags().Select(x => x.ID).ToArray());
+        if (Security.IsAllowed<CanSetupDataEntryTags>())
         {
-            Guid tagID = Guid.Empty;
-            foreach (var row in SelectedRows)
-            {
-                var rowTag = row.Get<DataEntryDocument, Guid>(x => x.Tag.ID);
-                if (tagID == Guid.Empty)
-                {
-                    tagID = rowTag;
-                }
-                else if (rowTag != tagID)
-                {
-                    tagID = Guid.Empty;
-                    break;
-                }
-            }
+            tagFilter.Or(x => x.Tag.ID).IsEqualTo(Guid.Empty);
+        }
 
-            var docIDs = SelectedRows.Select(r => r.Get<DataEntryDocument, Guid>(c => c.Document.ID)).ToArray();
-            var docs = new Client<Document>()
-                .Query(
-                    new Filter<Document>(x => x.ID).InList(docIDs),
-                    new Columns<Document>(x => x.ID).Add(x => x.Data).Add(x => x.FileName))
-                .ToObjects<Document>().ToDictionary(x => x.ID, x => x);
+        var docs = Client.Query(
+            new Filter<DataEntryDocument>(x => x.Archived).IsEqualTo(DateTime.MinValue)
+                .And(tagFilter),
+            new Columns<DataEntryDocument>(x => x.Document.ID));
 
-            var pages = new List<DataEntryReGroupWindow.Page>();
-            string filename = "";
-            foreach (var docID in docIDs)
+        DataEntryCache.Cache.ClearOld();
+        DataEntryCache.Cache.EnsureStrict(docs.Rows.Select(x => x.Get<DataEntryDocument, Guid>(x => x.Document.ID)).ToArray());
+    }
+
+    private static readonly BitmapImage link = PRSDesktop.Resources.link.AsBitmapImage();
+    
+    private BitmapImage? LinkedImage(CoreRow? arg)
+    {
+        return arg == null
+            ? link
+            : arg.Get<DataEntryDocument, Guid>(x => x.EntityID) != Guid.Empty
+                ? link
+                : null;
+    }
+
+    protected override void DoReconfigure(FluentList<DynamicGridOption> options)
+    {
+        base.DoReconfigure(options);
+        options.BeginUpdate()
+            .Clear()
+            .Add(DynamicGridOption.MultiSelect)
+            .Add(DynamicGridOption.DragSource)
+            .Add(DynamicGridOption.DragTarget)
+            .Add(DynamicGridOption.SelectColumns)
+            .EndUpdate();
+    }
+    
+    public static List<DataEntryTag> GetVisibleTagList()
+    {
+        var tags = new Client<DataEntryTag>().Query().ToObjects<DataEntryTag>().ToList();
+
+        var tagsList = new List<DataEntryTag>();
+        foreach (var tag in tags)
+        {
+            var entity = CoreUtils.GetEntityOrNull(tag.AppliesTo);
+            if (entity is null || Security.CanView(entity))
             {
-                if (docs.TryGetValue(docID, out var doc))
+                var tagHasEmployee = new Client<DataEntryTagDistributionEmployee>()
+                    .Query(
+                        new Filter<DataEntryTagDistributionEmployee>(x => x.Tag.ID).IsEqualTo(tag.ID)
+                            .And(x => x.Employee.ID).IsEqualTo(App.EmployeeID),
+                        new Columns<DataEntryTagDistributionEmployee>(x => x.ID))
+                    .Rows.Any();
+                if (tagHasEmployee)
                 {
-                    filename = doc.FileName;
-                    var ms = new MemoryStream(doc.Data);
-                    var pdfDoc = DataEntryReGroupWindow.RenderToPDF(doc.FileName, ms);
-                    foreach (var page in DataEntryReGroupWindow.SplitIntoPages(doc.FileName, pdfDoc))
-                    {
-                        pages.Add(page);
-                    }
+                    tagsList.Add(tag);
                 }
             }
+        }
+        return tagsList;
+    }
 
-            if (ShowDocumentWindow(pages, filename, tagID))
+    private List<DataEntryTag> GetVisibleTags()
+    {
+        _tags ??= GetVisibleTagList();
+        return _tags;
+    }
+    
+    public void DoExplode()
+    {
+        Guid tagID = Guid.Empty;
+        foreach (var row in SelectedRows)
+        {
+            var rowTag = row.Get<DataEntryDocument, Guid>(x => x.Tag.ID);
+            if (tagID == Guid.Empty)
             {
-                // ShowDocumentWindow already saves new scans, so we just need to get rid of the old ones.
-                DeleteItems(SelectedRows);
-                Refresh(false,true);
+                tagID = rowTag;
+            }
+            else if (rowTag != tagID)
+            {
+                tagID = Guid.Empty;
+                break;
             }
         }
-        
-        public void DoRemove()
-        {
-            var updates = SelectedRows.Select(x => x.ToObject<DataEntryDocument>()).ToArray();
-            foreach (var update in updates)
-                update.Archived = DateTime.Now;
-            new Client<DataEntryDocument>().Save(updates,"Removed from Data Entry Panel");
-            Refresh(false,true);
-        }
 
-        public void DoChangeTags(Guid tagid)
+        var docIDs = SelectedRows.Select(r => r.Get<DataEntryDocument, Guid>(c => c.Document.ID)).ToArray();
+        var docs = new Client<Document>()
+            .Query(
+                new Filter<Document>(x => x.ID).InList(docIDs),
+                new Columns<Document>(x => x.ID).Add(x => x.Data).Add(x => x.FileName))
+            .ToObjects<Document>().ToDictionary(x => x.ID, x => x);
+
+        var pages = new List<DataEntryReGroupWindow.Page>();
+        string filename = "";
+        foreach (var docID in docIDs)
         {
-            var updates = SelectedRows.Select(x => x.ToObject<DataEntryDocument>()).ToArray();
-            foreach (var update in updates)
+            if (docs.TryGetValue(docID, out var doc))
             {
-                if (update.Tag.ID != tagid)
+                filename = doc.FileName;
+                var ms = new MemoryStream(doc.Data);
+                var pdfDoc = DataEntryReGroupWindow.RenderToPDF(doc.FileName, ms);
+                foreach (var page in DataEntryReGroupWindow.SplitIntoPages(doc.FileName, pdfDoc))
                 {
-                    update.Tag.ID = tagid;
-                    update.EntityID = Guid.Empty;
+                    pages.Add(page);
                 }
             }
+        }
 
-            new Client<DataEntryDocument>().Save(updates.Where(x=>x.IsChanged()),"Updated Tags on Data Entry Panel");
+        if (ShowDocumentWindow(pages, filename, tagID))
+        {
+            // ShowDocumentWindow already saves new scans, so we just need to get rid of the old ones.
+            DeleteItems(SelectedRows);
             Refresh(false,true);
         }
-        
-        protected override DragDropEffects OnRowsDragStart(CoreRow[] rows)
+    }
+    
+    public void DoRemove()
+    {
+        var updates = SelectedRows.Select(x => x.ToObject<DataEntryDocument>()).ToArray();
+        foreach (var update in updates)
         {
-            var table = new CoreTable();
+            update.Archived = DateTime.Now;
+            DataEntryCache.Cache.Remove(update.Document.ID);
+        }
+        new Client<DataEntryDocument>().Save(updates,"Removed from Data Entry Panel");
+        Refresh(false,true);
+    }
 
-            table.Columns.Add(new CoreColumn { ColumnName = "ID", DataType = typeof(Guid) });
-            foreach(var row in rows)
+    public void DoChangeTags(Guid tagid)
+    {
+        var updates = SelectedRows.Select(x => x.ToObject<DataEntryDocument>()).ToArray();
+        foreach (var update in updates)
+        {
+            if (update.Tag.ID != tagid)
             {
-                var newRow = table.NewRow();
-                newRow.Set<Document, Guid>(x => x.ID, row.Get<DataEntryDocument, Guid>(x => x.Document.ID));
-                table.Rows.Add(newRow);
+                update.Tag.ID = tagid;
+                update.EntityID = Guid.Empty;
             }
+        }
+
+        new Client<DataEntryDocument>().Save(updates.Where(x=>x.IsChanged()),"Updated Tags on Data Entry Panel");
+        Refresh(false,true);
+    }
+    
+    protected override DragDropEffects OnRowsDragStart(CoreRow[] rows)
+    {
+        var table = new CoreTable();
 
-            return DragTable(typeof(Document), table);
+        table.Columns.Add(new CoreColumn { ColumnName = "ID", DataType = typeof(Guid) });
+        foreach(var row in rows)
+        {
+            var newRow = table.NewRow();
+            newRow.Set<Document, Guid>(x => x.ID, row.Get<DataEntryDocument, Guid>(x => x.Document.ID));
+            table.Rows.Add(newRow);
         }
 
-        public void UploadDocument(string filename, byte[] data, Guid tagID)
+        return DragTable(typeof(Document), table);
+    }
+
+    public void UploadDocument(string filename, byte[] data, Guid tagID)
+    {
+        var document = new Document
         {
-            var document = new Document
-            {
-                FileName = filename,
-                CRC = CoreUtils.CalculateCRC(data),
-                TimeStamp = DateTime.Now,
-                Data = data
-            };
+            FileName = filename,
+            CRC = CoreUtils.CalculateCRC(data),
+            TimeStamp = DateTime.Now,
+            Data = data
+        };
 
-            new Client<Document>().Save(document, "");
+        new Client<Document>().Save(document, "");
 
-            var dataentry = new DataEntryDocument
+        var dataentry = new DataEntryDocument
+        {
+            Document =
             {
-                Document =
-                {
-                    ID = document.ID
-                },
-                Tag =
-                {
-                    ID = tagID
-                },
-                Employee =
-                {
-                    ID = App.EmployeeID
-                },
-                Thumbnail = ImageUtils.GetPDFThumbnail(data, 256, 256)
-            };
-            new Client<DataEntryDocument>().Save(dataentry, "");
-
-            Dispatcher.Invoke(() =>
+                ID = document.ID
+            },
+            Tag =
             {
-                Refresh(false, true);
-            });
-        }
+                ID = tagID
+            },
+            Employee =
+            {
+                ID = App.EmployeeID
+            },
+            Thumbnail = ImageUtils.GetPDFThumbnail(data, 256, 256)
+        };
+        new Client<DataEntryDocument>().Save(dataentry, "");
 
-        private static PdfDocumentBase CombinePages(IEnumerable<DataEntryReGroupWindow.Page> pages)
+        DataEntryCache.Cache.Add(new DataEntryCachedDocument(document));
+
+        Dispatcher.Invoke(() =>
         {
-            var document = new PdfDocument();
-            foreach (var page in pages)
-            {
-                document.ImportPage(page.Pdf, page.PageIndex);
-            }
-            return document;
+            Refresh(false, true);
+        });
+    }
+
+    private static PdfDocumentBase CombinePages(IEnumerable<DataEntryReGroupWindow.Page> pages)
+    {
+        var document = new PdfDocument();
+        foreach (var page in pages)
+        {
+            document.ImportPage(page.Pdf, page.PageIndex);
         }
+        return document;
+    }
 
-        public bool ShowDocumentWindow(List<DataEntryReGroupWindow.Page> pages, string filename, Guid tagID)
+    public bool ShowDocumentWindow(List<DataEntryReGroupWindow.Page> pages, string filename, Guid tagID)
+    {
+        var window = new DataEntryReGroupWindow(pages, filename, tagID);
+        if (window.ShowDialog() == true)
         {
-            var window = new DataEntryReGroupWindow(pages, filename, tagID);
-            if (window.ShowDialog() == true)
+            Progress.ShowModal("Uploading Files", (progress) =>
             {
-                Progress.ShowModal("Uploading Files", (progress) =>
+                foreach (var group in window.Groups)
                 {
-                    foreach (var group in window.Groups)
-                    {
-                        progress.Report($"Uploading '{group.FileName}'");
-                        var doc = CombinePages(group.Pages);
+                    progress.Report($"Uploading '{group.FileName}'");
+                    var doc = CombinePages(group.Pages);
 
-                        byte[] data;
-                        using (var ms = new MemoryStream())
-                        {
-                            doc.Save(ms);
-                            data = ms.ToArray();
-                        }
-
-                        UploadDocument(group.FileName, data, group.TagID);
+                    byte[] data;
+                    using (var ms = new MemoryStream())
+                    {
+                        doc.Save(ms);
+                        data = ms.ToArray();
                     }
-                });
-                return true;
-            }
-            return false;
-        }
-        
-        protected override void GenerateColumns(DynamicGridColumns columns)
-        {
-            columns.Add<DataEntryDocument, string>(x => x.Document.FileName, 0, "Filename", "", Alignment.MiddleLeft);
-            columns.Add<DataEntryDocument, string>(x => x.Tag.Name, 100, "Tag", "", Alignment.MiddleLeft);
-        }
 
-        protected override void Reload(Filters<DataEntryDocument> criteria, Columns<DataEntryDocument> columns, ref SortOrder<DataEntryDocument>? sort, Action<CoreTable?, Exception?> action)
-        {
-            criteria.Add(new Filter<DataEntryDocument>(x => x.Archived).IsEqualTo(DateTime.MinValue));
+                    UploadDocument(group.FileName, data, group.TagID);
+                }
+            });
+            return true;
+        }
+        return false;
+    }
+    
+    protected override void GenerateColumns(DynamicGridColumns columns)
+    {
+        columns.Add<DataEntryDocument, string>(x => x.Document.FileName, 0, "Filename", "", Alignment.MiddleLeft);
+        columns.Add<DataEntryDocument, string>(x => x.Tag.Name, 100, "Tag", "", Alignment.MiddleLeft);
+    }
 
-            var tagFilter = new Filter<DataEntryDocument>(x => x.Tag.ID).InList(GetVisibleTags().Select(x => x.ID).ToArray());
-            if (Security.IsAllowed<CanSetupDataEntryTags>())
-            {
-                tagFilter.Or(x => x.Tag.ID).IsEqualTo(Guid.Empty);
-            }
+    protected override void Reload(Filters<DataEntryDocument> criteria, Columns<DataEntryDocument> columns, ref SortOrder<DataEntryDocument>? sort, Action<CoreTable?, Exception?> action)
+    {
+        criteria.Add(new Filter<DataEntryDocument>(x => x.Archived).IsEqualTo(DateTime.MinValue));
 
-            criteria.Add(tagFilter);
-            base.Reload(criteria, columns, ref sort, action);
+        var tagFilter = new Filter<DataEntryDocument>(x => x.Tag.ID).InList(GetVisibleTags().Select(x => x.ID).ToArray());
+        if (Security.IsAllowed<CanSetupDataEntryTags>())
+        {
+            tagFilter.Or(x => x.Tag.ID).IsEqualTo(Guid.Empty);
         }
+
+        criteria.Add(tagFilter);
+        base.Reload(criteria, columns, ref sort, action);
     }
 }

+ 48 - 31
prs.desktop/Panels/DataEntry/DataEntryList.xaml.cs

@@ -13,6 +13,7 @@ using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 using System.Windows;
+using System.Windows.Media;
 using System.Windows.Media.Imaging;
 using InABox.DynamicGrid;
 using InABox.WPF;
@@ -166,33 +167,40 @@ namespace PRSDesktop
          
             SelectedScans = selected;
             ViewListPanel.Children.Clear();
-            
-            var docIDs = SelectedScans.Select(x => x.Document.ID).Distinct().ToArray();
-            new Client<Document>()
-                .Query(
-                    new Filter<Document>(x => x.ID).InList(docIDs),
-                    new Columns<Document>(x => x.ID).Add(x => x.Data).Add(x => x.FileName),
-                    null,(o,e) => Dispatcher.BeginInvoke(() => LoadDocuments(o)));
+
+            Task.Run(() =>
+            {
+                try
+                {
+                    var docs = DataEntryCache.Cache.LoadDocuments(SelectedScans.Select(x => x.Document.ID).Distinct(), checkTimestamp: true);
+                    LoadDocuments(docs);
+                }
+                catch (Exception e)
+                {
+                    CoreUtils.LogException(ClientFactory.UserID, e);
+                    MessageBox.Show("An error occurred while loading the documents");
+                }
+            });
         }
 
-        private void LoadDocuments(CoreTable? data)
+        private void LoadDocuments(IEnumerable<Document> documents)
         {
-            var documents = data?.ToObjects<Document>() ?? Array.Empty<Document>();
-            var bitmaps = new Dictionary<Guid, List<BitmapImage>>();
+            var bitmaps = new Dictionary<Guid, List<ImageSource>>();
             foreach (var document in documents)
             {
                 List<byte[]> images;
+                var bitmapImages = new List<ImageSource>();
                 var extension = Path.GetExtension(document.FileName).ToLower();
                 if (extension == ".pdf")
                 {
+                    images = new List<byte[]>();
                     try
                     {
-                        images = ImageUtils.RenderPDFToImages(document.Data);
+                        bitmapImages = ImageUtils.RenderPDFToImageSources(document.Data);
                     }
                     catch (Exception e)
                     {
                         MessageBox.Show($"Cannot load document '{document.FileName}': {e.Message}");
-                        images = new List<byte[]>();
                     }
                 }
                 else if (extension == ".jpg" || extension == ".jpeg" || extension == ".png" || extension == ".bmp")
@@ -204,41 +212,50 @@ namespace PRSDesktop
                     images = ImageUtils.RenderTextFileToImages(Encoding.UTF8.GetString(document.Data));
                 }
 
-                foreach (var imageData in images)
+                bitmapImages.AddRange(images.Select(x =>
                 {
                     try
                     {
-                        if (!bitmaps.TryGetValue(document.ID, out var list))
-                        {
-                            list = new List<BitmapImage>();
-                            bitmaps[document.ID] = list;
-                        }
-
-                        list.Add(ImageUtils.LoadImage(imageData));
+                        return ImageUtils.LoadImage(x);
                     }
                     catch (Exception e)
                     {
                         MessageBox.Show($"Cannot load document '{document.FileName}': {e.Message}");
                     }
+                    return null;
+                }).Where(x => x != null).Cast<ImageSource>());
+
+                foreach (var image in bitmapImages)
+                {
+                    if (!bitmaps.TryGetValue(document.ID, out var list))
+                    {
+                        list = new List<ImageSource>();
+                        bitmaps[document.ID] = list;
+                    }
+
+                    list.Add(image);
                 }
             }
-       
-            foreach(var scan in SelectedScans)
+
+            Dispatcher.Invoke(() =>
             {
-                if(bitmaps.TryGetValue(scan.Document.ID, out var list))
+                foreach (var scan in SelectedScans)
                 {
-                    foreach(var bitmap in list)
+                    if (bitmaps.TryGetValue(scan.Document.ID, out var list))
                     {
-                        var image = new Image
+                        foreach (var bitmap in list)
                         {
-                            Source = bitmap,
-                            Margin = new Thickness(0, 0, 0, 20),
-                            ContextMenu = ViewListPanel.ContextMenu
-                        };
-                        ViewListPanel.Children.Add(image);
+                            var image = new Image
+                            {
+                                Source = bitmap,
+                                Margin = new Thickness(0, 0, 0, 20),
+                                ContextMenu = ViewListPanel.ContextMenu
+                            };
+                            ViewListPanel.Children.Add(image);
+                        }
                     }
                 }
-            }
+            });
         }
 
         #endregion

+ 21 - 43
prs.desktop/Panels/Factory/FactoryPanel.xaml.cs

@@ -120,9 +120,8 @@ namespace PRSDesktop
 
             Shipments = setups[nameof(Shipment)];
 
-            CurrentSection = Sections.FirstOrDefault(x => x.ID.Equals(settings.Section));
-            if (CurrentSection == null)
-                CurrentSection = Sections.FirstOrDefault();
+            CurrentSection = Sections.FirstOrDefault(x => x.ID.Equals(settings.Section))
+                ?? Sections.FirstOrDefault();
             var iSection = 0;
             var iCount = 0;
             foreach (var section in Sections)
@@ -286,18 +285,6 @@ namespace PRSDesktop
             }
         }
 
-        public static IEnumerable<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject
-        {
-            if (depObj != null)
-                for (var i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
-                {
-                    var child = VisualTreeHelper.GetChild(depObj, i);
-                    if (child != null && child is T) yield return (T)child;
-
-                    foreach (var childOfChild in FindVisualChildren<T>(child)) yield return childOfChild;
-                }
-        }
-
         private void ShutdownScanner()
         {
             try
@@ -331,7 +318,7 @@ namespace PRSDesktop
             BarcodeScannerManager.Instance.DataReceived += Instance_DataReceived;
         }
 
-        private void Instance_DataReceived(object sender, BarcodeScanEventArgs e)
+        private void Instance_DataReceived(object? sender, BarcodeScanEventArgs e)
         {
             Dispatcher.Invoke(() => { ProcessCode(Scanners[(int)e.ScannerId], e.Data); });
         }
@@ -353,8 +340,7 @@ namespace PRSDesktop
                         DoRefresh(true);
                         Progress.Close();
 
-                        if (scanner != null)
-                            scanner.Actions.SoundBeeper(BeepPattern.FastWarble);
+                        scanner?.Actions.SoundBeeper(BeepPattern.FastWarble);
                     }
                     else if (Trolleys.Any(x => x.ID.Equals(guid)))
                     {
@@ -380,13 +366,11 @@ namespace PRSDesktop
                         if (updates.Any())
                             new Client<ManufacturingPacket>().Save(updates, "Set Trolley to " + trolley, (o, e) => { });
 
-                        if (scanner != null)
-                            scanner.Actions.SoundBeeper(BeepPattern.LowHigh);
+                        scanner?.Actions.SoundBeeper(BeepPattern.LowHigh);
                     }
                     else
                     {
-                        if (scanner != null)
-                            scanner.Actions.SoundBeeper(BeepPattern.FourLowShort);
+                        scanner?.Actions.SoundBeeper(BeepPattern.FourLowShort);
                     }
                 }
                 else
@@ -400,8 +384,7 @@ namespace PRSDesktop
                             RackContents.ItemsSource = null;
                             rackid = Guid.Empty;
                             rackbarcode = "";
-                            if (scanner != null)
-                                scanner.Actions.SoundBeeper(BeepPattern.ThreeLowShort);
+                            scanner?.Actions.SoundBeeper(BeepPattern.ThreeLowShort);
                         }
                         else
                         {
@@ -418,8 +401,7 @@ namespace PRSDesktop
                             RackCount.Content = rows.Length.ToString();
 
                             RackPanel.Visibility = Visibility.Visible;
-                            if (scanner != null)
-                                scanner.Actions.SoundBeeper(BeepPattern.ThreeHighShort);
+                            scanner?.Actions.SoundBeeper(BeepPattern.ThreeHighShort);
                         }
                     }
                     else
@@ -436,8 +418,7 @@ namespace PRSDesktop
                                     delitem.ShipmentLink.ID = Guid.Empty;
                                     new Client<DeliveryItem>().Save(delitem, "Item Removed From Rack", (o, e) => { });
                                     row.Set<DeliveryItem, Guid>(x => x.ShipmentLink.ID, Guid.Empty);
-                                    if (scanner != null)
-                                        scanner.Actions.SoundBeeper(BeepPattern.HighLow);
+                                    scanner?.Actions.SoundBeeper(BeepPattern.HighLow);
                                 }
                                 else
                                 {
@@ -448,8 +429,7 @@ namespace PRSDesktop
                                     row.Set<DeliveryItem, Guid>(x => x.ShipmentLink.ID, rackid);
                                     if (Kanbans.Any(x => string.Equals(x.ID, delitem.ManufacturingPacketLink.ID.ToString())))
                                         ReloadPackets(true);
-                                    if (scanner != null)
-                                        scanner.Actions.SoundBeeper(BeepPattern.LowHigh);
+                                    scanner?.Actions.SoundBeeper(BeepPattern.LowHigh);
                                 }
 
                                 RackContents.ItemsSource = null;
@@ -466,8 +446,7 @@ namespace PRSDesktop
 
                             if (!id.HasValue)
                             {
-                                if (scanner != null)
-                                    scanner.Actions.SoundBeeper(BeepPattern.FourLowShort);
+                                scanner?.Actions.SoundBeeper(BeepPattern.FourLowShort);
                                 return;
                             }
 
@@ -475,8 +454,7 @@ namespace PRSDesktop
                             if (kanban == null)
                             {
                                 // Error - packet not visible from this station
-                                if (scanner != null)
-                                    scanner.Actions.SoundBeeper(BeepPattern.FourLowShort);
+                                scanner?.Actions.SoundBeeper(BeepPattern.FourLowShort);
                                 return;
                             }
 
@@ -485,8 +463,7 @@ namespace PRSDesktop
                                                                             .Equals(settings.Section))?.ToObject<ManufacturingPacketStage>();
 
                             if (stage == null)
-                                if (scanner != null)
-                                    scanner.Actions.SoundBeeper(BeepPattern.FourHighShort);
+                                scanner?.Actions.SoundBeeper(BeepPattern.FourHighShort);
 
                             if (stage.Station == 0) AddPacketToCurrentWorkload(stage);
 
@@ -496,16 +473,14 @@ namespace PRSDesktop
                                 UpdateSelectedKanban(false);
                             }
 
-                            if (scanner != null)
-                                scanner.Actions.SoundBeeper(BeepPattern.LowHigh);
+                            scanner?.Actions.SoundBeeper(BeepPattern.LowHigh);
                         }
                     }
                 }
             }
-            catch (Exception e)
+            catch (Exception)
             {
-                if (scanner != null)
-                    scanner.Actions.SoundBeeper(BeepPattern.FourLowShort);
+                scanner?.Actions.SoundBeeper(BeepPattern.FourLowShort);
             }
         }
 
@@ -931,7 +906,7 @@ namespace PRSDesktop
                     // Set the Proper Button Set for this type of Packet
                     MfgRow.Height = Pending ? new GridLength(00) : new GridLength(50);
 
-                    foreach (var btn in FindVisualChildren<Button>(ButtonGrid))
+                    foreach (var btn in ButtonGrid.FindVisualChildren<Button>())
                         btn.IsEnabled = CurrentKanban != null;
 
                     var idle = CompleteButton.Background;
@@ -1984,7 +1959,10 @@ namespace PRSDesktop
         private CoreTable Packets;
         private CoreTable Stages;
 
-        private ManufacturingSection CurrentSection;
+        /// <remarks>
+        /// <see langword="null"/> if there are no <see cref="ManufacturingSection"/>s in the system.
+        /// </remarks>
+        private ManufacturingSection? CurrentSection;
         private int CurrentStation;
         private readonly bool PendingVisible = true;
 

+ 1 - 1
prs.desktop/Panels/Staging/StagingPanel.xaml.cs

@@ -251,7 +251,7 @@ public class Module
             if (first is null)
                 return null;
             _documentdata = first.Get<Document, byte[]>(x => x.Data);
-            return ImageUtils.RenderPDFToImages(_documentdata);
+            return ImageUtils.RenderPDFToImageBytes(_documentdata);
         }
         
         private void RenderDocuments(List<byte[]>? documents)