diff --git a/Core/Models/TagDatabase/TagDatabase.WriteMethods.cs b/Core/Models/TagDatabase/TagDatabase.WriteMethods.cs index 6676717..d24575f 100644 --- a/Core/Models/TagDatabase/TagDatabase.WriteMethods.cs +++ b/Core/Models/TagDatabase/TagDatabase.WriteMethods.cs @@ -11,7 +11,6 @@ public partial class TagDatabase /// /// The tag object. /// The SqliteTransaction to execute queries on, will make its own if null. - /// true if the tag has been saved. /// Thrown if the tag already existed in the database. public async Task WriteTagToDatabase(Tag tag, SqliteTransaction? transaction = null) { @@ -43,12 +42,12 @@ public partial class TagDatabase if (index != -1) { this.Tags[index] = tag; - TagUpdated?.Invoke(this, tag); + TagUpdated.Invoke(this, tag); } else { this.Tags.Add(tag); - TagAdded?.Invoke(this, tag); + TagAdded.Invoke(this, tag); } } catch (SqliteException) @@ -62,6 +61,12 @@ public partial class TagDatabase } } + /// + /// Saves all tag objects to the database. + /// + /// The List of Tag objects. + /// The SqliteTransaction to execute queries on, will make its own if null. + /// Thrown if the tag already existed in the database. public async Task WriteTagsToDatabase(List tags, SqliteTransaction? transaction = null) { this.CheckInitialisation(); diff --git a/UI/TagDatabaseService.cs b/UI/TagDatabaseService.cs index a490dea..4f3a0a7 100644 --- a/UI/TagDatabaseService.cs +++ b/UI/TagDatabaseService.cs @@ -20,7 +20,7 @@ public class TagDatabaseService : ObservableObject public event EventHandler? TagAdded; public event EventHandler<(int id, string name)>? TagDeleted; public event EventHandler>? TagsAdded; - public event EventHandler? TagUpdated; + public event EventHandler>? TagsUpdated; public string DatabaseName => this.Database?.Name ?? string.Empty; public int DatabaseVersion => this.Database?.Version ?? 0; @@ -142,6 +142,12 @@ public class TagDatabaseService : ObservableObject this.NotifyDatabasePropertiesChanged(); } + public async Task WriteTagToDatabase(List tag) + { + if (this.Database is null) return; + await this.Database.WriteTagsToDatabase(tag); + } + public async Task WriteTagsToDatabase(List tags) { if (this.Database is null) return; @@ -189,7 +195,7 @@ public class TagDatabaseService : ObservableObject if (this.Database is null) return; this.Database.TagAdded += this.TagDatabase_OnTagAdded; this.Database.TagsAdded += this.TagDatabase_OnTagsAdded; - this.Database.TagUpdated += this.TagDatabase_OnTagUpdated; + this.Database.TagsUpdated += this.TagDatabase_OnTagsUpdated; this.Database.TagDeleted += this.TagDatabase_OnTagDeleted; } @@ -220,9 +226,9 @@ public class TagDatabaseService : ObservableObject this.NotifyDatabasePropertiesChanged(); } - private void TagDatabase_OnTagUpdated(object? sender, Tag updatedTag) + private void TagDatabase_OnTagsUpdated(object? sender, List updatedTags) { - this.TagUpdated?.Invoke(this, updatedTag); + this.TagsUpdated?.Invoke(this, updatedTags); this.NotifyDatabasePropertiesChanged(); } @@ -231,7 +237,7 @@ public class TagDatabaseService : ObservableObject if (this.Database is null) return; this.Database.TagAdded -= this.TagDatabase_OnTagAdded; this.Database.TagsAdded -= this.TagDatabase_OnTagsAdded; - this.Database.TagUpdated -= this.TagDatabase_OnTagUpdated; + this.Database.TagsUpdated -= this.TagDatabase_OnTagsUpdated; this.Database.TagDeleted -= this.TagDatabase_OnTagDeleted; } } \ No newline at end of file diff --git a/UI/ViewModels/HierarchyTreeViewModel.cs b/UI/ViewModels/HierarchyTreeViewModel.cs index 6c67958..08b8d05 100644 --- a/UI/ViewModels/HierarchyTreeViewModel.cs +++ b/UI/ViewModels/HierarchyTreeViewModel.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; +using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using TagHierarchyManager.Models; @@ -32,7 +33,7 @@ public partial class HierarchyTreeViewModel : ViewModelBase, IDisposable { if (!this._mainWindow.TagDatabaseService.IsDatabaseOpen) return; - this._mainWindow.TagDatabaseService.TagUpdated -= this.TagDatabase_OnTagUpdated; + this._mainWindow.TagDatabaseService.TagsUpdated -= this.TagDatabase_OnTagsUpdated; this._mainWindow.TagDatabaseService.TagAdded -= this.TagDatabase_OnTagAdded; this._mainWindow.TagDatabaseService.TagDeleted -= this.TagDatabase_OnTagDeleted; this._mainWindow.TagDatabaseService.TagsAdded -= this.TagDatabase_OnTagsAdded; @@ -42,23 +43,34 @@ public partial class HierarchyTreeViewModel : ViewModelBase, IDisposable public async Task InitializeAsync() { if (!this._mainWindow.TagDatabaseService.IsDatabaseOpen) return; - this.ViewModelMap.Clear(); - this.TopLevelTagNodes.Clear(); - await Task.Run(() => + + await Dispatcher.UIThread.InvokeAsync(() => + { + this.ViewModelMap.Clear(); + this.ChildNodeMap.Clear(); + this.TopLevelTagNodes.Clear(); + }); + + + var topLevelTags = await Task.Run(() => this._mainWindow.TagDatabaseService.GetAllTags(true)); + + await Dispatcher.UIThread.InvokeAsync(() => { - var topLevelTags = this._mainWindow.TagDatabaseService.GetAllTags(true); var buildingTopLevelNodes = new List(); foreach (var tagNode in - topLevelTags!.Select(tag => new TagItemViewModel(tag, this._getParentNamesById))) + topLevelTags.Select(tag => new TagItemViewModel(tag, this._getParentNamesById))) { buildingTopLevelNodes.Add(tagNode); this.AddTagNodeToViewModelMap(tagNode); this.AddAllChildrenAsync(tagNode); } - + foreach (var tagNode in buildingTopLevelNodes) this.TopLevelTagNodes.Add(tagNode); }); + + + await Task.CompletedTask; } private void AddAllChildrenAsync(TagItemViewModel tag, bool beingUpdated = false) @@ -72,8 +84,7 @@ public partial class HierarchyTreeViewModel : ViewModelBase, IDisposable var childTags = this._mainWindow.TagDatabaseService.GetAllTagChildren(tag.Id); - - if (childTags is null) return; + foreach (var childNode in childTags.Select(child => new TagItemViewModel(child, this._getParentNamesById))) { @@ -96,7 +107,7 @@ public partial class HierarchyTreeViewModel : ViewModelBase, IDisposable { if (parent.Children.Any(c => c.Id == tag.Id)) continue; - await Task.Run(() => + await Dispatcher.UIThread.InvokeAsync(() => { var tagNode = new TagItemViewModel(tag, this._getParentNamesById); this.AddAllChildrenAsync(tagNode); @@ -124,7 +135,7 @@ public partial class HierarchyTreeViewModel : ViewModelBase, IDisposable } else { - if (beingUpdated) tagNodeSet.Clear(); + // if (beingUpdated) tagNodeSet.Clear(); tagNodeSet.Add(tagNode); } } @@ -132,21 +143,22 @@ public partial class HierarchyTreeViewModel : ViewModelBase, IDisposable private async Task AddTopLevelNode(Tag tag) { if (this.TopLevelTagNodes.Any(t => t.Id == tag.Id)) return; - await Task.Run(() => - { - var newTopLevelTag = new TagItemViewModel(tag, this._getParentNamesById); - // this does create considerable delay, would be nice to speed it up somehow. - this.AddAllChildrenAsync(newTopLevelTag); - - var index = 0; - while (index < this.TopLevelTagNodes.Count - && string.Compare(this.TopLevelTagNodes[index].Name, newTopLevelTag.Name, - StringComparison.CurrentCultureIgnoreCase) < 0) - index++; - - this.TopLevelTagNodes.Insert(index, newTopLevelTag); - this.AddTagNodeToViewModelMap(newTopLevelTag); - }); + + var newTopLevelTag = new TagItemViewModel(tag, this._getParentNamesById); + // this does create considerable delay, would be nice to speed it up somehow. + this.AddAllChildrenAsync(newTopLevelTag); + + var index = 0; + while (index < this.TopLevelTagNodes.Count + && string.Compare(this.TopLevelTagNodes[index].Name, newTopLevelTag.Name, + StringComparison.CurrentCultureIgnoreCase) < 0) + index++; + + this.TopLevelTagNodes.Insert(index, newTopLevelTag); + this.AddTagNodeToViewModelMap(newTopLevelTag); + + await Task.CompletedTask; + } private async Task DeleteChildNode(int parentId, int idToDelete) @@ -154,22 +166,24 @@ public partial class HierarchyTreeViewModel : ViewModelBase, IDisposable if (!this.ViewModelMap.TryGetValue(parentId, out var parentViewModels)) return; foreach (var parentTag in parentViewModels) - await Task.Run(() => - { - var foundChild = - parentTag.Children.FirstOrDefault(t => t.Id == idToDelete)!; - parentTag.Children.Remove(foundChild); - }); + { + var foundChild = + parentTag.Children.FirstOrDefault(t => t.Id == idToDelete)!; + parentTag.Children.Remove(foundChild); + } + if (!this.ChildNodeMap.TryGetValue(idToDelete, out var parentSet)) return; parentSet.Remove(parentId); + await Task.CompletedTask; } private async Task DeleteTopLevelNode(int idToDelete) { var topLevelNode = this.TopLevelTagNodes.FirstOrDefault(t => t.Id == idToDelete); if (topLevelNode is null) return; - await Task.Run(() => { this.TopLevelTagNodes.Remove(topLevelNode); }); + this.TopLevelTagNodes.Remove(topLevelNode); + await Task.CompletedTask; } partial void OnSelectedTagChanged(TagItemViewModel? value) @@ -193,7 +207,7 @@ public partial class HierarchyTreeViewModel : ViewModelBase, IDisposable private void SubscribeToEvents() { if (!this._mainWindow.TagDatabaseService.IsDatabaseOpen) return; - this._mainWindow.TagDatabaseService.TagUpdated += this.TagDatabase_OnTagUpdated; + this._mainWindow.TagDatabaseService.TagsUpdated += this.TagDatabase_OnTagsUpdated; this._mainWindow.TagDatabaseService.TagAdded += this.TagDatabase_OnTagAdded; this._mainWindow.TagDatabaseService.TagDeleted += this.TagDatabase_OnTagDeleted; this._mainWindow.TagDatabaseService.TagsAdded += this.TagDatabase_OnTagsAdded; @@ -203,7 +217,7 @@ public partial class HierarchyTreeViewModel : ViewModelBase, IDisposable { try { - await this.ProcessTagAdditions([newTag]); + await Dispatcher.UIThread.InvokeAsync(async () => await this.ProcessTagAdditions([newTag])); } catch (Exception e) { @@ -216,7 +230,7 @@ public partial class HierarchyTreeViewModel : ViewModelBase, IDisposable { try { - await this.WipeTagNodes(deletedTag.id); + await Dispatcher.UIThread.InvokeAsync(async () => await this.WipeTagNodes(deletedTag.id)); } catch (Exception e) { @@ -238,48 +252,60 @@ public partial class HierarchyTreeViewModel : ViewModelBase, IDisposable } } - private async void TagDatabase_OnTagUpdated(object? sender, Tag updatedTag) + private void TagDatabase_OnTagsUpdated(object? sender, List updatedTags) { - try - { - if (!this.ViewModelMap.TryGetValue(updatedTag.Id, out var tagViewModels)) return; - - HashSet oldParents = []; - HashSet newParents = new(updatedTag.ParentIds); + Dispatcher.UIThread.Post(async void () => + { + try + { + foreach (var updatedTag in updatedTags) + await this.HandleTagUpdate(updatedTag); + } + catch (Exception e) + { + var error = new ErrorDialogViewModel(e.Message); + error.ShowDialog(); + } + }); + } - // grab old parents if they exist - if (this.ChildNodeMap.TryGetValue(updatedTag.Id, out var parentList)) - oldParents = [..parentList]; + private async Task HandleTagUpdate(Tag updatedTag) + { + if (!this.ViewModelMap.TryGetValue(updatedTag.Id, out var tagViewModels)) return; + + + foreach (var tag in tagViewModels) + { + tag.Tag = updatedTag; + tag.RefreshSelf(); tag.RefreshParentsString(); + } + + HashSet oldParents = []; + HashSet newParents = new(updatedTag.ParentIds); - // add/remove parents as necessary - var removedParents = oldParents.Except(newParents); - var addedParents = newParents.Except(oldParents); + // grab old parents if they exist + if (this.ChildNodeMap.TryGetValue(updatedTag.Id, out var parentList)) + oldParents = [..parentList]; - foreach (var parentId in removedParents) - await this.DeleteChildNode(parentId, updatedTag.Id); + // add/remove parents as necessary + var removedParents = oldParents.Except(newParents); + var addedParents = newParents.Except(oldParents); - foreach (var parentId in addedParents) - await this.AddChildNode(updatedTag, parentId); + foreach (var parentId in removedParents) + await this.DeleteChildNode(parentId, updatedTag.Id); - // refresh all tagViewModels - foreach (var tag in tagViewModels) - tag.RefreshSelf(); + foreach (var parentId in addedParents) + await this.AddChildNode(updatedTag, parentId); - // add/remove top level nodes as necessary - if (updatedTag.IsTopLevel) - await this.AddTopLevelNode(updatedTag); - else - await this.DeleteTopLevelNode(updatedTag.Id); + // add/remove top level nodes as necessary + if (updatedTag.IsTopLevel) + await this.AddTopLevelNode(updatedTag); + else + await this.DeleteTopLevelNode(updatedTag.Id); - // clear child node map if tag has no parents - if (newParents.Count == 0) - this.ChildNodeMap.Remove(updatedTag.Id); - } - catch (Exception e) - { - var error = new ErrorDialogViewModel(e.Message); - error.ShowDialog(); - } + // clear child node map if tag has no parents + if (newParents.Count == 0) + this.ChildNodeMap.Remove(updatedTag.Id); } private async Task WipeTagNodes(int idToDelete) diff --git a/UI/ViewModels/ImportDialogViewModel.cs b/UI/ViewModels/ImportDialogViewModel.cs index 07fd815..dded2d8 100644 --- a/UI/ViewModels/ImportDialogViewModel.cs +++ b/UI/ViewModels/ImportDialogViewModel.cs @@ -39,6 +39,9 @@ public partial class ImportDialogViewModel(MainWindowViewModel mainWindow) : Vie [RelayCommand] private async Task InitiateImport() { + var userWantsToSave = await this.MainWindow.ShowUnsavedChangesDialog(); + if (userWantsToSave is null) return; + try { if (string.IsNullOrEmpty(this.DatabaseFilePath) || string.IsNullOrEmpty(this.TemplateFilePath)) return; @@ -63,9 +66,6 @@ public partial class ImportDialogViewModel(MainWindowViewModel mainWindow) : Vie if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop || desktop.MainWindow is null) return; - var userWantsToSave = await this.MainWindow.ShowUnsavedChangesDialog(); - if (userWantsToSave is null) return; - var storageProvider = desktop.MainWindow?.StorageProvider; if (storageProvider is null) return; @@ -94,10 +94,7 @@ public partial class ImportDialogViewModel(MainWindowViewModel mainWindow) : Vie { if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop || desktop.MainWindow is null) return; - - var userWantsToSave = await this.MainWindow.ShowUnsavedChangesDialog(); - if (userWantsToSave is null) return; - + var storageProvider = desktop.MainWindow?.StorageProvider; if (storageProvider is null) return; diff --git a/UI/ViewModels/MainWindowViewModel.cs b/UI/ViewModels/MainWindowViewModel.cs index 9988bdc..bb0f028 100644 --- a/UI/ViewModels/MainWindowViewModel.cs +++ b/UI/ViewModels/MainWindowViewModel.cs @@ -100,19 +100,26 @@ public partial class MainWindowViewModel : ViewModelBase } [RelayCommand] - public async Task SaveSelectedTagAsync() + private async Task SaveSelectedTagAsync() { - if (this.SelectedTag is null || !this.TagDatabaseService.IsDatabaseOpen || !this.IsDbEnabled) return; + try + { + if (this.SelectedTag is null || !this.TagDatabaseService.IsDatabaseOpen || !this.IsDbEnabled) return; - this.SelectedTag.CommitEdit(); - await this.TagDatabaseService.WriteTagsToDatabase([this.SelectedTag.Tag]); - this.SelectedTag.RefreshParentsString(); - this.StatusBlockText = string.Format(Resources.StatusBlockTagSaveSuccessful, this.SelectedTag.Name); - this.UnsavedChanges = false; + this.SelectedTag.CommitEdit(); + await this.TagDatabaseService.WriteTagsToDatabase([this.SelectedTag.Tag]); + this.SelectedTag.RefreshParentsString(); + this.StatusBlockText = string.Format(Resources.StatusBlockTagSaveSuccessful, this.SelectedTag.Name); + this.UnsavedChanges = false; + } + catch (Exception ex) + { + this.ShowErrorDialog(ex.Message); + } } [RelayCommand] - public void ShowBulkAddDialog() + private void ShowBulkAddDialog() { if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop) return; @@ -126,7 +133,7 @@ public partial class MainWindowViewModel : ViewModelBase } [RelayCommand] - public void ShowDatabaseSettings() + private void ShowDatabaseSettings() { if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop) return; @@ -140,7 +147,7 @@ public partial class MainWindowViewModel : ViewModelBase } [RelayCommand] - public void ShowImportDialog() + private void ShowImportDialog() { if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop) return; @@ -153,7 +160,7 @@ public partial class MainWindowViewModel : ViewModelBase dialog.ShowDialog(desktop.MainWindow!); } - public async Task ShowNullableBoolDialog(Window dialog) + private async Task ShowNullableBoolDialog(Window dialog) { if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop) return null; diff --git a/UI/ViewModels/TagItemViewModel.cs b/UI/ViewModels/TagItemViewModel.cs index 0473ca0..02493c0 100644 --- a/UI/ViewModels/TagItemViewModel.cs +++ b/UI/ViewModels/TagItemViewModel.cs @@ -50,7 +50,7 @@ public partial class TagItemViewModel(Tag tag, Func, List>? ge public ObservableCollection Children { get; set; } = []; - internal Tag Tag { get; } = tag; + internal Tag Tag { get; set; } = tag; private bool IsTopLevel => this.Tag.IsTopLevel;