Добавьте файлы проекта.
This commit is contained in:
599
SQLVision.Visualizers/Factories/ControlFactory.cs
Normal file
599
SQLVision.Visualizers/Factories/ControlFactory.cs
Normal file
@@ -0,0 +1,599 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
40
SQLVision.Visualizers/Factories/VisualizerFactory.cs
Normal file
40
SQLVision.Visualizers/Factories/VisualizerFactory.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using SQLVision.Core.Enums;
|
||||
using SQLVision.Visualizers.Interfaces;
|
||||
using SQLVision.Visualizers.Visualizers;
|
||||
|
||||
namespace SQLVision.Visualizers.Factories;
|
||||
|
||||
public class VisualizerFactory : IVisualizerFactory
|
||||
{
|
||||
private readonly Dictionary<OutputType, IVisualizer> _visualizers;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public VisualizerFactory(IServiceProvider serviceProvider)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_visualizers = new Dictionary<OutputType, IVisualizer>
|
||||
{
|
||||
[OutputType.Table] = new TableVisualizer(),
|
||||
[OutputType.Chart] = new ChartVisualizer(),
|
||||
[OutputType.Text] = new TextVisualizer(),
|
||||
[OutputType.Grid] = new TableVisualizer(), // Пока используем TableVisualizer
|
||||
[OutputType.Custom] = new TableVisualizer() // Fallback
|
||||
};
|
||||
}
|
||||
|
||||
public IVisualizer GetVisualizer(OutputType type)
|
||||
{
|
||||
if (_visualizers.TryGetValue(type, out var visualizer))
|
||||
{
|
||||
return visualizer;
|
||||
}
|
||||
|
||||
// Fallback на табличный визуализатор
|
||||
return _visualizers[OutputType.Table];
|
||||
}
|
||||
|
||||
public void RegisterVisualizer(OutputType type, IVisualizer visualizer)
|
||||
{
|
||||
_visualizers[type] = visualizer;
|
||||
}
|
||||
}
|
||||
10
SQLVision.Visualizers/Interfaces/IControlFactory.cs
Normal file
10
SQLVision.Visualizers/Interfaces/IControlFactory.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
using SQLVision.Core.Models;
|
||||
|
||||
namespace SQLVision.Visualizers.Interfaces;
|
||||
|
||||
public interface IControlFactory
|
||||
{
|
||||
FrameworkElement CreateControl(ScriptParameter parameter, Action<object?> onValueChanged);
|
||||
void UpdateControlState(FrameworkElement control, ScriptParameter parameter, Dictionary<string, object?> currentValues);
|
||||
}
|
||||
12
SQLVision.Visualizers/Interfaces/IVisualizer.cs
Normal file
12
SQLVision.Visualizers/Interfaces/IVisualizer.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
using SQLVision.Core.Enums;
|
||||
using SQLVision.Core.Models;
|
||||
using System.Data;
|
||||
|
||||
namespace SQLVision.Visualizers.Interfaces;
|
||||
|
||||
public interface IVisualizer
|
||||
{
|
||||
FrameworkElement Visualize(DataTable data, OutputDefinition definition);
|
||||
bool CanVisualize(OutputType type);
|
||||
}
|
||||
9
SQLVision.Visualizers/Interfaces/IVisualizerFactory.cs
Normal file
9
SQLVision.Visualizers/Interfaces/IVisualizerFactory.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using SQLVision.Core.Enums;
|
||||
|
||||
namespace SQLVision.Visualizers.Interfaces;
|
||||
|
||||
public interface IVisualizerFactory
|
||||
{
|
||||
IVisualizer GetVisualizer(OutputType type);
|
||||
void RegisterVisualizer(OutputType type, IVisualizer visualizer);
|
||||
}
|
||||
25
SQLVision.Visualizers/SQLVision.Visualizers.csproj
Normal file
25
SQLVision.Visualizers/SQLVision.Visualizers.csproj
Normal file
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
|
||||
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SupportedOSPlatformVersion>10.0.17763.0</SupportedOSPlatformVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SQLVision.Core\SQLVision.Core.csproj" />
|
||||
<ProjectReference Include="..\SQLVision.Services\SQLVision.Services.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- WinUI 3 -->
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
|
||||
|
||||
<!-- Графики -->
|
||||
<PackageReference Include="LiveChartsCore.SkiaSharpView.WinUI" Version="2.0.0-rc5.3" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
17
SQLVision.Visualizers/ServiceExtensions.cs
Normal file
17
SQLVision.Visualizers/ServiceExtensions.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SQLVision.Visualizers.Factories;
|
||||
using SQLVision.Visualizers.Interfaces;
|
||||
|
||||
namespace SQLVision.Visualizers;
|
||||
|
||||
public static class ServiceExtensions
|
||||
{
|
||||
public static IServiceCollection AddSqlVisionVisualizers(this IServiceCollection services)
|
||||
{
|
||||
// Регистрация фабрик
|
||||
services.AddSingleton<IControlFactory, ControlFactory>();
|
||||
services.AddSingleton<IVisualizerFactory, VisualizerFactory>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
184
SQLVision.Visualizers/Visualizers/ChartVisualizer.cs
Normal file
184
SQLVision.Visualizers/Visualizers/ChartVisualizer.cs
Normal file
@@ -0,0 +1,184 @@
|
||||
using SQLVision.Core.Enums;
|
||||
using SQLVision.Core.Models;
|
||||
using SQLVision.Visualizers.Interfaces;
|
||||
using System.Data;
|
||||
|
||||
namespace SQLVision.Visualizers.Visualizers;
|
||||
|
||||
public class ChartVisualizer : IVisualizer
|
||||
{
|
||||
public FrameworkElement Visualize(DataTable data, OutputDefinition definition)
|
||||
{
|
||||
if (data.Rows.Count == 0)
|
||||
{
|
||||
return CreateEmptyChartMessage("Нет данных для построения графика");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var chartType = definition.ChartType;
|
||||
var cartesianChart = new CartesianChart
|
||||
{
|
||||
Series = CreateSeries(data, definition),
|
||||
XAxes = CreateXAxes(data, definition),
|
||||
YAxes = CreateYAxes(definition),
|
||||
LegendPosition = LegendPosition.Right,
|
||||
TooltipPosition = LiveChartsCore.Measure.TooltipPosition.Hidden
|
||||
};
|
||||
|
||||
return cartesianChart;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return CreateEmptyChartMessage("Ошибка при построении графика");
|
||||
}
|
||||
}
|
||||
|
||||
private ISeries[] CreateSeries(DataTable data, OutputDefinition definition)
|
||||
{
|
||||
var series = new List<ISeries>();
|
||||
|
||||
if (!string.IsNullOrEmpty(definition.SeriesColumn))
|
||||
{
|
||||
// Разделение по сериям
|
||||
var seriesGroups = data.AsEnumerable()
|
||||
.GroupBy(row => row[definition.SeriesColumn])
|
||||
.ToList();
|
||||
|
||||
foreach (var group in seriesGroups)
|
||||
{
|
||||
var seriesName = group.Key.ToString();
|
||||
var values = group.Select(row =>
|
||||
{
|
||||
if (string.IsNullOrEmpty(definition.YAxisColumn))
|
||||
return Convert.ToDouble(row[1]);
|
||||
return Convert.ToDouble(row[definition.YAxisColumn]);
|
||||
}).ToArray();
|
||||
|
||||
series.Add(CreateSeriesByType(definition.ChartType, values, seriesName));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Одна серия
|
||||
var values = data.AsEnumerable()
|
||||
.Select(row =>
|
||||
{
|
||||
if (string.IsNullOrEmpty(definition.YAxisColumn))
|
||||
return Convert.ToDouble(row[1]);
|
||||
return Convert.ToDouble(row[definition.YAxisColumn]);
|
||||
}).ToArray();
|
||||
|
||||
series.Add(CreateSeriesByType(definition.ChartType, values, definition.Description));
|
||||
}
|
||||
|
||||
return series.ToArray();
|
||||
}
|
||||
|
||||
private ISeries CreateSeriesByType(ChartType chartType, double[] values, string name)
|
||||
{
|
||||
return chartType switch
|
||||
{
|
||||
ChartType.Line => new LineSeries<double>
|
||||
{
|
||||
Values = values,
|
||||
Name = name,
|
||||
Fill = null,
|
||||
GeometrySize = 8,
|
||||
LineSmoothness = 0
|
||||
},
|
||||
ChartType.Bar => new ColumnSeries<double>
|
||||
{
|
||||
Values = values,
|
||||
Name = name
|
||||
},
|
||||
ChartType.Area => new LineSeries<double>
|
||||
{
|
||||
Values = values,
|
||||
Name = name,
|
||||
Fill = new SolidColorPaint(SKColors.Blue.WithAlpha(50))
|
||||
},
|
||||
ChartType.Scatter => new ScatterSeries<ObservablePoint>
|
||||
{
|
||||
Values = values.Select((v, i) => new ObservablePoint(i, v)),
|
||||
Name = name,
|
||||
GeometrySize = 10
|
||||
},
|
||||
_ => new LineSeries<double>
|
||||
{
|
||||
Values = values,
|
||||
Name = name
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Axis[] CreateXAxes(DataTable data, OutputDefinition definition)
|
||||
{
|
||||
var labels = new List<string>();
|
||||
|
||||
if (!string.IsNullOrEmpty(definition.XAxisColumn))
|
||||
{
|
||||
labels = data.AsEnumerable()
|
||||
.Select(row => row[definition.XAxisColumn].ToString())
|
||||
.ToList();
|
||||
}
|
||||
else if (data.Columns.Count > 0)
|
||||
{
|
||||
// Берем первый столбец для оси X
|
||||
labels = data.AsEnumerable()
|
||||
.Select(row => row[0].ToString())
|
||||
.ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
labels = Enumerable.Range(0, data.Rows.Count)
|
||||
.Select(i => i.ToString())
|
||||
.ToList();
|
||||
}
|
||||
|
||||
return new[]
|
||||
{
|
||||
new Axis
|
||||
{
|
||||
Labels = labels.ToArray(),
|
||||
LabelsRotation = labels.Count > 10 ? 45 : 0,
|
||||
TextSize = 12
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Axis[] CreateYAxes(OutputDefinition definition)
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
new Axis
|
||||
{
|
||||
Name = string.IsNullOrEmpty(definition.YAxisColumn) ? "Значения" : definition.YAxisColumn,
|
||||
TextSize = 12
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private FrameworkElement CreateEmptyChartMessage(string message)
|
||||
{
|
||||
var textBlock = new TextBlock
|
||||
{
|
||||
Text = message,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
FontSize = 16,
|
||||
Foreground = new Microsoft.UI.Xaml.Media.SolidColorBrush(Microsoft.UI.Colors.Gray)
|
||||
};
|
||||
|
||||
var border = new Border
|
||||
{
|
||||
Child = textBlock,
|
||||
Background = new Microsoft.UI.Xaml.Media.SolidColorBrush(Microsoft.UI.Colors.Transparent),
|
||||
Padding = new Thickness(20)
|
||||
};
|
||||
|
||||
return border;
|
||||
}
|
||||
|
||||
public bool CanVisualize(OutputType type) => type == OutputType.Chart;
|
||||
}
|
||||
75
SQLVision.Visualizers/Visualizers/TableVisualizer.cs
Normal file
75
SQLVision.Visualizers/Visualizers/TableVisualizer.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using SQLVision.Core.Enums;
|
||||
using SQLVision.Core.Models;
|
||||
using SQLVision.Visualizers.Interfaces;
|
||||
using System.Data;
|
||||
|
||||
namespace SQLVision.Visualizers.Visualizers;
|
||||
|
||||
public class TableVisualizer : IVisualizer
|
||||
{
|
||||
public FrameworkElement Visualize(DataTable data, OutputDefinition definition)
|
||||
{
|
||||
var listView = new ListView
|
||||
{
|
||||
ItemsSource = data.DefaultView,
|
||||
SelectionMode = ListViewSelectionMode.None,
|
||||
IsItemClickEnabled = false,
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||
VerticalAlignment = VerticalAlignment.Stretch
|
||||
};
|
||||
|
||||
// Автоматическое создание колонок
|
||||
listView.ItemTemplate = CreateDataTemplate(data);
|
||||
|
||||
var scrollViewer = new ScrollViewer
|
||||
{
|
||||
Content = listView,
|
||||
HorizontalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||||
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||||
MaxHeight = 600
|
||||
};
|
||||
|
||||
return scrollViewer;
|
||||
}
|
||||
|
||||
private DataTemplate CreateDataTemplate(DataTable data)
|
||||
{
|
||||
var gridFactory = new FrameworkElementFactory(typeof(Grid));
|
||||
|
||||
// Создаем колонки
|
||||
foreach (DataColumn column in data.Columns)
|
||||
{
|
||||
var columnDefinition = new ColumnDefinition();
|
||||
gridFactory.AppendChild(columnDefinition);
|
||||
}
|
||||
|
||||
// Создаем строку с текстовыми блоками
|
||||
var stackPanelFactory = new FrameworkElementFactory(typeof(StackPanel));
|
||||
stackPanelFactory.SetValue(StackPanel.OrientationProperty, Orientation.Horizontal);
|
||||
|
||||
foreach (DataColumn column in data.Columns)
|
||||
{
|
||||
var borderFactory = new FrameworkElementFactory(typeof(Border));
|
||||
borderFactory.SetValue(Border.BorderThicknessProperty, new Thickness(0, 0, 1, 1));
|
||||
borderFactory.SetValue(Border.BorderBrushProperty, new SolidColorBrush(Colors.LightGray));
|
||||
|
||||
var textBlockFactory = new FrameworkElementFactory(typeof(TextBlock));
|
||||
textBlockFactory.SetBinding(TextBlock.TextProperty,
|
||||
new Microsoft.UI.Xaml.Data.Binding { Path = new PropertyPath($"[{column.ColumnName}]") });
|
||||
textBlockFactory.SetValue(TextBlock.MarginProperty, new Thickness(4));
|
||||
textBlockFactory.SetValue(TextBlock.VerticalAlignmentProperty, VerticalAlignment.Center);
|
||||
|
||||
borderFactory.AppendChild(textBlockFactory);
|
||||
stackPanelFactory.AppendChild(borderFactory);
|
||||
}
|
||||
|
||||
gridFactory.AppendChild(stackPanelFactory);
|
||||
|
||||
return new DataTemplate { VisualTree = gridFactory };
|
||||
}
|
||||
|
||||
public bool CanVisualize(OutputType type) => type == OutputType.Table;
|
||||
}
|
||||
70
SQLVision.Visualizers/Visualizers/TextVisualizer.cs
Normal file
70
SQLVision.Visualizers/Visualizers/TextVisualizer.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using SQLVision.Core.Enums;
|
||||
using SQLVision.Core.Models;
|
||||
using SQLVision.Visualizers.Interfaces;
|
||||
using System.Data;
|
||||
using System.Text;
|
||||
|
||||
namespace SQLVision.Visualizers.Visualizers;
|
||||
|
||||
public class TextVisualizer : IVisualizer
|
||||
{
|
||||
public FrameworkElement Visualize(DataTable data, OutputDefinition definition)
|
||||
{
|
||||
var textBlock = new TextBlock
|
||||
{
|
||||
Text = ConvertDataTableToText(data),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
FontFamily = new Microsoft.UI.Xaml.Media.FontFamily("Consolas"),
|
||||
FontSize = 12,
|
||||
IsTextSelectionEnabled = true
|
||||
};
|
||||
|
||||
return new ScrollViewer
|
||||
{
|
||||
Content = textBlock,
|
||||
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||||
HorizontalScrollBarVisibility = ScrollBarVisibility.Auto
|
||||
};
|
||||
}
|
||||
|
||||
private string ConvertDataTableToText(DataTable data)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
// Заголовки
|
||||
for (int i = 0; i < data.Columns.Count; i++)
|
||||
{
|
||||
sb.Append(data.Columns[i].ColumnName);
|
||||
if (i < data.Columns.Count - 1)
|
||||
sb.Append(" | ");
|
||||
}
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(new string('-', data.Columns.Count * 20));
|
||||
|
||||
// Данные
|
||||
foreach (DataRow row in data.Rows)
|
||||
{
|
||||
for (int i = 0; i < data.Columns.Count; i++)
|
||||
{
|
||||
var value = row[i];
|
||||
var text = value?.ToString() ?? "NULL";
|
||||
|
||||
// Обрезаем слишком длинные значения
|
||||
if (text.Length > 50)
|
||||
text = text.Substring(0, 47) + "...";
|
||||
|
||||
sb.Append(text);
|
||||
|
||||
if (i < data.Columns.Count - 1)
|
||||
sb.Append(" | ");
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public bool CanVisualize(OutputType type) => type == OutputType.Text;
|
||||
}
|
||||
26
SQLVision.Visualizers/app.manifest
Normal file
26
SQLVision.Visualizers/app.manifest
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="SQLVision.Application"/>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Windows 10 and Windows 11 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
</assembly>
|
||||
Reference in New Issue
Block a user