using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.UI.Text; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; using SQLVision.Core.Enums; using SQLVision.Core.Interfaces; using SQLVision.Core.Models; using SQLVision.Visualizers.Interfaces; using System.Text.Json; using Windows.Storage.Pickers; using WinRT.Interop; namespace SQLVision.Visualizers.Factories; public class ControlFactory : IControlFactory { private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; private readonly ISqlExecutionService? _executionService; public ControlFactory( IServiceProvider serviceProvider, ILogger logger) { _serviceProvider = serviceProvider; _logger = logger; _executionService = serviceProvider.GetService(); } public FrameworkElement CreateControl(ScriptParameter parameter, Action onValueChanged) { try { return parameter.Type switch { ParameterType.String => CreateTextBox(parameter, onValueChanged), ParameterType.Integer => CreateNumberBox(parameter, onValueChanged), ParameterType.Decimal => CreateNumberBox(parameter, onValueChanged, true), ParameterType.DateTime => CreateDatePicker(parameter, onValueChanged), ParameterType.Boolean => CreateCheckBox(parameter, onValueChanged), ParameterType.Table => CreateComboBox(parameter, onValueChanged), ParameterType.MultiSelect => CreateListView(parameter, onValueChanged), ParameterType.Color => CreateColorPicker(parameter, onValueChanged), ParameterType.File => CreateFilePicker(parameter, onValueChanged), ParameterType.Json => CreateJsonEditor(parameter, onValueChanged), _ => CreateTextBox(parameter, onValueChanged) }; } catch (Exception ex) { _logger.LogError(ex, "Error creating control for parameter {Parameter}", parameter.Name); return CreateFallbackControl(parameter, onValueChanged); } } private FrameworkElement CreateTextBox(ScriptParameter parameter, Action onValueChanged) { var stackPanel = new StackPanel { Spacing = 4 }; var header = new TextBlock { Text = parameter.EffectiveDisplayName, FontWeight = FontWeights.SemiBold }; var textBox = new TextBox { PlaceholderText = parameter.Watermark ?? $"Введите {parameter.EffectiveDisplayName.ToLower()}", Text = parameter.DefaultValue?.ToString() ?? string.Empty }; textBox.TextChanged += (s, e) => onValueChanged(textBox.Text); stackPanel.Children.Add(header); stackPanel.Children.Add(textBox); return stackPanel; } private FrameworkElement CreateNumberBox(ScriptParameter parameter, Action onValueChanged, bool isDecimal = false) { var stackPanel = new StackPanel { Spacing = 4 }; var header = new TextBlock { Text = parameter.EffectiveDisplayName, FontWeight = FontWeights.SemiBold }; var numberBox = new NumberBox { PlaceholderText = parameter.Watermark ?? $"Введите {parameter.EffectiveDisplayName.ToLower()}", SmallChange = isDecimal ? 0.1 : 1, LargeChange = isDecimal ? 1 : 10, SpinButtonPlacementMode = NumberBoxSpinButtonPlacementMode.Inline, AcceptsExpression = false }; if (isDecimal) { numberBox.Value = Convert.ToDouble(parameter.DefaultValue ?? 0); numberBox.ValueChanged += (s, e) => onValueChanged(e.NewValue); } else { numberBox.Value = Convert.ToInt32(parameter.DefaultValue ?? 0); numberBox.ValueChanged += (s, e) => onValueChanged((int)e.NewValue); } if (parameter.ValidationRules != null) { if (parameter.ValidationRules.TryGetValue("min", out var min)) numberBox.Minimum = Convert.ToDouble(min); if (parameter.ValidationRules.TryGetValue("max", out var max)) numberBox.Maximum = Convert.ToDouble(max); } stackPanel.Children.Add(header); stackPanel.Children.Add(numberBox); return stackPanel; } private FrameworkElement CreateDatePicker(ScriptParameter parameter, Action onValueChanged) { var stackPanel = new StackPanel { Spacing = 4 }; var header = new TextBlock { Text = parameter.EffectiveDisplayName, FontWeight = FontWeights.SemiBold }; var datePicker = new DatePicker { Date = parameter.DefaultValue is DateTime defaultDate ? new DateTimeOffset(defaultDate) : DateTimeOffset.Now }; datePicker.DateChanged += (s, e) => onValueChanged(e.NewDate.DateTime); if (parameter.ValidationRules != null) { if (parameter.ValidationRules.TryGetValue("mindate", out var minDate) && minDate is string minDateStr && DateTime.TryParse(minDateStr, out var minDateTime)) { datePicker.MinYear = new DateTimeOffset(minDateTime); } if (parameter.ValidationRules.TryGetValue("maxdate", out var maxDate) && maxDate is string maxDateStr && DateTime.TryParse(maxDateStr, out var maxDateTime)) { datePicker.MaxYear = new DateTimeOffset(maxDateTime); } } stackPanel.Children.Add(header); stackPanel.Children.Add(datePicker); return stackPanel; } private FrameworkElement CreateCheckBox(ScriptParameter parameter, Action onValueChanged) { var checkBox = new CheckBox { Content = parameter.EffectiveDisplayName, IsChecked = parameter.DefaultValue is bool defaultBool ? defaultBool : false }; checkBox.Checked += (s, e) => onValueChanged(true); checkBox.Unchecked += (s, e) => onValueChanged(false); return checkBox; } private FrameworkElement CreateComboBox(ScriptParameter parameter, Action onValueChanged) { var stackPanel = new StackPanel { Spacing = 4 }; var header = new TextBlock { Text = parameter.EffectiveDisplayName, FontWeight = FontWeights.SemiBold }; var comboBox = new ComboBox { PlaceholderText = parameter.Watermark ?? $"Выберите {parameter.EffectiveDisplayName.ToLower()}", DisplayMemberPath = parameter.DisplayMember, SelectedValuePath = parameter.ValueMember }; LoadComboBoxDataAsync(comboBox, parameter).ConfigureAwait(false); comboBox.SelectionChanged += (s, e) => { if (comboBox.SelectedValue != null) onValueChanged(comboBox.SelectedValue); }; stackPanel.Children.Add(header); stackPanel.Children.Add(comboBox); return stackPanel; } private async Task LoadComboBoxDataAsync(ComboBox comboBox, ScriptParameter parameter) { if (string.IsNullOrEmpty(parameter.TableQuery) || _executionService == null) return; try { var connectionString = "Server=localhost;Database=master;Trusted_Connection=True;"; var dataTable = await _executionService.LoadComboBoxDataAsync( parameter.TableQuery, connectionString, DatabaseProvider.SqlServer); await comboBox.DispatcherQueue.EnqueueAsync(() => { comboBox.ItemsSource = dataTable.DefaultView; if (parameter.DefaultValue != null) { comboBox.SelectedValue = parameter.DefaultValue; } }); } catch (Exception ex) { _logger.LogError(ex, "Error loading combo box data for {Parameter}", parameter.Name); await comboBox.DispatcherQueue.EnqueueAsync(() => { comboBox.ItemsSource = new List { "Ошибка загрузки данных" }; comboBox.IsEnabled = false; }); } } private FrameworkElement CreateListView(ScriptParameter parameter, Action onValueChanged) { var stackPanel = new StackPanel { Spacing = 4 }; var header = new TextBlock { Text = parameter.EffectiveDisplayName, FontWeight = FontWeights.SemiBold }; var listView = new ListView { SelectionMode = ListViewSelectionMode.Multiple }; // TODO: Загрузка данных для ListView listView.SelectionChanged += (s, e) => { var selectedItems = new List(); foreach (var item in listView.SelectedItems) { selectedItems.Add(item); } onValueChanged(selectedItems); }; stackPanel.Children.Add(header); stackPanel.Children.Add(listView); return stackPanel; } private FrameworkElement CreateColorPicker(ScriptParameter parameter, Action onValueChanged) { var stackPanel = new StackPanel { Spacing = 4 }; var header = new TextBlock { Text = parameter.EffectiveDisplayName, FontWeight = FontWeights.SemiBold }; var colorPicker = new ColorPicker { ColorSpectrumShape = ColorSpectrumShape.Box, IsMoreButtonVisible = true, IsColorSliderVisible = true, IsColorChannelTextInputVisible = true, IsHexInputVisible = true }; // Установка цвета по умолчанию с нашей реализацией ParseColor if (parameter.DefaultValue is string defaultColor) { try { colorPicker.Color = ParseColor(defaultColor); } catch { // Если не удалось распарсить, оставляем цвет по умолчанию _logger.LogWarning("Failed to parse color: {Color}", defaultColor); } } colorPicker.ColorChanged += (s, e) => onValueChanged($"#{e.NewColor.R:X2}{e.NewColor.G:X2}{e.NewColor.B:X2}"); stackPanel.Children.Add(header); stackPanel.Children.Add(colorPicker); return stackPanel; } // Наша собственная реализация парсинга цвета (без Microsoft.Toolkit.Uwp) private Windows.UI.Color ParseColor(string colorString) { if (string.IsNullOrEmpty(colorString)) return Windows.UI.Colors.Black; // Удаляем # colorString = colorString.Trim().TrimStart('#'); try { if (colorString.Length == 6) { // RRGGBB var r = Convert.ToByte(colorString.Substring(0, 2), 16); var g = Convert.ToByte(colorString.Substring(2, 2), 16); var b = Convert.ToByte(colorString.Substring(4, 2), 16); return Windows.UI.Color.FromArgb(255, r, g, b); } else if (colorString.Length == 8) { // AARRGGBB var a = Convert.ToByte(colorString.Substring(0, 2), 16); var r = Convert.ToByte(colorString.Substring(2, 2), 16); var g = Convert.ToByte(colorString.Substring(4, 2), 16); var b = Convert.ToByte(colorString.Substring(6, 2), 16); return Windows.UI.Color.FromArgb(a, r, g, b); } else if (colorString.Length == 3) { // RGB var r = Convert.ToByte(new string(colorString[0], 2), 16); var g = Convert.ToByte(new string(colorString[1], 2), 16); var b = Convert.ToByte(new string(colorString[2], 2), 16); return Windows.UI.Color.FromArgb(255, r, g, b); } } catch (Exception ex) { _logger.LogError(ex, "Failed to parse color string: {ColorString}", colorString); } return Windows.UI.Colors.Black; } private FrameworkElement CreateFilePicker(ScriptParameter parameter, Action onValueChanged) { var stackPanel = new StackPanel { Spacing = 4 }; var header = new TextBlock { Text = parameter.EffectiveDisplayName, FontWeight = FontWeights.SemiBold }; var contentStack = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 }; var textBox = new TextBox { PlaceholderText = "Путь к файлу", Width = 200, IsReadOnly = true }; var button = new Button { Content = "Выбрать" }; button.Click += async (s, e) => { // Получаем активное окно для инициализации FileOpenPicker var window = Application.Current as App; var hwnd = IntPtr.Zero; if (window?.MainWindow != null) { hwnd = WindowNative.GetWindowHandle(window.MainWindow); } var openPicker = new FileOpenPicker(); if (hwnd != IntPtr.Zero) { InitializeWithWindow.Initialize(openPicker, hwnd); } openPicker.ViewMode = PickerViewMode.List; openPicker.SuggestedStartLocation = PickerLocationId.DocumentsLibrary; if (parameter.ValidationRules != null && parameter.ValidationRules.TryGetValue("extensions", out var extensions)) { foreach (var ext in extensions.ToString()!.Split(',')) { openPicker.FileTypeFilter.Add(ext.Trim()); } } else { openPicker.FileTypeFilter.Add("*"); } var file = await openPicker.PickSingleFileAsync(); if (file != null) { textBox.Text = file.Path; onValueChanged(file.Path); } }; contentStack.Children.Add(textBox); contentStack.Children.Add(button); stackPanel.Children.Add(header); stackPanel.Children.Add(contentStack); return stackPanel; } private FrameworkElement CreateJsonEditor(ScriptParameter parameter, Action onValueChanged) { var stackPanel = new StackPanel { Spacing = 4 }; var header = new TextBlock { Text = parameter.EffectiveDisplayName, FontWeight = FontWeights.SemiBold }; var textBox = new TextBox { PlaceholderText = "Введите JSON", AcceptsReturn = true, TextWrapping = TextWrapping.Wrap, Height = 100, FontFamily = new FontFamily("Consolas") }; if (parameter.DefaultValue != null) { try { textBox.Text = JsonSerializer.Serialize( parameter.DefaultValue, new JsonSerializerOptions { WriteIndented = true }); } catch { textBox.Text = parameter.DefaultValue.ToString(); } } textBox.TextChanged += (s, e) => { try { var json = JsonSerializer.Deserialize(textBox.Text); onValueChanged(json); } catch { // Игнорируем ошибки парсинга JSON } }; stackPanel.Children.Add(header); stackPanel.Children.Add(textBox); return stackPanel; } private FrameworkElement CreateFallbackControl(ScriptParameter parameter, Action onValueChanged) { var stackPanel = new StackPanel { Spacing = 4 }; var header = new TextBlock { Text = parameter.EffectiveDisplayName, FontWeight = FontWeights.SemiBold }; var textBox = new TextBox { Text = $"Ошибка создания контрола для типа {parameter.Type}", IsReadOnly = true, Foreground = new SolidColorBrush(Windows.UI.Colors.Red) }; stackPanel.Children.Add(header); stackPanel.Children.Add(textBox); return stackPanel; } public void UpdateControlState(FrameworkElement control, ScriptParameter parameter, Dictionary currentValues) { // Обновление состояния контрола на основе зависимостей if (!string.IsNullOrEmpty(parameter.DependsOn)) { var isEnabled = CheckDependency(parameter, currentValues); control.IsEnabled = isEnabled; if (!isEnabled) { ResetControlValue(control); } } } private bool CheckDependency(ScriptParameter parameter, Dictionary currentValues) { if (!currentValues.TryGetValue(parameter.DependsOn, out var dependencyValue)) return false; if (parameter.DependencyValues != null) { return parameter.DependencyValues .Any(kvp => object.Equals(kvp.Value, dependencyValue)); } return dependencyValue != null && !string.IsNullOrWhiteSpace(dependencyValue.ToString()); } private void ResetControlValue(FrameworkElement control) { // Ищем первый дочерний элемент нужного типа var child = FindChildOfType(control) ?? FindChildOfType(control) ?? FindChildOfType(control) ?? FindChildOfType(control) ?? FindChildOfType(control); switch (child) { case TextBox textBox: textBox.Text = string.Empty; break; case ComboBox comboBox: comboBox.SelectedIndex = -1; break; case CheckBox checkBox: checkBox.IsChecked = false; break; case DatePicker datePicker: datePicker.Date = DateTimeOffset.Now; break; case NumberBox numberBox: numberBox.Value = 0; break; } } private T? FindChildOfType(DependencyObject parent) where T : DependencyObject { var queue = new Queue(); queue.Enqueue(parent); while (queue.Count > 0) { var current = queue.Dequeue(); if (current is T result) return result; for (int i = 0; i < VisualTreeHelper.GetChildrenCount(current); i++) { queue.Enqueue(VisualTreeHelper.GetChild(current, i)); } } return null; } }