599 lines
19 KiB
C#
599 lines
19 KiB
C#
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<ControlFactory> _logger;
|
||
private readonly ISqlExecutionService? _executionService;
|
||
|
||
public ControlFactory(
|
||
IServiceProvider serviceProvider,
|
||
ILogger<ControlFactory> logger)
|
||
{
|
||
_serviceProvider = serviceProvider;
|
||
_logger = logger;
|
||
|
||
_executionService = serviceProvider.GetService<ISqlExecutionService>();
|
||
}
|
||
|
||
public FrameworkElement CreateControl(ScriptParameter parameter, Action<object?> 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<object?> 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<object?> 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<object?> 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<object?> 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<object?> 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<string> { "Ошибка загрузки данных" };
|
||
comboBox.IsEnabled = false;
|
||
});
|
||
}
|
||
}
|
||
|
||
private FrameworkElement CreateListView(ScriptParameter parameter, Action<object?> 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<object>();
|
||
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<object?> 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<object?> 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<object?> 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<JsonElement>(textBox.Text);
|
||
onValueChanged(json);
|
||
}
|
||
catch
|
||
{
|
||
// Игнорируем ошибки парсинга JSON
|
||
}
|
||
};
|
||
|
||
stackPanel.Children.Add(header);
|
||
stackPanel.Children.Add(textBox);
|
||
|
||
return stackPanel;
|
||
}
|
||
|
||
private FrameworkElement CreateFallbackControl(ScriptParameter parameter, Action<object?> 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<string, object?> currentValues)
|
||
{
|
||
// Обновление состояния контрола на основе зависимостей
|
||
if (!string.IsNullOrEmpty(parameter.DependsOn))
|
||
{
|
||
var isEnabled = CheckDependency(parameter, currentValues);
|
||
control.IsEnabled = isEnabled;
|
||
|
||
if (!isEnabled)
|
||
{
|
||
ResetControlValue(control);
|
||
}
|
||
}
|
||
}
|
||
|
||
private bool CheckDependency(ScriptParameter parameter, Dictionary<string, object?> 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<TextBox>(control) ??
|
||
FindChildOfType<ComboBox>(control) ??
|
||
FindChildOfType<CheckBox>(control) ??
|
||
FindChildOfType<DatePicker>(control) ??
|
||
FindChildOfType<NumberBox>(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<T>(DependencyObject parent) where T : DependencyObject
|
||
{
|
||
var queue = new Queue<DependencyObject>();
|
||
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;
|
||
}
|
||
} |