/ src / modules / cmdpal / Microsoft.CmdPal.UI / Controls / ContentFormControl.xaml.cs
ContentFormControl.xaml.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 AdaptiveCards.ObjectModel.WinUI3;
  6  using AdaptiveCards.Rendering.WinUI3;
  7  using Microsoft.CmdPal.UI.ViewModels;
  8  using Microsoft.UI.Xaml;
  9  using Microsoft.UI.Xaml.Controls;
 10  using Microsoft.UI.Xaml.Media;
 11  
 12  namespace Microsoft.CmdPal.UI.Controls;
 13  
 14  public sealed partial class ContentFormControl : UserControl
 15  {
 16      private static readonly AdaptiveCardRenderer _renderer;
 17      private ContentFormViewModel? _viewModel;
 18  
 19      // LOAD-BEARING: if you don't hang onto a reference to the RenderedAdaptiveCard
 20      // then the GC might clean it up sometime, even while the card is in the UI
 21      // tree. If this gets GC'ed, then it'll revoke our Action handler, and the
 22      // form will do seemingly nothing.
 23      private RenderedAdaptiveCard? _renderedCard;
 24  
 25      public ContentFormViewModel? ViewModel { get => _viewModel; set => AttachViewModel(value); }
 26  
 27      static ContentFormControl()
 28      {
 29          // We can't use `CardOverrideStyles` here yet, because we haven't called InitializeComponent once.
 30          // But also, the default value isn't `null` here. It's... some other default empty value.
 31          // So clear it out so that we know when the first time we get created is
 32          _renderer = new AdaptiveCardRenderer()
 33          {
 34              OverrideStyles = null,
 35          };
 36      }
 37  
 38      public ContentFormControl()
 39      {
 40          this.InitializeComponent();
 41          var lightTheme = ActualTheme == Microsoft.UI.Xaml.ElementTheme.Light;
 42          _renderer.HostConfig = lightTheme ? AdaptiveCardsConfig.Light : AdaptiveCardsConfig.Dark;
 43  
 44          // 5% BODGY: if we set this multiple times over the lifetime of the app,
 45          // then the second call will explode, because "CardOverrideStyles is already the child of another element".
 46          // SO only set this once.
 47          if (_renderer.OverrideStyles is null)
 48          {
 49              _renderer.OverrideStyles = CardOverrideStyles;
 50          }
 51  
 52          // TODO in the future, we should handle ActualThemeChanged and replace
 53          // our rendered card with one for that theme. But today is not that day
 54      }
 55  
 56      private void AttachViewModel(ContentFormViewModel? vm)
 57      {
 58          if (_viewModel is not null)
 59          {
 60              _viewModel.PropertyChanged -= ViewModel_PropertyChanged;
 61          }
 62  
 63          _viewModel = vm;
 64  
 65          if (_viewModel is not null)
 66          {
 67              _viewModel.PropertyChanged += ViewModel_PropertyChanged;
 68  
 69              var c = _viewModel.Card;
 70              if (c is not null)
 71              {
 72                  DisplayCard(c);
 73              }
 74          }
 75      }
 76  
 77      private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
 78      {
 79          if (ViewModel is null)
 80          {
 81              return;
 82          }
 83  
 84          if (e.PropertyName == nameof(ViewModel.Card))
 85          {
 86              var c = ViewModel.Card;
 87              if (c is not null)
 88              {
 89                  DisplayCard(c);
 90              }
 91          }
 92      }
 93  
 94      private void DisplayCard(AdaptiveCardParseResult result)
 95      {
 96          _renderedCard = _renderer.RenderAdaptiveCard(result.AdaptiveCard);
 97          ContentGrid.Children.Clear();
 98          if (_renderedCard.FrameworkElement is not null)
 99          {
100              ContentGrid.Children.Add(_renderedCard.FrameworkElement);
101  
102              // Use the Loaded event to ensure we focus after the card is in the visual tree
103              _renderedCard.FrameworkElement.Loaded += OnFrameworkElementLoaded;
104          }
105  
106          _renderedCard.Action += Rendered_Action;
107      }
108  
109      private void OnFrameworkElementLoaded(object sender, RoutedEventArgs e)
110      {
111          // Unhook the event handler to avoid multiple registrations
112          if (sender is FrameworkElement element)
113          {
114              element.Loaded -= OnFrameworkElementLoaded;
115  
116              if (!ViewModel?.OnlyControlOnPage ?? true)
117              {
118                  return;
119              }
120  
121              // Focus on the first focusable element asynchronously to ensure the visual tree is fully built
122              element.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, () =>
123              {
124                  var focusableElement = FindFirstFocusableElement(element);
125                  focusableElement?.Focus(FocusState.Programmatic);
126              });
127          }
128      }
129  
130      private Control? FindFirstFocusableElement(DependencyObject parent)
131      {
132          var childCount = VisualTreeHelper.GetChildrenCount(parent);
133  
134          // Process children first (depth-first search)
135          for (var i = 0; i < childCount; i++)
136          {
137              var child = VisualTreeHelper.GetChild(parent, i);
138  
139              // If the child is a focusable control like TextBox, ComboBox, etc.
140              if (child is Control control &&
141                  control.IsEnabled &&
142                  control.IsTabStop &&
143                  control.Visibility == Visibility.Visible &&
144                  control.AllowFocusOnInteraction)
145              {
146                  return control;
147              }
148  
149              // Recursively check children
150              var result = FindFirstFocusableElement(child);
151              if (result is not null)
152              {
153                  return result;
154              }
155          }
156  
157          return null;
158      }
159  
160      private void Rendered_Action(RenderedAdaptiveCard sender, AdaptiveActionEventArgs args) =>
161          ViewModel?.HandleSubmit(args.Action, args.Inputs.AsJson());
162  }