ContentTreeViewModel.cs
1 // Copyright (c) Microsoft Corporation 2 // The Microsoft Corporation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System.Collections.ObjectModel; 6 using Microsoft.CmdPal.Core.ViewModels; 7 using Microsoft.CmdPal.Core.ViewModels.Models; 8 using Microsoft.CommandPalette.Extensions; 9 using Microsoft.CommandPalette.Extensions.Toolkit; 10 11 namespace Microsoft.CmdPal.UI.ViewModels; 12 13 public partial class ContentTreeViewModel(ITreeContent _tree, WeakReference<IPageContext> context) : 14 ContentViewModel(context) 15 { 16 public ExtensionObject<ITreeContent> Model { get; } = new(_tree); 17 18 // Remember - "observable" properties from the model (via PropChanged) 19 // cannot be marked [ObservableProperty] 20 public ContentViewModel? RootContent { get; protected set; } 21 22 public ObservableCollection<ContentViewModel> Children { get; } = []; 23 24 public bool HasChildren => Children.Count > 0; 25 26 // This is the content that's actually bound in XAML. We needed a 27 // collection, even if the collection is just a single item. 28 public ObservableCollection<ContentViewModel> Root => RootContent is not null ? [RootContent] : []; 29 30 public override void InitializeProperties() 31 { 32 var model = Model.Unsafe; 33 if (model is null) 34 { 35 return; 36 } 37 38 var root = model.RootContent; 39 if (root is not null) 40 { 41 RootContent = ViewModelFromContent(root, PageContext); 42 RootContent?.InitializeProperties(); 43 UpdateProperty(nameof(RootContent)); 44 UpdateProperty(nameof(Root)); 45 } 46 47 FetchContent(); 48 model.PropChanged += Model_PropChanged; 49 model.ItemsChanged += Model_ItemsChanged; 50 } 51 52 // Theoretically, we should unify this with the one in CommandPalettePageViewModelFactory 53 // and maybe just have a ContentViewModelFactory or something 54 public ContentViewModel? ViewModelFromContent(IContent content, WeakReference<IPageContext> context) 55 { 56 ContentViewModel? viewModel = content switch 57 { 58 IFormContent form => new ContentFormViewModel(form, context), 59 IMarkdownContent markdown => new ContentMarkdownViewModel(markdown, context), 60 ITreeContent tree => new ContentTreeViewModel(tree, context), 61 _ => null, 62 }; 63 return viewModel; 64 } 65 66 // TODO: Does this need to hop to a _different_ thread, so that we don't block the extension while we're fetching? 67 private void Model_ItemsChanged(object sender, IItemsChangedEventArgs args) => FetchContent(); 68 69 private void Model_PropChanged(object sender, IPropChangedEventArgs args) 70 { 71 try 72 { 73 var propName = args.PropertyName; 74 FetchProperty(propName); 75 } 76 catch (Exception ex) 77 { 78 ShowException(ex); 79 } 80 } 81 82 protected void FetchProperty(string propertyName) 83 { 84 var model = Model.Unsafe; 85 if (model is null) 86 { 87 return; // throw? 88 } 89 90 switch (propertyName) 91 { 92 case nameof(RootContent): 93 var root = model.RootContent; 94 if (root is not null) 95 { 96 RootContent = ViewModelFromContent(root, PageContext); 97 } 98 else 99 { 100 root = null; 101 } 102 103 UpdateProperty(nameof(Root)); 104 105 break; 106 } 107 108 UpdateProperty(propertyName); 109 } 110 111 //// Run on background thread, from InitializeAsync or Model_ItemsChanged 112 private void FetchContent() 113 { 114 List<ContentViewModel> newContent = []; 115 try 116 { 117 var newItems = Model.Unsafe!.GetChildren(); 118 119 foreach (var item in newItems) 120 { 121 var viewModel = ViewModelFromContent(item, PageContext); 122 if (viewModel is not null) 123 { 124 viewModel.InitializeProperties(); 125 newContent.Add((ContentViewModel)viewModel); 126 } 127 } 128 } 129 catch (Exception ex) 130 { 131 ShowException(ex); 132 throw; 133 } 134 135 // Now, back to a UI thread to update the observable collection 136 DoOnUiThread( 137 () => 138 { 139 ListHelpers.InPlaceUpdateList(Children, newContent); 140 }); 141 142 UpdateProperty(nameof(HasChildren)); 143 } 144 145 protected override void UnsafeCleanup() 146 { 147 base.UnsafeCleanup(); 148 RootContent?.SafeCleanup(); 149 foreach (var item in Children) 150 { 151 item.SafeCleanup(); 152 } 153 154 Children.Clear(); 155 var model = Model.Unsafe; 156 if (model is not null) 157 { 158 model.PropChanged -= Model_PropChanged; 159 model.ItemsChanged -= Model_ItemsChanged; 160 } 161 } 162 }