/ src / modules / cmdpal / Core / Microsoft.CmdPal.Core.ViewModels / ContentPageViewModel.cs
ContentPageViewModel.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 System.Diagnostics.CodeAnalysis;
  7  using CommunityToolkit.Mvvm.ComponentModel;
  8  using CommunityToolkit.Mvvm.Input;
  9  using CommunityToolkit.Mvvm.Messaging;
 10  using Microsoft.CmdPal.Core.ViewModels.Messages;
 11  using Microsoft.CmdPal.Core.ViewModels.Models;
 12  using Microsoft.CommandPalette.Extensions;
 13  using Microsoft.CommandPalette.Extensions.Toolkit;
 14  
 15  namespace Microsoft.CmdPal.Core.ViewModels;
 16  
 17  public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
 18  {
 19      private readonly ExtensionObject<IContentPage> _model;
 20  
 21      [ObservableProperty]
 22      public partial ObservableCollection<ContentViewModel> Content { get; set; } = [];
 23  
 24      public List<IContextItemViewModel> Commands { get; private set; } = [];
 25  
 26      public bool HasCommands => ActualCommands.Count > 0;
 27  
 28      public DetailsViewModel? Details { get; private set; }
 29  
 30      [MemberNotNullWhen(true, nameof(Details))]
 31      public bool HasDetails => Details is not null;
 32  
 33      /////// ICommandBarContext ///////
 34      public IEnumerable<IContextItemViewModel> MoreCommands => Commands.Skip(1);
 35  
 36      private List<CommandContextItemViewModel> ActualCommands => Commands.OfType<CommandContextItemViewModel>().ToList();
 37  
 38      public bool HasMoreCommands => ActualCommands.Count > 1;
 39  
 40      public string SecondaryCommandName => SecondaryCommand?.Name ?? string.Empty;
 41  
 42      public CommandItemViewModel? PrimaryCommand => HasCommands ? ActualCommands[0] : null;
 43  
 44      public CommandItemViewModel? SecondaryCommand => HasMoreCommands ? ActualCommands[1] : null;
 45  
 46      public List<IContextItemViewModel> AllCommands => Commands;
 47  
 48      // Remember - "observable" properties from the model (via PropChanged)
 49      // cannot be marked [ObservableProperty]
 50      public ContentPageViewModel(IContentPage model, TaskScheduler scheduler, AppExtensionHost host)
 51          : base(model, scheduler, host)
 52      {
 53          _model = new(model);
 54      }
 55  
 56      // TODO: Does this need to hop to a _different_ thread, so that we don't block the extension while we're fetching?
 57      private void Model_ItemsChanged(object sender, IItemsChangedEventArgs args) => FetchContent();
 58  
 59      //// Run on background thread, from InitializeAsync or Model_ItemsChanged
 60      private void FetchContent()
 61      {
 62          List<ContentViewModel> newContent = [];
 63          try
 64          {
 65              var newItems = _model.Unsafe!.GetContent();
 66  
 67              foreach (var item in newItems)
 68              {
 69                  var viewModel = ViewModelFromContent(item, PageContext);
 70                  if (viewModel is not null)
 71                  {
 72                      viewModel.InitializeProperties();
 73                      newContent.Add(viewModel);
 74                  }
 75              }
 76          }
 77          catch (Exception ex)
 78          {
 79              ShowException(ex, _model?.Unsafe?.Name);
 80              throw;
 81          }
 82  
 83          var oneContent = newContent.Count == 1;
 84          newContent.ForEach(c => c.OnlyControlOnPage = oneContent);
 85  
 86          // Now, back to a UI thread to update the observable collection
 87          DoOnUiThread(
 88          () =>
 89          {
 90              ListHelpers.InPlaceUpdateList(Content, newContent);
 91          });
 92      }
 93  
 94      public virtual ContentViewModel? ViewModelFromContent(IContent content, WeakReference<IPageContext> context)
 95      {
 96          // The core ContentPageViewModel doesn't actually handle any content,
 97          // so we just return null here.
 98          // The real content is handled by the derived class CommandPaletteContentPageViewModel
 99          return null;
100      }
101  
102      public override void InitializeProperties()
103      {
104          base.InitializeProperties();
105  
106          var model = _model.Unsafe;
107          if (model is null)
108          {
109              return; // throw?
110          }
111  
112          Commands = model.Commands
113                  .ToList()
114                  .Select<IContextItem, IContextItemViewModel>(item =>
115                  {
116                      if (item is ICommandContextItem contextItem)
117                      {
118                          return new CommandContextItemViewModel(contextItem, PageContext);
119                      }
120                      else
121                      {
122                          return new SeparatorViewModel();
123                      }
124                  })
125                  .ToList();
126  
127          Commands
128              .OfType<CommandContextItemViewModel>()
129              .ToList()
130              .ForEach(contextItem =>
131              {
132                  contextItem.InitializeProperties();
133              });
134  
135          var extensionDetails = model.Details;
136          if (extensionDetails is not null)
137          {
138              Details = new(extensionDetails, PageContext);
139              Details.InitializeProperties();
140          }
141  
142          UpdateDetails();
143  
144          FetchContent();
145          model.ItemsChanged += Model_ItemsChanged;
146  
147          DoOnUiThread(
148          () =>
149          {
150              WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(this));
151          });
152      }
153  
154      protected override void FetchProperty(string propertyName)
155      {
156          base.FetchProperty(propertyName);
157  
158          var model = this._model.Unsafe;
159          if (model is null)
160          {
161              return; // throw?
162          }
163  
164          switch (propertyName)
165          {
166              case nameof(Commands):
167  
168                  var more = model.Commands;
169                  if (more is not null)
170                  {
171                      var newContextMenu = more
172                              .ToList()
173                              .Select(item =>
174                              {
175                                  if (item is ICommandContextItem contextItem)
176                                  {
177                                      return new CommandContextItemViewModel(contextItem, PageContext) as IContextItemViewModel;
178                                  }
179                                  else
180                                  {
181                                      return new SeparatorViewModel();
182                                  }
183                              })
184                              .ToList();
185  
186                      lock (Commands)
187                      {
188                          ListHelpers.InPlaceUpdateList(Commands, newContextMenu);
189                      }
190  
191                      Commands
192                          .OfType<CommandContextItemViewModel>()
193                          .ToList()
194                          .ForEach(contextItem =>
195                          {
196                              contextItem.SlowInitializeProperties();
197                          });
198                  }
199                  else
200                  {
201                      Commands.Clear();
202                  }
203  
204                  UpdateProperty(nameof(PrimaryCommand));
205                  UpdateProperty(nameof(SecondaryCommand));
206                  UpdateProperty(nameof(SecondaryCommandName));
207                  UpdateProperty(nameof(HasCommands));
208                  UpdateProperty(nameof(HasMoreCommands));
209                  UpdateProperty(nameof(AllCommands));
210                  DoOnUiThread(
211                  () =>
212                  {
213                      WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(this));
214                  });
215  
216                  break;
217              case nameof(Details):
218                  var extensionDetails = model.Details;
219                  Details = extensionDetails is not null ? new(extensionDetails, PageContext) : null;
220                  UpdateDetails();
221                  break;
222          }
223  
224          UpdateProperty(propertyName);
225      }
226  
227      private void UpdateDetails()
228      {
229          UpdateProperty(nameof(Details));
230          UpdateProperty(nameof(HasDetails));
231  
232          DoOnUiThread(
233              () =>
234              {
235                  if (HasDetails)
236                  {
237                      WeakReferenceMessenger.Default.Send<ShowDetailsMessage>(new(Details));
238                  }
239                  else
240                  {
241                      WeakReferenceMessenger.Default.Send<HideDetailsMessage>();
242                  }
243              });
244      }
245  
246      // InvokeItemCommand is what this will be in Xaml due to source generator
247      // this comes in on Enter keypresses in the SearchBox
248      [RelayCommand]
249      private void InvokePrimaryCommand(ContentPageViewModel page)
250      {
251          if (PrimaryCommand is not null)
252          {
253              WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(PrimaryCommand.Command.Model, PrimaryCommand.Model));
254          }
255      }
256  
257      // this comes in on Ctrl+Enter keypresses in the SearchBox
258      [RelayCommand]
259      private void InvokeSecondaryCommand(ContentPageViewModel page)
260      {
261          if (SecondaryCommand is not null)
262          {
263              WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(SecondaryCommand.Command.Model, SecondaryCommand.Model));
264          }
265      }
266  
267      protected override void UnsafeCleanup()
268      {
269          base.UnsafeCleanup();
270  
271          Details?.SafeCleanup();
272  
273          Commands
274              .OfType<CommandContextItemViewModel>()
275              .ToList()
276              .ForEach(item => item.SafeCleanup());
277  
278          Commands.Clear();
279  
280          foreach (var item in Content)
281          {
282              item.SafeCleanup();
283          }
284  
285          Content.Clear();
286  
287          var model = _model.Unsafe;
288          if (model is not null)
289          {
290              model.ItemsChanged -= Model_ItemsChanged;
291          }
292      }
293  }