/ src / modules / cmdpal / Microsoft.CmdPal.UI.ViewModels / ContentTreeViewModel.cs
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  }