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 }