diff --git a/UI/ViewModels/HierarchyTreeViewModel.cs b/UI/ViewModels/HierarchyTreeViewModel.cs new file mode 100644 index 0000000..ffe1c54 --- /dev/null +++ b/UI/ViewModels/HierarchyTreeViewModel.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using TagHierarchyManager.Models; + +namespace TagHierarchyManager.UI.ViewModels; + +public partial class HierarchyTreeViewModel : ViewModelBase +{ + // private readonly TagDatabase _database; + private readonly MainWindowViewModel _mainWindow; + private readonly Dictionary _viewModelMap = new(); + + [ObservableProperty] + private ObservableCollection _topLevelTags = []; + + public HierarchyTreeViewModel(MainWindowViewModel mainWindow) + { + this._mainWindow = mainWindow; + // TODO tell it to sync hierarchy. + this._mainWindow.Database.TagUpdated += this.OnTagUpdated; + } + + public async Task InitializeAsync() + { + await this.SyncHierarchyAsync(); + } + + + private async void OnTagUpdated(object sender, Tag updatedTag) + { + try + { + await Task.Run(() => + { + foreach (var viewModel in this._viewModelMap.Values) + { + viewModel.RefreshParentsString(); + } + }); + await this.SyncHierarchyAsync(); + } + catch (Exception e) + { + throw; // TODO handle exception + } + } + + public async Task SyncHierarchyAsync() + { + if (this._mainWindow.Database is null) return; + + var activeKeys = new HashSet(); + + try + { + var result = await Task.Run(() => + { + var activeKeys = new HashSet(); + var children = this._mainWindow.Database.Tags + .SelectMany(t => t.ParentIds.Select(pId => new { ParentId = pId, Child = t })) + .ToLookup(x => x.ParentId, x => x.Child); + var topLevelTags = this._mainWindow.Database.Tags.Where(t => t.IsTopLevel).OrderBy(t => t.Name) + .ToList(); + + return (activeKeys, topLevelTags, children); + }); + + var topLevelViewModels = result.topLevelTags.Select(t => + { + var vm = GetOrCreateViewModel(t, 0); + activeKeys.Add($"0_{t.Id}"); + SyncTagRecursive(vm, result.children, activeKeys); + return vm; + }).ToList(); + SyncCollection(TopLevelTags, topLevelViewModels); + + var keysToRemove = _viewModelMap.Keys.Where(k => !activeKeys.Contains(k)).ToList(); + foreach (var key in keysToRemove) + { + _viewModelMap.Remove(key); + } + } + catch + { + throw; // TODO handle exception + } + } + + + // HierarchyTreeViewModel + private TagItemViewModel GetOrCreateViewModel(Tag tag, int parentId) + { + string key = $"{parentId}_{tag.Id}"; + if (!this._viewModelMap.TryGetValue(key, out var viewModel)) + { + viewModel = new TagItemViewModel(tag, id => + this._viewModelMap.Values.FirstOrDefault(v => v.Id == id)?.Name); + this._viewModelMap[key] = viewModel; + } + return viewModel; + } + + private void SyncTagRecursive(TagItemViewModel parentVm, ILookup childrenLookup, HashSet activeKeys) + { + var childTags = childrenLookup[parentVm.Id].OrderBy(t => t.Name).ToList(); + var childVms = new List(); + + foreach (var ct in childTags) + { + var key = $"{parentVm.Id}_{ct.Id}"; + activeKeys.Add(key); + + var childVm = GetOrCreateViewModel(ct, parentVm.Id); + childVms.Add(childVm); + + // Recurse: build this child's children + SyncTagRecursive(childVm, childrenLookup, activeKeys); + } + + parentVm.SyncChildren(childVms); + } + + // HierarchyTreeViewModel + private void SyncCollection(ObservableCollection collection, List newItems) + { + var updatedKeys = newItems.Select(v => v.Id).ToHashSet(); + for (int i = collection.Count - 1; i >= 0; i--) + { + if (!updatedKeys.Contains(collection[i].Id)) + collection.RemoveAt(i); + } + + var currentKeys = collection.Select(v => v.Id).ToHashSet(); + foreach (var newItem in newItems) + { + if (!currentKeys.Contains(newItem.Id)) + { + // Find the correct index to maintain alphabetical order + int index = 0; + while (index < collection.Count && string.Compare(collection[index].Name, newItem.Name, StringComparison.CurrentCultureIgnoreCase) < 0) + { + index++; + } + collection.Insert(index, newItem); + } + } + } +} \ No newline at end of file diff --git a/UI/ViewModels/MainWindowViewModel.cs b/UI/ViewModels/MainWindowViewModel.cs index 80d8f6e..2021f8b 100644 --- a/UI/ViewModels/MainWindowViewModel.cs +++ b/UI/ViewModels/MainWindowViewModel.cs @@ -12,11 +12,10 @@ namespace TagHierarchyManager.UI.ViewModels; public partial class MainWindowViewModel : ViewModelBase { - private TagDatabase _database; - - - - private readonly Dictionary _viewModelMap = new(); + internal TagDatabase Database; + + [ObservableProperty] + private HierarchyTreeViewModel _hierarchyTreeViewModel; [ObservableProperty] private TagItemViewModel? _selectedTag; @@ -27,10 +26,10 @@ public partial class MainWindowViewModel : ViewModelBase [ObservableProperty] private bool _isDbLoaded; - public int TotalTags => this._database?.Tags.Count ?? 0; + public int TotalTags => this.Database?.Tags.Count ?? 0; public string WindowTitle => IsDbLoaded - ? string.Format(Resources.TitleWithDatabase, this._database.Name) + ? string.Format(Resources.TitleWithDatabase, this.Database.Name) : Resources.Title; // TODO searchViewModel // TODO hierarchyViewModel @@ -39,6 +38,7 @@ public partial class MainWindowViewModel : ViewModelBase { } + // TagEditorViewModel, maybe? partial void OnSelectedTagChanged(TagItemViewModel? value) { value?.BeginEdit(); @@ -46,137 +46,35 @@ public partial class MainWindowViewModel : ViewModelBase public async Task LoadDatabase(string filePath) { + this.IsDbLoaded = false; TagDatabase db = new(); db.InitialisationComplete += OnDatabaseLoaded; - this.IsDbLoaded = false; - this.TopLevelTags.Clear(); - this._viewModelMap.Clear(); await db.LoadAsync(filePath); } + // TagEditorViewModel public async Task SaveTag() { - if (SelectedTag is null || _database is null) return; + if (SelectedTag is null || this.Database is null) return; var oldName = SelectedTag.Name; SelectedTag.CommitEdit(); - await _database.WriteTagToDatabase(SelectedTag.Tag); + await this.Database.WriteTagToDatabase(SelectedTag.Tag); } private void OnDatabaseLoaded(object sender, EventArgs e) { if (sender is not TagDatabase db) return; - Avalonia.Threading.Dispatcher.UIThread.Post(() => + Avalonia.Threading.Dispatcher.UIThread.Post(async () => { - this._database = db; + this.Database = db; this.IsDbLoaded = true; - this.SyncHierarchyAsync(); + this.HierarchyTreeViewModel = new HierarchyTreeViewModel(this); + await this.HierarchyTreeViewModel.InitializeAsync(); this.OnPropertyChanged(nameof(TotalTags)); this.OnPropertyChanged(nameof(WindowTitle)); - this._database.InitialisationComplete -= OnDatabaseLoaded; - this._database.TagUpdated += OnTagUpdated; - + this.Database.InitialisationComplete -= OnDatabaseLoaded; }); Debug.WriteLine($"Database loaded on UI - name: {db.Name}, version: {db.Version}"); } - - private void OnTagUpdated(object sender, Tag updatedTag) - { - _ = Task.Run(async () => { - foreach (var viewModel in _viewModelMap.Values) - { - viewModel.RefreshParentsString(); - } - this.SyncHierarchyAsync(); - }); - } - - public async Task SyncHierarchyAsync() - { - if (this._database is null) return; - - var activeKeys = new HashSet(); - - var result = await Task.Run(() => - { - var activeKeys = new HashSet(); - var children = _database.Tags - .SelectMany(t => t.ParentIds.Select(pId => new { ParentId = pId, Child = t })) - .ToLookup(x => x.ParentId, x => x.Child); - var topLevelTags = _database.Tags.Where(t => t.IsTopLevel).OrderBy(t => t.Name).ToList(); - - return (activeKeys, topLevelTags, children); - }); - - var topLevelViewModels = result.topLevelTags.Select(t => { - var vm = GetOrCreateViewModel(t, 0); - activeKeys.Add($"0_{t.Id}"); - SyncTagRecursive(vm, result.children, activeKeys); - return vm; - }).ToList(); - SyncCollection(TopLevelTags, topLevelViewModels); - - var keysToRemove = _viewModelMap.Keys.Where(k => !activeKeys.Contains(k)).ToList(); - foreach (var key in keysToRemove) - { - _viewModelMap.Remove(key); - } - } - - private void SyncTagRecursive(TagItemViewModel parentVm, ILookup childrenLookup, HashSet activeKeys) - { - var childTags = childrenLookup[parentVm.Id].OrderBy(t => t.Name).ToList(); - var childVms = new List(); - - foreach (var ct in childTags) - { - var key = $"{parentVm.Id}_{ct.Id}"; - activeKeys.Add(key); - - var childVm = GetOrCreateViewModel(ct, parentVm.Id); - childVms.Add(childVm); - - // Recurse: build this child's children - SyncTagRecursive(childVm, childrenLookup, activeKeys); - } - - parentVm.SyncChildren(childVms); - } - - private TagItemViewModel GetOrCreateViewModel(Tag tag, int parentId) - { - string key = $"{parentId}_{tag.Id}"; - if (!this._viewModelMap.TryGetValue(key, out var viewModel)) - { - viewModel = new TagItemViewModel(tag, id => - this._viewModelMap.Values.FirstOrDefault(v => v.Id == id)?.Name); - this._viewModelMap[key] = viewModel; - } - return viewModel; - } - - private void SyncCollection(ObservableCollection collection, List newItems) - { - var updatedKeys = newItems.Select(v => v.Id).ToHashSet(); - for (int i = collection.Count - 1; i >= 0; i--) - { - if (!updatedKeys.Contains(collection[i].Id)) - collection.RemoveAt(i); - } - - var currentKeys = collection.Select(v => v.Id).ToHashSet(); - foreach (var newItem in newItems) - { - if (!currentKeys.Contains(newItem.Id)) - { - // Find the correct index to maintain alphabetical order - int index = 0; - while (index < collection.Count && string.Compare(collection[index].Name, newItem.Name, StringComparison.CurrentCultureIgnoreCase) < 0) - { - index++; - } - collection.Insert(index, newItem); - } - } - } } \ No newline at end of file diff --git a/UI/ViewModels/TagItemViewModel.cs b/UI/ViewModels/TagItemViewModel.cs index cdf9088..74c93e3 100644 --- a/UI/ViewModels/TagItemViewModel.cs +++ b/UI/ViewModels/TagItemViewModel.cs @@ -150,6 +150,7 @@ public class TagItemViewModel(Tag tag, Func getNameById) : ViewMod OnPropertyChanged(nameof(EditingParents)); } + public void SyncChildren(List children) { var newChildren = children.Select(c => c.Id).ToHashSet(); diff --git a/UI/Views/MainWindow.axaml b/UI/Views/MainWindow.axaml index 707831c..23bad0d 100644 --- a/UI/Views/MainWindow.axaml +++ b/UI/Views/MainWindow.axaml @@ -47,7 +47,7 @@ BorderThickness="5" BorderBrush="Teal" VerticalAlignment="Stretch" - ItemsSource="{Binding TopLevelTags}" + ItemsSource="{Binding HierarchyTreeViewModel.TopLevelTags}" SelectedItem="{Binding SelectedTag}">