diff --git a/UI/ViewModels/HierarchyTreeViewModel.cs b/UI/ViewModels/HierarchyTreeViewModel.cs index c83aed6..1d40b5c 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; @@ -12,172 +13,149 @@ public partial class HierarchyTreeViewModel : ViewModelBase, IDisposable { private readonly MainWindowViewModel _mainWindow; private readonly Dictionary _viewModelMap = new(); - - [ObservableProperty] - private TagItemViewModel? _selectedTag; - partial void OnSelectedTagChanged(TagItemViewModel? value) - { - _mainWindow.SelectedTag = value; - } - - [ObservableProperty] - private ObservableCollection _topLevelTags = []; - + [ObservableProperty] private TagItemViewModel? _selectedTag; + + [ObservableProperty] private ObservableCollection _topLevelTags = []; + public HierarchyTreeViewModel(MainWindowViewModel mainWindow) { this._mainWindow = mainWindow; - // TODO proper methods here so it can be unsubscribed. this.SubscribeToEvents(); } - - // TODO unsubscribe, if necessary. - private void SubscribeToEvents() - { - this._mainWindow.Database.TagUpdated += TagDatabase_OnTagUpdated; - this._mainWindow.Database.TagAdded += TagDatabase_OnTagAdded; - this._mainWindow.Database.TagDeleted += TagDatabase_OnTagDeleted; - } - - private void TagDatabase_OnTagUpdated(object? sender, Tag _) => - Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(OnTreeUpdate); - - private void TagDatabase_OnTagAdded(object? sender, Tag _) => - Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(OnTreeUpdate); - - private void TagDatabase_OnTagDeleted(object? sender, (int id, string name) _) => - Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(OnTreeUpdate); public void Dispose() { - this._mainWindow.Database.TagUpdated -= TagDatabase_OnTagUpdated; - this._mainWindow.Database.TagAdded -= TagDatabase_OnTagAdded; - this._mainWindow.Database.TagDeleted -= TagDatabase_OnTagDeleted; - - _viewModelMap.Clear(); + this._mainWindow.Database.TagUpdated -= this.TagDatabase_OnTagUpdated; + this._mainWindow.Database.TagAdded -= this.TagDatabase_OnTagAdded; + this._mainWindow.Database.TagDeleted -= this.TagDatabase_OnTagDeleted; + + this._viewModelMap.Clear(); } - + public async Task InitializeAsync() { await this.SyncHierarchyAsync(); } - private async Task OnTreeUpdate() - { - 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 result = await Task.Run(() => - { - - 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(); + 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 (topLevelTags, children); - }); + return (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 + var topLevelViewModels = result.topLevelTags.Select(t => { - throw; // TODO handle exception - } + var vm = this.GetOrCreateViewModel(t, 0); + activeKeys.Add($"0_{t.Id}"); + this.SyncTagRecursive(vm, result.children, activeKeys); + return vm; + }).ToList(); + this.SyncCollection(this.TopLevelTags, topLevelViewModels); + + var keysToRemove = this._viewModelMap.Keys.Where(k => !activeKeys.Contains(k)).ToList(); + foreach (var key in keysToRemove) this._viewModelMap.Remove(key); } - + private TagItemViewModel GetOrCreateViewModel(Tag tag, int parentId) { - string key = $"{parentId}_{tag.Id}"; + var key = $"{parentId}_{tag.Id}"; if (!this._viewModelMap.TryGetValue(key, out var viewModel)) { - viewModel = new TagItemViewModel(tag, id => + viewModel = new TagItemViewModel(tag, id => this._viewModelMap.Values.FirstOrDefault(v => v.Id == id)?.Name); this._viewModelMap[key] = viewModel; viewModel.UserEditedTag += (s, e) => this._mainWindow.UnsavedChanges = true; } + return viewModel; } - - private void SyncTagRecursive(TagItemViewModel parentVm, ILookup childrenLookup, HashSet activeKeys) + + partial void OnSelectedTagChanged(TagItemViewModel? value) { - var childTags = childrenLookup[parentVm.Id].OrderBy(t => t.Name).ToList(); - var childVms = new List(); + this._mainWindow.SelectedTag = value; + } - foreach (var ct in childTags) + private async Task OnTreeUpdate() + { + await Task.Run(() => { - var key = $"{parentVm.Id}_{ct.Id}"; - activeKeys.Add(key); - - var childVm = GetOrCreateViewModel(ct, parentVm.Id); - childVms.Add(childVm); - - SyncTagRecursive(childVm, childrenLookup, activeKeys); - } + foreach (var viewModel in this._viewModelMap.Values) viewModel.RefreshParentsString(); + }); + await this.SyncHierarchyAsync(); + } - parentVm.SyncChildren(childVms); + private void SubscribeToEvents() + { + this._mainWindow.Database.TagUpdated += this.TagDatabase_OnTagUpdated; + this._mainWindow.Database.TagAdded += this.TagDatabase_OnTagAdded; + this._mainWindow.Database.TagDeleted += this.TagDatabase_OnTagDeleted; } - + private void SyncCollection(ObservableCollection collection, List newItems) { var updatedKeys = newItems.Select(v => v.Id).ToHashSet(); - for (int i = collection.Count - 1; i >= 0; i--) - { + for (var 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++; - } + var index = 0; + while (index < collection.Count && string.Compare(collection[index].Name, newItem.Name, + StringComparison.CurrentCultureIgnoreCase) < 0) index++; collection.Insert(index, newItem); } + } + + 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 = this.GetOrCreateViewModel(ct, parentVm.Id); + childVms.Add(childVm); + + this.SyncTagRecursive(childVm, childrenLookup, activeKeys); } + + parentVm.SyncChildren(childVms); + } + + private void TagDatabase_OnTagAdded(object? sender, Tag _) + { + Dispatcher.UIThread.InvokeAsync(this.OnTreeUpdate); + } + + private void TagDatabase_OnTagDeleted(object? sender, (int id, string name) _) + { + Dispatcher.UIThread.InvokeAsync(this.OnTreeUpdate); + } + + private void TagDatabase_OnTagUpdated(object? sender, Tag _) + { + Dispatcher.UIThread.InvokeAsync(this.OnTreeUpdate); } } \ No newline at end of file diff --git a/UI/ViewModels/MainWindowViewModel.cs b/UI/ViewModels/MainWindowViewModel.cs index 60f5349..9ebef60 100644 --- a/UI/ViewModels/MainWindowViewModel.cs +++ b/UI/ViewModels/MainWindowViewModel.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using TagHierarchyManager.Common; using TagHierarchyManager.Exporters; @@ -19,74 +20,104 @@ public partial class MainWindowViewModel : ViewModelBase { // TODO consider moving editor-related functionality to a separate ViewModel. internal TagDatabase Database; - - [ObservableProperty] - private bool _unsavedChanges; - + + [ObservableProperty] private HierarchyTreeViewModel _hierarchyTreeViewModel; + + [ObservableProperty] private bool _isDbLoaded; + private bool _isSwitching; - - [ObservableProperty] - private HierarchyTreeViewModel _hierarchyTreeViewModel; - + // Since multiple view models will be using this tag, best to store it here as the authoritative source. private TagItemViewModel? _selectedTag; + [ObservableProperty] private string _statusBlockText = Resources.StatusBlockReady; + + [ObservableProperty] private ObservableCollection _topLevelTags = []; + + [ObservableProperty] private bool _unsavedChanges; + + // TODO searchViewModel + + public int TotalTags => this.Database?.Tags.Count ?? 0; + + public string WindowTitle => + this.IsDbLoaded + ? string.Format(Resources.TitleWithDatabase, this.Database.Name) + : Resources.Title; + public TagItemViewModel? SelectedTag { - get => _selectedTag; + get => this._selectedTag; set { - if (_selectedTag == value || _isSwitching) return; - if (_selectedTag != null && this.UnsavedChanges) + if (this._selectedTag == value || this._isSwitching) return; + if (this._selectedTag != null && this.UnsavedChanges) { _ = this.HandleTagSwitchAsync(this._selectedTag, value); } else { - _selectedTag = value; - HierarchyTreeViewModel.SelectedTag = value; - _selectedTag?.BeginEdit(); + this._selectedTag = value; + this.HierarchyTreeViewModel.SelectedTag = value; + this._selectedTag?.BeginEdit(); this.UnsavedChanges = false; this.OnPropertyChanged(); } } } - - [ObservableProperty] - private ObservableCollection _topLevelTags = []; - [ObservableProperty] - private bool _isDbLoaded; - - public int TotalTags => this.Database?.Tags.Count ?? 0; - - public string WindowTitle => IsDbLoaded - ? string.Format(Resources.TitleWithDatabase, this.Database.Name) - : Resources.Title; - - [ObservableProperty] - private string _statusBlockText = Resources.StatusBlockReady; - - // TODO searchViewModel - - public MainWindowViewModel() + public async Task CreateNewDatabase(string filePath) { + this.IsDbLoaded = false; + TagDatabase db = new(); + db.InitialisationComplete += this.TagDatabase_OnInitalisationComplete; + // overwrite is set to true here for now since the OS should handle the overwrite request. + // will need to remove once Terminal.Gui is replaced. + await db.CreateAsync(filePath, true); } - public async Task ShowNullableBoolDialog(Window dialog) + public async Task ExportAsync(string path) { - if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop) - return null; - - var mainWindow = desktop.MainWindow; - var result = await dialog.ShowDialog(mainWindow); - return result; + var exporter = PickExporterFromFileExt(path); + this.StatusBlockText = "Exporting..."; + var exportContent = exporter.ExportDatabase(this.Database); + await File.WriteAllTextAsync(path, exportContent); + this.StatusBlockText = "Export complete."; } - - public async Task ShowUnsavedChangesDialog() + + public async Task LoadDatabase(string filePath) { - var dialog = new UnsavedChangesDialog(); + this.IsDbLoaded = false; + TagDatabase db = new(); + db.InitialisationComplete += this.TagDatabase_OnInitalisationComplete; + await db.LoadAsync(filePath); + } + + public void NewTag() + { + this.SelectedTag = new TagItemViewModel( + new Tag + { + Name = string.Empty, + IsTopLevel = true + } + ); + this.SelectedTag.BeginEdit(); + this.UnsavedChanges = true; + } + public async Task SaveSelectedTagAsync() + { + if (this.SelectedTag is null || this.Database is null) return; + + this.SelectedTag.CommitEdit(); + await this.Database.WriteTagToDatabase(this.SelectedTag.Tag); + this.StatusBlockText = $"Successfully saved tag {this.SelectedTag.Name}"; + this.UnsavedChanges = false; + } + + public async Task ShowNullableBoolDialog(Window dialog) + { if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop) return null; @@ -101,137 +132,91 @@ public partial class MainWindowViewModel : ViewModelBase if (result == null) return; - if (result == true) - { - await DeleteSelectedTagAsync(); - } + if (result == true) await this.DeleteSelectedTagAsync(); } private static IExporter PickExporterFromFileExt(string path) { - string fileExt = Path.GetExtension(path); + var fileExt = Path.GetExtension(path); // ReSharper disable once ConvertIfStatementToReturnStatement if (fileExt == FileTypes.MusicBeeTagHierarchyTemplate.FileExtension) return new MusicBeeTagHierarchyExporter(); throw new NotSupportedException($"File extension '{fileExt}' is not supported."); } - - public async Task ExportAsync(string path) - { - IExporter exporter = PickExporterFromFileExt(path); - this.StatusBlockText = "Exporting..."; - string exportContent = exporter.ExportDatabase(this.Database); - await File.WriteAllTextAsync(path, exportContent); - this.StatusBlockText = "Export complete."; - } - - private void TagDatabase_TagAdded(object? sender, Tag _) => this.OnPropertyChanged(nameof(TotalTags)); - private void TagDatabase_TagDeleted(object? sender, (int id, string name) _) => this.OnPropertyChanged(nameof(TotalTags)); private async Task DeleteSelectedTagAsync() { - if (SelectedTag is null || this.Database is null) return; - await this.Database.DeleteTag(SelectedTag.Tag.Id); - _selectedTag = null; - HierarchyTreeViewModel.SelectedTag = null; - this.OnPropertyChanged(nameof(SelectedTag)); + if (this.SelectedTag is null || this.Database is null) return; + await this.Database.DeleteTag(this.SelectedTag.Tag.Id); + this._selectedTag = null; + this.HierarchyTreeViewModel.SelectedTag = null; + this.OnPropertyChanged(nameof(this.SelectedTag)); } private async Task HandleTagSwitchAsync(TagItemViewModel? oldTag, TagItemViewModel? newTag) { - _isSwitching = true; + this._isSwitching = true; try { var result = await this.ShowNullableBoolDialog(new UnsavedChangesDialog()); if (result == null) { - _selectedTag = oldTag; - HierarchyTreeViewModel.SelectedTag = oldTag; - this.OnPropertyChanged(nameof(SelectedTag)); + this._selectedTag = oldTag; + this.HierarchyTreeViewModel.SelectedTag = oldTag; + this.OnPropertyChanged(nameof(this.SelectedTag)); return; } if (result == true) await this.SaveSelectedTagAsync(); - _selectedTag = newTag; - HierarchyTreeViewModel.SelectedTag = newTag; - _selectedTag?.BeginEdit(); - this.OnPropertyChanged(nameof(SelectedTag)); + this._selectedTag = newTag; + this.HierarchyTreeViewModel.SelectedTag = newTag; + this._selectedTag?.BeginEdit(); + this.OnPropertyChanged(nameof(this.SelectedTag)); this.UnsavedChanges = false; } finally { - _isSwitching = false; + this._isSwitching = false; } } - - public async Task LoadDatabase(string filePath) - { - this.IsDbLoaded = false; - TagDatabase db = new(); - db.InitialisationComplete += this.TagDatabase_OnInitalisationComplete; - await db.LoadAsync(filePath); - } - - public async Task CreateNewDatabase(string filePath) - { - this.IsDbLoaded = false; - TagDatabase db = new(); - db.InitialisationComplete += this.TagDatabase_OnInitalisationComplete; - // overwrite is set to true here for now since the OS should handle the overwrite request. - // will need to remove once Terminal.Gui is replaced. - await db.CreateAsync(filePath, true); - } - - public async Task SaveSelectedTagAsync() - { - if (SelectedTag is null || this.Database is null) return; - - SelectedTag.CommitEdit(); - await this.Database.WriteTagToDatabase(SelectedTag.Tag); - this.StatusBlockText = $"Successfully saved tag {SelectedTag.Name}"; - this.UnsavedChanges = false; - } - + private void TagDatabase_OnInitalisationComplete(object sender, EventArgs e) { if (sender is not TagDatabase db) return; - Avalonia.Threading.Dispatcher.UIThread.Post(async () => + Dispatcher.UIThread.Post(async () => { (this.HierarchyTreeViewModel as IDisposable)?.Dispose(); if (this.Database != null) { - this.Database.TagAdded -= TagDatabase_TagAdded; - this.Database.TagDeleted -= TagDatabase_TagDeleted; + this.Database.TagAdded -= this.TagDatabase_TagAdded; + this.Database.TagDeleted -= this.TagDatabase_TagDeleted; } - + this.Database = db; this.IsDbLoaded = true; this.HierarchyTreeViewModel = new HierarchyTreeViewModel(this); - this.Database.TagAdded += TagDatabase_TagAdded; - this.Database.TagDeleted += TagDatabase_TagDeleted; + this.Database.TagAdded += this.TagDatabase_TagAdded; + this.Database.TagDeleted += this.TagDatabase_TagDeleted; await this.HierarchyTreeViewModel.InitializeAsync(); - this.OnPropertyChanged(nameof(TotalTags)); - this.OnPropertyChanged(nameof(WindowTitle)); + this.OnPropertyChanged(nameof(this.TotalTags)); + this.OnPropertyChanged(nameof(this.WindowTitle)); this.Database.InitialisationComplete -= this.TagDatabase_OnInitalisationComplete; this.StatusBlockText = string.Format(Resources.StatusBlockDbLoadSuccessful, this.Database.Name); }); Debug.WriteLine($"Database loaded on UI - name: {db.Name}, version: {db.Version}"); } - public void NewTag() + private void TagDatabase_TagAdded(object? sender, Tag _) { - this.SelectedTag = new TagItemViewModel( - new() - { - Name = string.Empty, - IsTopLevel = true - } - ); - this.SelectedTag.BeginEdit(); - this.UnsavedChanges = true; + this.OnPropertyChanged(nameof(this.TotalTags)); + } + + private void TagDatabase_TagDeleted(object? sender, (int id, string name) _) + { + this.OnPropertyChanged(nameof(this.TotalTags)); } } \ No newline at end of file diff --git a/UI/ViewModels/TagItemViewModel.cs b/UI/ViewModels/TagItemViewModel.cs index 199481a..c669bb4 100644 --- a/UI/ViewModels/TagItemViewModel.cs +++ b/UI/ViewModels/TagItemViewModel.cs @@ -10,139 +10,126 @@ namespace TagHierarchyManager.UI.ViewModels; public partial class TagItemViewModel(Tag tag, Func? getNameById = null) : ViewModelBase { - internal Tag Tag { get; } = tag; + [ObservableProperty] private string _editingAliases; - public int Id => Tag.Id; - - public string Name => Tag.Name; + [ObservableProperty] private bool _editingIsTopLevel; - [ObservableProperty] - private string _editingName = tag.Name; + [ObservableProperty] private string _editingName = tag.Name; - private string Parents => (getNameById != null && Tag.ParentIds.Count > 0) - ? string.Join("; ", Tag.ParentIds.Select(getNameById).Where(n => n != null)) - : string.Empty; - - [ObservableProperty] - private string _editingParents; - - public string TagBindings => Tag.TagBindings.Count > 0 - ? string.Join("; ", Tag.TagBindings) - : string.Empty; - - [ObservableProperty] - private string _editingTagBindings; - - public string Aliases => Tag.Aliases.Count > 0 - ? string.Join("; ", Tag.Aliases) - : string.Empty; - - [ObservableProperty] - private string _editingAliases; - - public string Notes => Tag.Notes; - - [ObservableProperty] - private string _editingNotes; - - private bool IsTopLevel => Tag.IsTopLevel; - [ObservableProperty] - private bool _editingIsTopLevel; - - - public ObservableCollection Children { get; } = []; + [ObservableProperty] private string _editingNotes; + + [ObservableProperty] private string _editingParents; + + [ObservableProperty] private string _editingTagBindings; private bool _isInitialising; - + public event EventHandler? UserEditedTag; - + + public string Aliases => + this.Tag.Aliases.Count > 0 + ? string.Join("; ", this.Tag.Aliases) + : string.Empty; + + + public ObservableCollection Children { get; } = []; + + public int Id => this.Tag.Id; + + public string Name => this.Tag.Name; + + public string Notes => this.Tag.Notes; + + public string TagBindings => + this.Tag.TagBindings.Count > 0 + ? string.Join("; ", this.Tag.TagBindings) + : string.Empty; + + internal Tag Tag { get; } = tag; + + private bool IsTopLevel => this.Tag.IsTopLevel; + + private string Parents => getNameById != null && this.Tag.ParentIds.Count > 0 + ? string.Join("; ", this.Tag.ParentIds.Select(getNameById).Where(n => n != null)) + : string.Empty; + public void BeginEdit() { - _isInitialising = true; - EditingName = Tag.Name; - this.EditingParents = Parents; - this.EditingIsTopLevel = IsTopLevel; - this.EditingTagBindings = TagBindings; - this.EditingAliases = Aliases; - this.EditingNotes = Notes; - _isInitialising = false; - } - - protected override void OnPropertyChanged(PropertyChangedEventArgs e) - { - base.OnPropertyChanged(e); - - if (!_isInitialising && e.PropertyName.StartsWith("Editing")) - { - UserEditedTag?.Invoke(this, EventArgs.Empty); - } + this._isInitialising = true; + this.EditingName = this.Tag.Name; + this.EditingParents = this.Parents; + this.EditingIsTopLevel = this.IsTopLevel; + this.EditingTagBindings = this.TagBindings; + this.EditingAliases = this.Aliases; + this.EditingNotes = this.Notes; + this._isInitialising = false; } public void CommitEdit() { - Tag.Name = EditingName; - Tag.Parents = !string.IsNullOrWhiteSpace(EditingParents) - ? EditingParents.Split(';', StringSplitOptions.RemoveEmptyEntries | - StringSplitOptions.TrimEntries) + this.Tag.Name = this.EditingName; + this.Tag.Parents = !string.IsNullOrWhiteSpace(this.EditingParents) + ? this.EditingParents.Split(';', StringSplitOptions.RemoveEmptyEntries | + StringSplitOptions.TrimEntries) .ToList() : []; - Tag.TagBindings = !string.IsNullOrWhiteSpace(EditingTagBindings) - ? EditingTagBindings.Split(';', StringSplitOptions.RemoveEmptyEntries | - StringSplitOptions.TrimEntries) + this.Tag.TagBindings = !string.IsNullOrWhiteSpace(this.EditingTagBindings) + ? this.EditingTagBindings.Split(';', StringSplitOptions.RemoveEmptyEntries | + StringSplitOptions.TrimEntries) .ToList() : []; - Tag.Aliases = !string.IsNullOrWhiteSpace(EditingAliases) - ? EditingAliases.Split(';', StringSplitOptions.RemoveEmptyEntries | - StringSplitOptions.TrimEntries) + this.Tag.Aliases = !string.IsNullOrWhiteSpace(this.EditingAliases) + ? this.EditingAliases.Split(';', StringSplitOptions.RemoveEmptyEntries | + StringSplitOptions.TrimEntries) .ToList() : []; - Tag.Notes = !string.IsNullOrWhiteSpace(EditingNotes) ? EditingNotes : ""; - Tag.IsTopLevel = EditingIsTopLevel; - OnPropertyChanged(nameof(Name)); - OnPropertyChanged(nameof(Parents)); - OnPropertyChanged(nameof(Aliases)); - OnPropertyChanged(nameof(TagBindings)); - OnPropertyChanged(nameof(Notes)); - OnPropertyChanged(nameof(IsTopLevel)); - RefreshParentsString(); + this.Tag.Notes = !string.IsNullOrWhiteSpace(this.EditingNotes) ? this.EditingNotes : ""; + this.Tag.IsTopLevel = this.EditingIsTopLevel; + this.OnPropertyChanged(nameof(this.Name)); + this.OnPropertyChanged(nameof(this.Parents)); + this.OnPropertyChanged(nameof(this.Aliases)); + this.OnPropertyChanged(nameof(this.TagBindings)); + this.OnPropertyChanged(nameof(this.Notes)); + this.OnPropertyChanged(nameof(this.IsTopLevel)); + this.RefreshParentsString(); } public void RefreshParentsString() { - _isInitialising = true; - OnPropertyChanged(nameof(Parents)); - _editingParents = Parents; - OnPropertyChanged(nameof(EditingParents)); - _isInitialising = false; + this._isInitialising = true; + this.OnPropertyChanged(nameof(this.Parents)); + this._editingParents = this.Parents; + this.OnPropertyChanged(nameof(this.EditingParents)); + this._isInitialising = false; } - - + + public void SyncChildren(List children) { var newChildren = children.Select(c => c.Id).ToHashSet(); - - for (int i = Children.Count - 1; i >= 0; i--) - { - if (!newChildren.Contains(Children[i].Id)) - { - Children.RemoveAt(i); - } - } - var currentIds = Children.Select(c => c.Id).ToHashSet(); + for (var i = this.Children.Count - 1; i >= 0; i--) + if (!newChildren.Contains(this.Children[i].Id)) + this.Children.RemoveAt(i); + + var currentIds = this.Children.Select(c => c.Id).ToHashSet(); foreach (var child in children) - { if (!currentIds.Contains(child.Id)) { // Find the correct index to maintain alphabetical order - int index = 0; - while (index < Children.Count && string.Compare(Children[index].Name, child.Name, StringComparison.CurrentCultureIgnoreCase) < 0) - { - index++; - } - Children.Insert(index, child); + var index = 0; + while (index < this.Children.Count && string.Compare(this.Children[index].Name, child.Name, + StringComparison.CurrentCultureIgnoreCase) < 0) index++; + + this.Children.Insert(index, child); } - } - + } + + protected override void OnPropertyChanged(PropertyChangedEventArgs e) + { + base.OnPropertyChanged(e); + + if (!this._isInitialising && e.PropertyName.StartsWith("Editing")) + this.UserEditedTag?.Invoke(this, EventArgs.Empty); } } \ No newline at end of file diff --git a/UI/ViewModels/UnsavedChangesViewModel.cs b/UI/ViewModels/UnsavedChangesViewModel.cs deleted file mode 100644 index e3bad3e..0000000 --- a/UI/ViewModels/UnsavedChangesViewModel.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; - -namespace TagHierarchyManager.UI.ViewModels; - -public enum UnsavedChangesResult -{ - Cancel, - Save, - Discard -} - -public partial class UnsavedChangesViewModel : ViewModelBase -{ - public void Save() => CloseAction?.Invoke(UnsavedChangesResult.Save); - public void Discard() => CloseAction?.Invoke(UnsavedChangesResult.Discard); - public void Cancel() => CloseAction?.Invoke(UnsavedChangesResult.Cancel); - - public Action CloseAction { get; set; } - - public UnsavedChangesViewModel() - { - } -} \ No newline at end of file diff --git a/UI/Views/MainWindow.axaml.cs b/UI/Views/MainWindow.axaml.cs index b080d27..cb3dba1 100644 --- a/UI/Views/MainWindow.axaml.cs +++ b/UI/Views/MainWindow.axaml.cs @@ -126,7 +126,7 @@ public partial class MainWindow : Window if (this.ViewModel.UnsavedChanges) { e.Cancel = true; - var result = await this.ViewModel.ShowUnsavedChangesDialog(); + var result = await this.ViewModel.ShowNullableBoolDialog(new UnsavedChangesDialog()); switch (result) { case true: diff --git a/UI/Views/UnsavedChangesDialog.axaml b/UI/Views/UnsavedChangesDialog.axaml index 38c1a9a..006e45d 100644 --- a/UI/Views/UnsavedChangesDialog.axaml +++ b/UI/Views/UnsavedChangesDialog.axaml @@ -3,11 +3,9 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:assets="clr-namespace:TagHierarchyManager.UI.Assets" - xmlns:vm="clr-namespace:TagHierarchyManager.UI.ViewModels" mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="120" Width="500" Height="120" CanResize="False" x:Class="TagHierarchyManager.UI.Views.UnsavedChangesDialog" - x:DataType="vm:UnsavedChangesViewModel" WindowStartupLocation="CenterOwner" ShowInTaskbar="False" Title="">