Добавьте файлы проекта.

This commit is contained in:
FrigaT
2026-01-05 00:29:19 +03:00
committed by FrigaT
parent 76a09d80d4
commit d0653c2098
105 changed files with 6729 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
namespace SQLVision.Core.Enums;
public enum ChartType
{
Line,
Bar,
Pie,
Area,
Scatter,
Heatmap,
Candlestick
}

View File

@@ -0,0 +1,10 @@
namespace SQLVision.Core.Enums;
public enum DatabaseProvider
{
SqlServer, // Только MSSQL для начала
// PostgreSQL,
// MySQL,
// SQLite,
// Oracle
}

View File

@@ -0,0 +1,9 @@
namespace SQLVision.Core.Enums;
public enum NotificationType
{
Information,
Success,
Warning,
Error
}

View File

@@ -0,0 +1,11 @@
namespace SQLVision.Core.Enums;
public enum OutputType
{
Table,
Chart,
Text,
Grid,
Map,
Custom
}

View File

@@ -0,0 +1,15 @@
namespace SQLVision.Core.Enums;
public enum ParameterType
{
String,
Integer,
Decimal,
DateTime,
Boolean,
Table, // Для ComboBox с данными из БД
MultiSelect, // ListBox с множественным выбором
Color,
File,
Json
}

View File

@@ -0,0 +1,9 @@
namespace SQLVision.Core.Enums;
public enum ScriptChangeType
{
Created,
Updated,
Deleted,
Renamed
}

View File

@@ -0,0 +1,11 @@
using SQLVision.Core.Models;
using System.Data;
namespace SQLVision.Services.Exporters;
public interface IExportHandler
{
string FormatName { get; }
Task ExportAsync(DataTable data, string filePath, ExportOptions options);
Task<byte[]> ExportToMemoryAsync(DataTable data, ExportOptions options);
}

View File

@@ -0,0 +1,11 @@
using SQLVision.Core.Models;
using System.Data;
namespace SQLVision.Core.Interfaces;
public interface IExportService
{
Task ExportAsync(DataTable data, string filePath, ExportOptions options);
Task ExportAsync(DataSet dataSet, string filePath, ExportOptions options);
Task<byte[]> ExportToMemoryAsync(DataTable data, ExportOptions options);
}

View File

@@ -0,0 +1,10 @@
using SQLVision.Core.Models;
using System.Data;
using System.Threading.Tasks;
namespace SQLVision.Core.Interfaces;
public interface IMemoryExportHandler
{
Task<byte[]> ExportToMemoryAsync(DataTable data, ExportOptions options);
}

View File

@@ -0,0 +1,13 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using SQLVision.Core.Enums;
namespace SQLVision.Core.Interfaces;
public interface IPluginContext
{
IServiceProvider ServiceProvider { get; }
IConfiguration Configuration { get; }
ILogger Logger { get; }
Task ShowNotificationAsync(string message, NotificationType type);
}

View File

@@ -0,0 +1,13 @@
using SQLVision.Core.Models;
namespace SQLVision.Core.Interfaces;
public interface IPluginManager
{
void LoadPlugins(string pluginsDirectory);
IEnumerable<ISqlVisionPlugin> GetPlugins();
T? GetPlugin<T>() where T : ISqlVisionPlugin;
Task BeforeExecutionAsync(ScriptMetadata script, Dictionary<string, object> parameters);
Task AfterExecutionAsync(ScriptMetadata script, ExecutionResult result);
}

View File

@@ -0,0 +1,38 @@
using SQLVision.Core.Enums;
using SQLVision.Core.Models;
namespace SQLVision.Core.Interfaces;
public interface IScriptManager
{
Task<IEnumerable<ScriptMetadata>> LoadScriptsAsync(string? directory = null);
Task<ScriptMetadata> ReloadScriptAsync(string filePath);
void WatchDirectory(string directory, Action<string> onScriptChanged);
event EventHandler<ScriptChangedEventArgs> ScriptChanged;
event EventHandler<ScriptsReloadedEventArgs> ScriptsReloaded;
}
public class ScriptChangedEventArgs : EventArgs
{
public string FilePath { get; }
public ScriptChangeType ChangeType { get; }
public ScriptMetadata? Script { get; }
public ScriptChangedEventArgs(string filePath, ScriptChangeType changeType, ScriptMetadata? script = null)
{
FilePath = filePath;
ChangeType = changeType;
Script = script;
}
}
public class ScriptsReloadedEventArgs : EventArgs
{
public IEnumerable<ScriptMetadata> Scripts { get; }
public ScriptsReloadedEventArgs(IEnumerable<ScriptMetadata> scripts)
{
Scripts = scripts;
}
}

View File

@@ -0,0 +1,30 @@
using SQLVision.Core.Enums;
using SQLVision.Core.Models;
using System.Data;
namespace SQLVision.Core.Interfaces;
public interface ISqlExecutionService
{
Task<ExecutionResult> ExecuteAsync(
ScriptMetadata script,
Dictionary<string, object> parameters,
CancellationToken cancellationToken = default);
Task<ExecutionResult> ExecuteAsync(
string sql,
Dictionary<string, object> parameters,
string connectionString,
CancellationToken cancellationToken = default);
Task<bool> TestConnectionAsync(
string connectionString,
DatabaseProvider provider,
CancellationToken cancellationToken = default);
Task<DataTable> LoadComboBoxDataAsync(
string query,
string connectionString,
DatabaseProvider provider,
CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,9 @@
using SQLVision.Core.Models;
namespace SQLVision.Core.Interfaces;
public interface ISqlScriptParser
{
ScriptMetadata Parse(string filePath, string sqlContent);
Task<ScriptMetadata> ParseAsync(string filePath, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,11 @@
namespace SQLVision.Core.Interfaces;
public interface ISqlVisionPlugin
{
string Name { get; }
string Description { get; }
Version Version { get; }
Task InitializeAsync(IPluginContext context);
Task ShutdownAsync();
}

View File

@@ -0,0 +1,12 @@
using System.Collections.Generic;
namespace SQLVision.Core.Models;
public class ChartSeries
{
public string Name { get; set; }
public List<object> Values { get; set; } = new();
public string Color { get; set; }
public double LineSmoothness { get; set; } = 0;
public bool ShowPoints { get; set; } = true;
}

View File

@@ -0,0 +1,36 @@
using System.Text.Json.Serialization;
namespace SQLVision.Core.Models;
public class ExecutionHistoryItem
{
[JsonPropertyName("id")]
public string Id { get; set; } = Guid.NewGuid().ToString();
[JsonPropertyName("scriptId")]
public string ScriptId { get; set; } = string.Empty;
[JsonPropertyName("scriptName")]
public string ScriptName { get; set; } = string.Empty;
[JsonPropertyName("executionTime")]
public DateTime ExecutionTime { get; set; }
[JsonPropertyName("duration")]
public TimeSpan Duration { get; set; }
[JsonPropertyName("success")]
public bool Success { get; set; }
[JsonPropertyName("parameters")]
public Dictionary<string, object> Parameters { get; set; } = new();
[JsonPropertyName("rowCount")]
public int RowCount { get; set; }
[JsonPropertyName("errorMessage")]
public string? ErrorMessage { get; set; }
[JsonPropertyName("executedSql")]
public string? ExecutedSql { get; set; }
}

View File

@@ -0,0 +1,41 @@
using System.Data;
using System.Text.Json.Serialization;
namespace SQLVision.Core.Models;
public class ExecutionResult
{
[JsonPropertyName("data")]
public DataSet? Data { get; set; }
[JsonPropertyName("isSuccess")]
public bool IsSuccess { get; set; }
[JsonPropertyName("errorMessage")]
public string? ErrorMessage { get; set; }
[JsonPropertyName("executionTime")]
public TimeSpan ExecutionTime { get; set; }
[JsonPropertyName("isFromCache")]
public bool IsFromCache { get; set; }
[JsonPropertyName("executionDate")]
public DateTime ExecutionDate { get; set; } = DateTime.UtcNow;
[JsonPropertyName("parameters")]
public Dictionary<string, object> Parameters { get; set; } = new();
[JsonPropertyName("executedSql")]
public string ExecutedSql { get; set; } = string.Empty;
[JsonPropertyName("rowCount")]
public int RowCount { get; set; }
[JsonPropertyName("metrics")]
public Dictionary<string, object> Metrics { get; set; } = new();
[JsonPropertyName("connectionName")]
public string? ConnectionName { get; set; }
}

View File

@@ -0,0 +1,12 @@
namespace SQLVision.Core.Models;
public class ExportOptions
{
public string Format { get; set; } = "Excel";
public bool IncludeHeaders { get; set; } = true;
public bool AutoFilter { get; set; } = true;
public bool IncludeCharts { get; set; } = false;
public string? ChartType { get; set; }
public bool OpenAfterExport { get; set; } = false;
public Dictionary<string, object> CustomOptions { get; set; } = new();
}

View File

@@ -0,0 +1,38 @@
using SQLVision.Core.Enums;
using System.Text.Json.Serialization;
namespace SQLVision.Core.Models;
public class OutputDefinition
{
[JsonPropertyName("type")]
public OutputType Type { get; set; } = OutputType.Table;
[JsonPropertyName("subType")]
public string? SubType { get; set; }
[JsonPropertyName("description")]
public string Description { get; set; } = "Result";
[JsonPropertyName("isPrimary")]
public bool IsPrimary { get; set; } = false;
[JsonPropertyName("options")]
public Dictionary<string, string> Options { get; set; } = new();
[JsonPropertyName("dataTableName")]
public string? DataTableName { get; set; }
// Для графиков
[JsonPropertyName("xAxisColumn")]
public string? XAxisColumn { get; set; }
[JsonPropertyName("yAxisColumn")]
public string? YAxisColumn { get; set; }
[JsonPropertyName("seriesColumn")]
public string? SeriesColumn { get; set; }
[JsonPropertyName("chartType")]
public ChartType ChartType { get; set; } = ChartType.Line;
}

View File

@@ -0,0 +1,8 @@
namespace SQLVision.Core.Models;
public class ScriptCategory
{
public string Name { get; set; } = string.Empty;
public List<ScriptMetadata> Scripts { get; set; } = new();
public bool IsExpanded { get; set; } = true;
}

View File

@@ -0,0 +1,63 @@
using SQLVision.Core.Enums;
using System.Text.Json.Serialization;
namespace SQLVision.Core.Models;
public class ScriptMetadata
{
[JsonPropertyName("id")]
public string Id { get; set; } = Guid.NewGuid().ToString();
[JsonPropertyName("fileName")]
public string FileName { get; set; } = string.Empty;
[JsonPropertyName("fullPath")]
public string FullPath { get; set; } = string.Empty;
[JsonPropertyName("description")]
public string? Description { get; set; }
[JsonPropertyName("rawSql")]
public string RawSql { get; set; } = string.Empty;
[JsonPropertyName("processedSql")]
public string ProcessedSql { get; set; } = string.Empty;
[JsonPropertyName("connectionString")]
public string? ConnectionString { get; set; }
[JsonPropertyName("databaseProvider")]
public DatabaseProvider DatabaseProvider { get; set; } = DatabaseProvider.SqlServer;
[JsonPropertyName("parameters")]
public List<ScriptParameter> Parameters { get; set; } = new();
[JsonPropertyName("outputs")]
public List<OutputDefinition> Outputs { get; set; } = new();
[JsonPropertyName("metadata")]
public Dictionary<string, object> Metadata { get; set; } = new();
[JsonPropertyName("lastModified")]
public DateTime LastModified { get; set; } = DateTime.UtcNow;
[JsonPropertyName("category")]
public string? Category { get; set; }
[JsonPropertyName("tags")]
public List<string> Tags { get; set; } = new();
[JsonPropertyName("executionCount")]
public int ExecutionCount { get; set; }
[JsonPropertyName("averageExecutionTime")]
public TimeSpan AverageExecutionTime { get; set; }
[JsonIgnore]
public string DisplayName => !string.IsNullOrEmpty(Description)
? Description
: Path.GetFileNameWithoutExtension(FileName);
[JsonIgnore]
public bool IsVisible { get; set; } = true;
}

View File

@@ -0,0 +1,66 @@
using SQLVision.Core.Enums;
using System.Text.Json.Serialization;
namespace SQLVision.Core.Models;
public class ScriptParameter
{
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
[JsonPropertyName("type")]
public ParameterType Type { get; set; } = ParameterType.String;
[JsonPropertyName("displayName")]
public string? DisplayName { get; set; }
[JsonPropertyName("description")]
public string? Description { get; set; }
[JsonPropertyName("defaultValue")]
public object? DefaultValue { get; set; }
[JsonPropertyName("isRequired")]
public bool IsRequired { get; set; } = false;
[JsonPropertyName("order")]
public int Order { get; set; } = 0;
[JsonPropertyName("group")]
public string? Group { get; set; }
[JsonPropertyName("tableQuery")]
public string? TableQuery { get; set; }
[JsonPropertyName("valueMember")]
public string ValueMember { get; set; } = "Id";
[JsonPropertyName("displayMember")]
public string DisplayMember { get; set; } = "Name";
[JsonPropertyName("dependsOn")]
public string? DependsOn { get; set; }
[JsonPropertyName("dependencyValues")]
public Dictionary<string, object>? DependencyValues { get; set; }
[JsonPropertyName("validationRules")]
public Dictionary<string, object>? ValidationRules { get; set; }
[JsonPropertyName("watermark")]
public string? Watermark { get; set; }
[JsonPropertyName("icon")]
public string? Icon { get; set; }
[JsonIgnore]
public string EffectiveDisplayName => DisplayName ?? Name;
public bool Validate(object? value)
{
if (IsRequired && (value == null || string.IsNullOrWhiteSpace(value.ToString())))
return false;
return true;
}
}

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Text.Json" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.1" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,8 @@
namespace SQLVision.Services.Configuration;
public class CacheOptions
{
public bool Enabled { get; set; } = true;
public int DurationMinutes { get; set; } = 10;
public long MaxSizeBytes { get; set; } = 100 * 1024 * 1024; // 100MB
}

View File

@@ -0,0 +1,13 @@
namespace SQLVision.Services.Configuration;
public class DatabaseOptions
{
public const string SectionName = "Database";
public string DefaultConnection { get; set; } = string.Empty;
public Dictionary<string, string> ConnectionStrings { get; set; } = new();
public int CommandTimeout { get; set; } = 300;
public bool EnableStatistics { get; set; } = true;
public CacheOptions Cache { get; set; } = new();
public RetryOptions Retry { get; set; } = new();
}

View File

@@ -0,0 +1,8 @@
namespace SQLVision.Services.Configuration;
public class RetryOptions
{
public bool Enabled { get; set; } = true;
public int MaxRetries { get; set; } = 3;
public int DelayMilliseconds { get; set; } = 1000;
}

View File

@@ -0,0 +1,86 @@
using CsvHelper;
using CsvHelper.Configuration;
using Microsoft.Extensions.Logging;
using SQLVision.Core.Models;
using System.Data;
namespace SQLVision.Services.Exporters;
public class CsvExporter : IExportHandler
{
private readonly ILogger<CsvExporter> _logger;
public string FormatName => "CSV";
public CsvExporter(ILogger<CsvExporter> logger)
{
_logger = logger;
}
public async Task ExportAsync(DataTable data, string filePath, ExportOptions options)
{
var config = new CsvConfiguration(System.Globalization.CultureInfo.CurrentCulture)
{
Delimiter = options.CustomOptions.TryGetValue("Delimiter", out var delimiter)
? delimiter.ToString() ?? ","
: ","
};
using var writer = new StreamWriter(filePath);
using var csv = new CsvWriter(writer, config);
// Запись заголовков
if (options.IncludeHeaders)
{
foreach (DataColumn column in data.Columns)
{
csv.WriteField(column.ColumnName);
}
csv.NextRecord();
}
// Запись данных
foreach (DataRow row in data.Rows)
{
for (int i = 0; i < data.Columns.Count; i++)
{
csv.WriteField(row[i]);
}
csv.NextRecord();
}
await csv.FlushAsync();
_logger.LogInformation("Exported {Rows} rows to CSV: {FilePath}", data.Rows.Count, filePath);
}
public async Task<byte[]> ExportToMemoryAsync(DataTable data, ExportOptions options)
{
using var stream = new MemoryStream();
using var writer = new StreamWriter(stream);
using var csv = new CsvWriter(writer, System.Globalization.CultureInfo.CurrentCulture);
// Запись заголовков
if (options.IncludeHeaders)
{
foreach (DataColumn column in data.Columns)
{
csv.WriteField(column.ColumnName);
}
csv.NextRecord();
}
// Запись данных
foreach (DataRow row in data.Rows)
{
for (int i = 0; i < data.Columns.Count; i++)
{
csv.WriteField(row[i]);
}
csv.NextRecord();
}
await csv.FlushAsync();
await writer.FlushAsync();
return stream.ToArray();
}
}

View File

@@ -0,0 +1,139 @@
using ClosedXML.Excel;
using Microsoft.Extensions.Logging;
using SQLVision.Core.Models;
using System.Data;
namespace SQLVision.Services.Exporters;
public class ExcelExporter : IExportHandler
{
private readonly ILogger<ExcelExporter> _logger;
public string FormatName => "Excel";
public ExcelExporter(ILogger<ExcelExporter> logger)
{
_logger = logger;
}
public async Task ExportAsync(DataTable data, string filePath, ExportOptions options)
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Data");
WriteDataToWorksheet(worksheet, data, options);
if (options.IncludeCharts && data.Rows.Count > 0)
{
AddCharts(worksheet, data, options);
}
await Task.Run(() => workbook.SaveAs(filePath));
_logger.LogInformation("Exported {Rows} rows to Excel: {FilePath}", data.Rows.Count, filePath);
}
public async Task<byte[]> ExportToMemoryAsync(DataTable data, ExportOptions options)
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Data");
WriteDataToWorksheet(worksheet, data, options);
using var stream = new MemoryStream();
await Task.Run(() => workbook.SaveAs(stream));
return stream.ToArray();
}
private void WriteDataToWorksheet(IXLWorksheet worksheet, DataTable data, ExportOptions options)
{
if (options.IncludeHeaders)
{
for (int col = 0; col < data.Columns.Count; col++)
{
worksheet.Cell(1, col + 1).Value = data.Columns[col].ColumnName;
worksheet.Cell(1, col + 1).Style.Font.Bold = true;
}
}
int startRow = options.IncludeHeaders ? 2 : 1;
for (int row = 0; row < data.Rows.Count; row++)
{
for (int col = 0; col < data.Columns.Count; col++)
{
var value = data.Rows[row][col];
worksheet.Cell(startRow + row, col + 1).Value = ConvertValue(value);
ApplyFormatting(worksheet.Cell(startRow + row, col + 1), value);
}
}
if (options.AutoFilter)
{
var endRow = startRow + data.Rows.Count - 1;
worksheet.Range(1, 1, endRow, data.Columns.Count).SetAutoFilter();
}
worksheet.Columns().AdjustToContents();
}
private XLCellValue ConvertValue(object value)
{
if (value == null || value == DBNull.Value) return Blank.Value;
if (value is DateTime dateTime)
return dateTime;
if (value is decimal || value is double || value is float)
return Convert.ToDouble(value);
if (value is bool b)
return b;
return value.ToString();
}
private void ApplyFormatting(IXLCell cell, object value)
{
if (value is DateTime)
{
cell.Style.DateFormat.Format = "dd.MM.yyyy HH:mm:ss";
}
else if (value is decimal || value is double || value is float)
{
cell.Style.NumberFormat.Format = "#,##0.00";
}
}
private void AddCharts(IXLWorksheet worksheet, DataTable data, ExportOptions options)
{
if (data.Columns.Count < 2) return;
//TODO: chart
/*var chartType = GetChartType(options.ChartType);
var chart = worksheet.CreateChart(0, data.Columns.Count + 2, 20, data.Columns.Count + 10);
chart.ChartType = chartType;
// Добавление серий на основе данных
for (int col = 1; col < Math.Min(5, data.Columns.Count); col++)
{
var series = chart.AddSeries(
worksheet.Range(2, col + 1, data.Rows.Count + 1, col + 1),
worksheet.Range(2, 1, data.Rows.Count + 1, 1));
series.ChartType = chartType;
}
*/
}
private XLChartType GetChartType(string? chartType) => chartType?.ToLower() switch
{
"line" => XLChartType.Line,
"column" => XLChartType.ColumnClustered,
"bar" => XLChartType.BarClustered,
"pie" => XLChartType.Pie,
"area" => XLChartType.Area,
_ => XLChartType.Line
};
}

View File

@@ -0,0 +1,59 @@
using Microsoft.Extensions.Logging;
using SQLVision.Core.Models;
using System.Data;
using System.Text.Json;
namespace SQLVision.Services.Exporters;
public class JsonExporter : IExportHandler
{
private readonly ILogger<JsonExporter> _logger;
private readonly JsonSerializerOptions _jsonOptions;
public string FormatName => "JSON";
public JsonExporter(ILogger<JsonExporter> logger)
{
_logger = logger;
_jsonOptions = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
}
public async Task ExportAsync(DataTable data, string filePath, ExportOptions options)
{
var records = ConvertDataTableToList(data);
var json = JsonSerializer.Serialize(records, _jsonOptions);
await File.WriteAllTextAsync(filePath, json);
_logger.LogInformation("Exported {Rows} rows to JSON: {FilePath}", data.Rows.Count, filePath);
}
public async Task<byte[]> ExportToMemoryAsync(DataTable data, ExportOptions options)
{
var records = ConvertDataTableToList(data);
var json = JsonSerializer.Serialize(records, _jsonOptions);
return System.Text.Encoding.UTF8.GetBytes(json);
}
private List<Dictionary<string, object>> ConvertDataTableToList(DataTable data)
{
var list = new List<Dictionary<string, object>>();
foreach (DataRow row in data.Rows)
{
var dict = new Dictionary<string, object>();
foreach (DataColumn column in data.Columns)
{
dict[column.ColumnName] = row[column] ?? DBNull.Value;
}
list.Add(dict);
}
return list;
}
}

View File

@@ -0,0 +1,312 @@
using Microsoft.Extensions.Logging;
using SQLVision.Core.Enums;
using SQLVision.Core.Interfaces;
using SQLVision.Core.Models;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
namespace SQLVision.Services.Parsers;
public class SqlScriptParser : ISqlScriptParser
{
private readonly ILogger<SqlScriptParser> _logger;
private readonly JsonSerializerOptions _jsonOptions;
public SqlScriptParser(ILogger<SqlScriptParser> logger)
{
_logger = logger;
_jsonOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
WriteIndented = true
};
}
public ScriptMetadata Parse(string filePath, string sqlContent)
{
var metadata = new ScriptMetadata
{
FileName = Path.GetFileName(filePath),
FullPath = filePath,
RawSql = sqlContent,
LastModified = File.GetLastWriteTimeUtc(filePath)
};
var lines = sqlContent.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
var sqlBuilder = new StringBuilder();
var inMultilineComment = false;
foreach (var line in lines)
{
var trimmedLine = line.Trim();
// Обработка многострочных комментариев
if (trimmedLine.StartsWith("/*"))
{
inMultilineComment = true;
if (trimmedLine.Contains("*/"))
{
inMultilineComment = false;
ProcessInlineMultilineComment(trimmedLine, metadata);
}
else
{
ProcessMultilineCommentStart(trimmedLine, metadata);
}
continue;
}
if (inMultilineComment)
{
if (trimmedLine.Contains("*/"))
{
inMultilineComment = false;
ProcessMultilineCommentEnd(trimmedLine, metadata);
}
else
{
ProcessMultilineCommentContent(trimmedLine, metadata);
}
continue;
}
// Обработка однострочных комментариев
if (trimmedLine.StartsWith("--"))
{
ProcessSingleLineComment(trimmedLine, metadata);
}
else
{
sqlBuilder.AppendLine(line);
}
}
metadata.ProcessedSql = sqlBuilder.ToString();
ExtractCategoryAndTags(metadata);
return metadata;
}
private void ProcessSingleLineComment(string line, ScriptMetadata metadata)
{
// Удаляем "--" и триммируем
var content = line.Substring(2).Trim();
// Проверяем на директивы
if (content.StartsWith("@"))
{
ProcessDirective(content, metadata);
}
else if (string.IsNullOrEmpty(metadata.Description))
{
// Первый комментарий без директивы - это описание
metadata.Description = content;
}
}
private void ProcessDirective(string content, ScriptMetadata metadata) // Убрали ref
{
// Убираем "@"
content = content.Substring(1).Trim();
var spaceIndex = content.IndexOf(' ');
if (spaceIndex <= 0) return;
var directive = content.Substring(0, spaceIndex).ToLower();
var value = content.Substring(spaceIndex + 1).Trim();
try
{
switch (directive)
{
case "description":
metadata.Description = value.Trim('"');
break;
case "param":
var param = ParseParameter(value);
if (param != null)
metadata.Parameters.Add(param);
break;
case "output":
var output = ParseOutput(value);
if (output != null)
metadata.Outputs.Add(output);
break;
case "connection":
metadata.ConnectionString = value.Trim('"');
break;
case "database":
if (Enum.TryParse<DatabaseProvider>(value, true, out var provider))
metadata.DatabaseProvider = provider;
break;
case "category":
metadata.Category = value.Trim('"');
break;
case "tags":
metadata.Tags = value.Split(',')
.Select(t => t.Trim().Trim('"'))
.Where(t => !string.IsNullOrEmpty(t))
.ToList();
break;
case "metadata":
try
{
var metadataJson = JsonSerializer.Deserialize<Dictionary<string, object>>(
value, _jsonOptions);
foreach (var kvp in metadataJson)
metadata.Metadata[kvp.Key] = kvp.Value;
}
catch { /* Игнорируем ошибки парсинга JSON */ }
break;
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error parsing directive: {Directive}", directive);
}
}
private ScriptParameter? ParseParameter(string value)
{
// Два формата: JSON и старый текстовый
if (value.TrimStart().StartsWith("{"))
{
return ParseJsonParameter(value);
}
else
{
return ParseLegacyParameter(value);
}
}
private ScriptParameter? ParseJsonParameter(string json)
{
try
{
var param = JsonSerializer.Deserialize<ScriptParameter>(json, _jsonOptions);
// Валидация обязательных полей
if (string.IsNullOrEmpty(param?.Name))
throw new ArgumentException("Parameter name is required");
if (param.Type == ParameterType.Table && string.IsNullOrEmpty(param.TableQuery))
throw new ArgumentException("TableQuery is required for Table type");
return param;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error parsing JSON parameter");
return null;
}
}
private ScriptParameter? ParseLegacyParameter(string legacy)
{
// Формат: Type Name "Display Name" default="value"
var match = Regex.Match(legacy,
@"(\w+)\s+(\w+)\s+""([^""]+)""(?:\s+default=""([^""]*)"")?(?:\s+@table\s+""([^""]+)"")?");
if (!match.Success) return null;
return new ScriptParameter
{
Type = Enum.TryParse<ParameterType>(match.Groups[1].Value, true, out var type)
? type : ParameterType.String,
Name = match.Groups[2].Value,
DisplayName = match.Groups[3].Value,
DefaultValue = match.Groups[4].Success ? match.Groups[4].Value : null,
TableQuery = match.Groups[5].Success ? match.Groups[5].Value : null
};
}
private OutputDefinition? ParseOutput(string value)
{
if (value.TrimStart().StartsWith("{"))
{
return JsonSerializer.Deserialize<OutputDefinition>(value, _jsonOptions);
}
// Старый формат: type:subtype "Description"
var match = Regex.Match(value, @"(\w+)(?::(\w+))?\s+""([^""]+)""");
if (!match.Success) return null;
return new OutputDefinition
{
Type = Enum.TryParse<OutputType>(match.Groups[1].Value, true, out var type)
? type : OutputType.Table,
SubType = match.Groups[2].Success ? match.Groups[2].Value : null,
Description = match.Groups[3].Value
};
}
private void ExtractCategoryAndTags(ScriptMetadata metadata)
{
// Извлекаем категорию из пути файла
if (string.IsNullOrEmpty(metadata.Category))
{
var relativePath = Path.GetDirectoryName(metadata.FullPath);
if (!string.IsNullOrEmpty(relativePath))
{
metadata.Category = Path.GetFileName(relativePath);
}
}
// Автоматическое добавление тегов на основе имени файла
var fileName = Path.GetFileNameWithoutExtension(metadata.FileName);
var words = fileName.Split('_', '-', ' ')
.Where(w => w.Length > 2)
.Select(w => w.ToLower());
metadata.Tags.AddRange(words);
metadata.Tags = metadata.Tags.Distinct().ToList();
}
public async Task<ScriptMetadata> ParseAsync(string filePath, CancellationToken cancellationToken = default)
{
var content = await File.ReadAllTextAsync(filePath, cancellationToken);
return Parse(filePath, content);
}
// Вспомогательные методы для многострочных комментариев
private void ProcessInlineMultilineComment(string line, ScriptMetadata metadata)
{
var content = line.Substring(2, line.IndexOf("*/") - 2).Trim();
ProcessCommentContent(content, metadata);
}
private void ProcessMultilineCommentStart(string line, ScriptMetadata metadata)
{
var content = line.Substring(2).Trim();
ProcessCommentContent(content, metadata);
}
private void ProcessMultilineCommentEnd(string line, ScriptMetadata metadata)
{
var content = line.Substring(0, line.IndexOf("*/")).Trim();
ProcessCommentContent(content, metadata);
}
private void ProcessMultilineCommentContent(string line, ScriptMetadata metadata)
{
ProcessCommentContent(line, metadata);
}
private void ProcessCommentContent(string content, ScriptMetadata metadata) // Убрали ref
{
if (content.StartsWith("@"))
{
ProcessDirective(content, metadata);
}
}
}

View File

@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\SQLVision.Core\SQLVision.Core.csproj" />
</ItemGroup>
<ItemGroup>
<!-- MSSQL драйвер -->
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.3" />
<!-- Экспорт в Excel -->
<PackageReference Include="ClosedXML" Version="0.105.0" />
<!-- Экспорт в CSV -->
<PackageReference Include="CsvHelper" Version="33.1.0" />
<!-- DI, конфигурация, кэширование -->
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.1" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,122 @@
using Microsoft.Extensions.Logging;
using SQLVision.Core.Interfaces;
using SQLVision.Core.Models;
using SQLVision.Services.Exporters;
using System.Data;
namespace SQLVision.Services.Services;
public class ExportService : IExportService
{
private readonly ILogger<ExportService> _logger;
private readonly Dictionary<string, IExportHandler> _exportHandlers;
public ExportService(
ILogger<ExportService> logger,
IEnumerable<IExportHandler> exportHandlers)
{
_logger = logger;
_exportHandlers = exportHandlers.ToDictionary(h => h.FormatName, h => h);
}
public async Task ExportAsync(DataTable data, string filePath, ExportOptions options)
{
if (data == null) throw new ArgumentNullException(nameof(data));
var extension = Path.GetExtension(filePath).TrimStart('.').ToLower();
var format = options?.Format ?? GetFormatFromExtension(extension);
if (_exportHandlers.TryGetValue(format, out var handler))
{
try
{
await handler.ExportAsync(data, filePath, options ?? new ExportOptions());
_logger.LogInformation("Exported {Rows} rows to {FilePath} as {Format}",
data.Rows.Count, filePath, format);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error exporting to {Format}", format);
throw;
}
}
else
{
throw new NotSupportedException($"Export format '{format}' is not supported");
}
}
public async Task ExportAsync(DataSet dataSet, string filePath, ExportOptions options)
{
if (dataSet.Tables.Count == 0)
throw new InvalidOperationException("DataSet contains no tables");
if (dataSet.Tables.Count == 1)
{
await ExportAsync(dataSet.Tables[0], filePath, options);
}
else
{
// Для Excel создаем несколько листов
var extension = Path.GetExtension(filePath);
if (extension.Equals(".xlsx", StringComparison.OrdinalIgnoreCase))
{
await ExportToMultiSheetExcel(dataSet, filePath, options);
}
else
{
// Для других форматов - экспортируем каждый лист в отдельный файл
var basePath = Path.Combine(Path.GetDirectoryName(filePath)!,
Path.GetFileNameWithoutExtension(filePath));
for (int i = 0; i < dataSet.Tables.Count; i++)
{
var table = dataSet.Tables[i];
var tableFilePath = $"{basePath}_{table.TableName}_{i + 1}{extension}";
await ExportAsync(table, tableFilePath, options);
}
}
}
}
public async Task<byte[]> ExportToMemoryAsync(DataTable data, ExportOptions options)
{
var format = options?.Format ?? "Excel";
if (_exportHandlers.TryGetValue(format, out var handler))
{
return await handler.ExportToMemoryAsync(data, options ?? new ExportOptions());
}
throw new NotSupportedException($"Export format '{format}' is not supported");
}
private async Task ExportToMultiSheetExcel(DataSet dataSet, string filePath, ExportOptions options)
{
using var workbook = new ClosedXML.Excel.XLWorkbook();
for (int i = 0; i < dataSet.Tables.Count; i++)
{
var table = dataSet.Tables[i];
var worksheet = workbook.Worksheets.Add(table.TableName ?? $"Sheet{i + 1}");
// Используем ExcelExporter для записи данных
if (_exportHandlers.TryGetValue("Excel", out var excelHandler) &&
excelHandler is ExcelExporter excelExporter)
{
var excelOptions = options ?? new ExportOptions();
await excelExporter.ExportAsync(table, filePath, excelOptions);
}
}
await Task.Run(() => workbook.SaveAs(filePath));
}
private string GetFormatFromExtension(string extension) => extension switch
{
"xlsx" => "Excel",
"csv" => "CSV",
"json" => "JSON",
_ => "Excel"
};
}

View File

@@ -0,0 +1,122 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SQLVision.Core.Enums;
using SQLVision.Core.Interfaces;
using SQLVision.Core.Models;
using System.Reflection;
namespace SQLVision.Services.Services;
public class PluginManager : IPluginManager
{
private readonly ILogger<PluginManager> _logger;
private readonly IServiceProvider _serviceProvider;
private readonly List<ISqlVisionPlugin> _plugins = new();
public PluginManager(ILogger<PluginManager> logger, IServiceProvider serviceProvider)
{
_logger = logger;
_serviceProvider = serviceProvider;
}
public void LoadPlugins(string pluginsDirectory)
{
if (!Directory.Exists(pluginsDirectory))
{
Directory.CreateDirectory(pluginsDirectory);
_logger.LogInformation("Created plugins directory: {Directory}", pluginsDirectory);
return;
}
var pluginFiles = Directory.GetFiles(pluginsDirectory, "*.dll");
_logger.LogInformation("Found {Count} plugin files", pluginFiles.Length);
foreach (var pluginFile in pluginFiles)
{
try
{
var assembly = Assembly.LoadFrom(pluginFile);
var pluginTypes = assembly.GetTypes()
.Where(t => typeof(ISqlVisionPlugin).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract);
foreach (var pluginType in pluginTypes)
{
try
{
var plugin = (ISqlVisionPlugin)Activator.CreateInstance(pluginType)!;
var context = new PluginContext(_serviceProvider);
plugin.InitializeAsync(context).Wait();
_plugins.Add(plugin);
_logger.LogInformation("Loaded plugin: {Name} v{Version}", plugin.Name, plugin.Version);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to initialize plugin from type {Type}", pluginType.FullName);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load plugin from {File}", pluginFile);
}
}
}
public IEnumerable<ISqlVisionPlugin> GetPlugins() => _plugins.AsReadOnly();
public T? GetPlugin<T>() where T : ISqlVisionPlugin
=> _plugins.OfType<T>().FirstOrDefault();
public async Task BeforeExecutionAsync(ScriptMetadata script, Dictionary<string, object> parameters)
{
foreach (var plugin in _plugins)
{
try
{
await plugin.InitializeAsync(new PluginContext(_serviceProvider));
// TODO: Добавить метод BeforeExecution в ISqlVisionPlugin если нужно
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in plugin {Plugin} BeforeExecution", plugin.Name);
}
}
}
public async Task AfterExecutionAsync(ScriptMetadata script, ExecutionResult result)
{
foreach (var plugin in _plugins)
{
try
{
// TODO: Добавить метод AfterExecution в ISqlVisionPlugin если нужно
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in plugin {Plugin} AfterExecution", plugin.Name);
}
}
}
private class PluginContext : IPluginContext
{
public IServiceProvider ServiceProvider { get; }
public IConfiguration Configuration { get; }
public ILogger Logger { get; }
public PluginContext(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
Configuration = serviceProvider.GetRequiredService<IConfiguration>();
Logger = serviceProvider.GetRequiredService<ILogger<PluginManager>>();
}
public Task ShowNotificationAsync(string message, NotificationType type)
{
// TODO: Реализовать показ уведомлений через UI
Logger.LogInformation("Plugin notification ({Type}): {Message}", type, message);
return Task.CompletedTask;
}
}
}

View File

@@ -0,0 +1,197 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using SQLVision.Core.Enums;
using SQLVision.Core.Interfaces;
using SQLVision.Core.Models;
using System.Collections.Concurrent;
namespace SQLVision.Services.Services;
public class ScriptManager : IScriptManager, IDisposable
{
private readonly ISqlScriptParser _parser;
private readonly ILogger<ScriptManager> _logger;
private readonly FileSystemWatcher _watcher;
private readonly ConcurrentDictionary<string, ScriptMetadata> _scripts;
private readonly string _scriptsDirectory;
public event EventHandler<ScriptChangedEventArgs>? ScriptChanged;
public event EventHandler<ScriptsReloadedEventArgs>? ScriptsReloaded;
public ScriptManager(ISqlScriptParser parser, IConfiguration configuration, ILogger<ScriptManager> logger)
{
_parser = parser;
_logger = logger;
_scripts = new ConcurrentDictionary<string, ScriptMetadata>();
_scriptsDirectory = configuration["Scripts:Directory"] ?? "Scripts";
_watcher = new FileSystemWatcher
{
Path = _scriptsDirectory,
Filter = "*.sql",
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName,
EnableRaisingEvents = false
};
_watcher.Changed += OnScriptChanged;
_watcher.Created += OnScriptCreated;
_watcher.Deleted += OnScriptDeleted;
_watcher.Renamed += OnScriptRenamed;
}
public async Task<IEnumerable<ScriptMetadata>> LoadScriptsAsync(string? directory = null)
{
var targetDirectory = directory ?? _scriptsDirectory;
if (!Directory.Exists(targetDirectory))
{
Directory.CreateDirectory(targetDirectory);
_logger.LogInformation("Created scripts directory: {Directory}", targetDirectory);
return Enumerable.Empty<ScriptMetadata>();
}
var sqlFiles = Directory.GetFiles(targetDirectory, "*.sql", SearchOption.AllDirectories);
var tasks = sqlFiles.Select(LoadScriptAsync);
var results = await Task.WhenAll(tasks);
_scripts.Clear();
foreach (var script in results.Where(s => s != null))
{
_scripts[script!.FullPath] = script;
}
StartWatching();
ScriptsReloaded?.Invoke(this, new ScriptsReloadedEventArgs(results.Where(s => s != null).ToList()!));
_logger.LogInformation("Loaded {Count} scripts from {Directory}", _scripts.Count, targetDirectory);
return _scripts.Values;
}
private async Task<ScriptMetadata?> LoadScriptAsync(string filePath)
{
try
{
return await _parser.ParseAsync(filePath);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error loading script: {FilePath}", filePath);
return null;
}
}
public async Task<ScriptMetadata> ReloadScriptAsync(string filePath)
{
try
{
var script = await LoadScriptAsync(filePath);
if (script != null)
{
_scripts[filePath] = script;
ScriptChanged?.Invoke(this, new ScriptChangedEventArgs(filePath, ScriptChangeType.Updated, script));
}
return script!;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error reloading script: {FilePath}", filePath);
throw;
}
}
private void OnScriptChanged(object sender, FileSystemEventArgs e)
{
// Задержка для избежания многократных вызовов
Task.Delay(300).ContinueWith(async _ =>
{
try
{
var script = await ReloadScriptAsync(e.FullPath);
if (script != null)
{
_logger.LogInformation("Script changed: {FileName}", e.Name);
}
}
catch { /* Игнорируем ошибки */ }
});
}
private void OnScriptCreated(object sender, FileSystemEventArgs e)
{
Task.Delay(300).ContinueWith(async _ =>
{
try
{
var script = await LoadScriptAsync(e.FullPath);
if (script != null)
{
_scripts[e.FullPath] = script;
ScriptChanged?.Invoke(this, new ScriptChangedEventArgs(e.FullPath, ScriptChangeType.Created, script));
_logger.LogInformation("Script created: {FileName}", e.Name);
}
}
catch { /* Игнорируем */ }
});
}
private void OnScriptDeleted(object sender, FileSystemEventArgs e)
{
if (_scripts.TryRemove(e.FullPath, out var script))
{
ScriptChanged?.Invoke(this, new ScriptChangedEventArgs(e.FullPath, ScriptChangeType.Deleted, script));
_logger.LogInformation("Script deleted: {FileName}", e.Name);
}
}
private void OnScriptRenamed(object sender, RenamedEventArgs e)
{
Task.Delay(300).ContinueWith(async _ =>
{
try
{
// Удаляем старый файл
_scripts.TryRemove(e.OldFullPath, out var s);
// Загружаем новый
var script = await LoadScriptAsync(e.FullPath);
if (script != null)
{
_scripts[e.FullPath] = script;
ScriptChanged?.Invoke(this,
new ScriptChangedEventArgs(e.FullPath, ScriptChangeType.Renamed, script));
_logger.LogInformation("Script renamed: {OldName} -> {NewName}",
Path.GetFileName(e.OldFullPath), e.Name);
}
}
catch { /* Игнорируем */ }
});
}
private void StartWatching()
{
if (!_watcher.EnableRaisingEvents)
{
_watcher.EnableRaisingEvents = true;
_logger.LogDebug("Started watching directory: {Directory}", _scriptsDirectory);
}
}
public void WatchDirectory(string directory, Action<string> onScriptChanged)
{
if (_watcher.EnableRaisingEvents)
{
_watcher.EnableRaisingEvents = false;
}
_watcher.Path = directory;
_watcher.EnableRaisingEvents = true;
ScriptChanged += (sender, e) => onScriptChanged?.Invoke(e.FilePath);
}
public void Dispose()
{
_watcher?.Dispose();
}
}

View File

@@ -0,0 +1,36 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using SQLVision.Core.Interfaces;
using SQLVision.Services.Exporters;
using SQLVision.Services.Parsers;
using SQLVision.Services.Services;
namespace SQLVision.Services;
public static class ServiceExtensions
{
public static IServiceCollection AddSqlVisionServices(this IServiceCollection services)
{
// Регистрация парсера
services.TryAddSingleton<ISqlScriptParser, SqlScriptParser>();
// Регистрация сервиса выполнения SQL
services.TryAddSingleton<ISqlExecutionService, SqlExecutionService>();
// Регистрация менеджера скриптов
services.TryAddSingleton<IScriptManager, ScriptManager>();
// Регистрация сервиса экспорта
services.TryAddSingleton<IExportService, ExportService>();
// Регистрация менеджера плагинов
services.TryAddSingleton<IPluginManager, PluginManager>();
// Регистрация экспортеров
services.TryAddSingleton<IExportHandler, ExcelExporter>();
services.TryAddSingleton<IExportHandler, CsvExporter>();
services.TryAddSingleton<IExportHandler, JsonExporter>();
return services;
}
}

View File

@@ -0,0 +1,305 @@
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using SQLVision.Core.Enums;
using SQLVision.Core.Interfaces;
using SQLVision.Core.Models;
using SQLVision.Services.Configuration;
using System.Collections.Concurrent;
using System.Data;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;
namespace SQLVision.Services.Services;
public class SqlExecutionService : ISqlExecutionService, IDisposable
{
private readonly IMemoryCache _cache;
private readonly ILogger<SqlExecutionService> _logger;
private readonly IOptions<DatabaseOptions> _options;
private readonly ConcurrentDictionary<string, Task<DataTable>> _loadingComboBoxData = new();
public SqlExecutionService(
IMemoryCache cache,
ILogger<SqlExecutionService> logger,
IOptions<DatabaseOptions> options)
{
_cache = cache;
_logger = logger;
_options = options;
}
public async Task<ExecutionResult> ExecuteAsync(
ScriptMetadata script,
Dictionary<string, object> parameters,
CancellationToken cancellationToken = default)
{
var stopwatch = Stopwatch.StartNew();
var result = new ExecutionResult
{
Parameters = new Dictionary<string, object>(parameters),
ExecutionDate = DateTime.UtcNow,
ConnectionName = script.ConnectionString
};
try
{
// Генерация ключа кэша
var cacheKey = GenerateCacheKey(script, parameters);
if (_options.Value.Cache.Enabled)
{
if (_cache.TryGetValue<ExecutionResult>(cacheKey, out var cachedResult))
{
_logger.LogDebug("Returning cached result for {Script}", script.FileName);
cachedResult!.IsFromCache = true;
cachedResult.ExecutionTime = stopwatch.Elapsed;
return cachedResult;
}
}
// Подготовка SQL с параметрами
var (processedSql, dbParameters) = PrepareSql(
script.ProcessedSql,
parameters,
script.DatabaseProvider);
result.ExecutedSql = processedSql;
// Выполнение запроса
var dataSet = await ExecuteQueryAsync(
processedSql,
dbParameters,
script.ConnectionString ?? _options.Value.DefaultConnection,
cancellationToken);
stopwatch.Stop();
result.Data = dataSet;
result.IsSuccess = true;
result.ExecutionTime = stopwatch.Elapsed;
result.RowCount = dataSet.Tables.Cast<DataTable>().Sum(t => t.Rows.Count);
result.Metrics = new Dictionary<string, object>
{
["ExecutionTimeMs"] = stopwatch.ElapsedMilliseconds,
["RowsAffected"] = result.RowCount,
["TablesCount"] = dataSet.Tables.Count
};
// Кэширование результата
if (_options.Value.Cache.Enabled && result.RowCount > 0)
{
var cacheOptions = new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(_options.Value.Cache.DurationMinutes),
Size = CalculateDataSetSize(dataSet)
};
_cache.Set(cacheKey, result, cacheOptions);
}
_logger.LogInformation(
"Executed {Script} in {ElapsedMs}ms, returned {Rows} rows",
script.FileName, stopwatch.ElapsedMilliseconds, result.RowCount);
return result;
}
catch (Exception ex)
{
stopwatch.Stop();
result.IsSuccess = false;
result.ErrorMessage = ex.Message;
result.ExecutionTime = stopwatch.Elapsed;
_logger.LogError(ex, "Error executing script {Script}", script.FileName);
return result;
}
}
private async Task<DataSet> ExecuteQueryAsync(
string sql,
List<SqlParameter> parameters,
string connectionString,
CancellationToken cancellationToken)
{
var dataSet = new DataSet();
await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync(cancellationToken);
await using var command = new SqlCommand(sql, connection);
command.CommandTimeout = _options.Value.CommandTimeout;
// Добавление параметров
command.Parameters.AddRange(parameters.ToArray());
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
do
{
var dataTable = new DataTable();
dataTable.Load(reader);
dataSet.Tables.Add(dataTable);
} while (!reader.IsClosed && await reader.NextResultAsync(cancellationToken));
return dataSet;
}
private (string Sql, List<SqlParameter> Parameters) PrepareSql(
string sql,
Dictionary<string, object> parameters,
DatabaseProvider provider)
{
var dbParameters = new List<SqlParameter>();
var processedSql = new StringBuilder(sql);
foreach (var (key, value) in parameters)
{
var paramName = $"@{key}";
var sqlParam = CreateSqlParameter(paramName, value);
dbParameters.Add(sqlParam);
}
return (processedSql.ToString(), dbParameters);
}
private SqlParameter CreateSqlParameter(string name, object? value)
{
var sqlParam = new SqlParameter(name, value ?? DBNull.Value);
// Автоматическое определение типа данных
if (value is DateTime dateTime)
{
sqlParam.SqlDbType = SqlDbType.DateTime2;
sqlParam.Value = dateTime;
}
else if (value is int intValue)
{
sqlParam.SqlDbType = SqlDbType.Int;
sqlParam.Value = intValue;
}
else if (value is decimal decimalValue)
{
sqlParam.SqlDbType = SqlDbType.Decimal;
sqlParam.Value = decimalValue;
}
else if (value is bool boolValue)
{
sqlParam.SqlDbType = SqlDbType.Bit;
sqlParam.Value = boolValue;
}
else if (value is string stringValue)
{
sqlParam.SqlDbType = SqlDbType.NVarChar;
sqlParam.Value = stringValue;
sqlParam.Size = Math.Min(stringValue.Length * 2, 4000); // Ограничение для NVARCHAR
}
return sqlParam;
}
public async Task<ExecutionResult> ExecuteAsync(
string sql,
Dictionary<string, object> parameters,
string connectionString,
CancellationToken cancellationToken = default)
{
var script = new ScriptMetadata
{
ProcessedSql = sql,
ConnectionString = connectionString,
DatabaseProvider = DatabaseProvider.SqlServer
};
return await ExecuteAsync(script, parameters, cancellationToken);
}
public async Task<bool> TestConnectionAsync(
string connectionString,
DatabaseProvider provider,
CancellationToken cancellationToken = default)
{
try
{
await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync(cancellationToken);
await connection.CloseAsync();
return true;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Connection test failed for {Provider}", provider);
return false;
}
}
public async Task<DataTable> LoadComboBoxDataAsync(
string query,
string connectionString,
DatabaseProvider provider,
CancellationToken cancellationToken = default)
{
var cacheKey = $"ComboBox_{provider}_{connectionString}_{query.GetHashCode()}";
return await _loadingComboBoxData.GetOrAdd(cacheKey, async key =>
{
try
{
await using var connection = new SqlConnection(connectionString);
await using var command = new SqlCommand(query, connection);
command.CommandTimeout = 30;
await connection.OpenAsync(cancellationToken);
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
var dataTable = new DataTable();
dataTable.Load(reader);
return dataTable;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load combo box data for query: {Query}", query);
throw;
}
finally
{
_loadingComboBoxData.TryRemove(key, out _);
}
});
}
private string GenerateCacheKey(ScriptMetadata script, Dictionary<string, object> parameters)
{
using var sha256 = SHA256.Create();
// Создаем строку для хэширования
var keyBuilder = new StringBuilder();
keyBuilder.Append(script.Id);
foreach (var param in parameters.OrderBy(p => p.Key))
{
keyBuilder.Append($"|{param.Key}={param.Value}");
}
var keyData = keyBuilder.ToString();
var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(keyData));
return Convert.ToBase64String(hash);
}
private long CalculateDataSetSize(DataSet dataSet)
{
long size = 0;
foreach (DataTable table in dataSet.Tables)
{
// Примерный расчет размера: кол-во строк * кол-во столбцов * средний размер
size += table.Rows.Count * table.Columns.Count * 64; // 64 байта на ячейку
}
return size;
}
public void Dispose()
{
// Очищаем кэш загрузки данных для ComboBox
_loadingComboBoxData.Clear();
}
}

View File

@@ -0,0 +1,397 @@
using Microsoft.UI;
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 System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
namespace SQLVision.UI.Controls;
public class ControlFactory : IControlFactory
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<ControlFactory> _logger;
public ControlFactory(IServiceProvider serviceProvider, ILogger<ControlFactory> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}
public FrameworkElement CreateControl(ScriptParameter parameter, Action<object> onValueChanged)
{
try
{
var control = parameter.Type switch
{
ParameterType.String => CreateTextBox(parameter, onValueChanged),
ParameterType.Int => CreateNumberBox(parameter, onValueChanged),
ParameterType.Decimal => CreateNumberBox(parameter, onValueChanged, true),
ParameterType.DateTime => CreateDatePicker(parameter, onValueChanged),
ParameterType.Bool => CreateCheckBox(parameter, onValueChanged),
ParameterType.Table => CreateComboBox(parameter, onValueChanged),
ParameterType.MultiSelect => CreateListBox(parameter, onValueChanged),
ParameterType.Color => CreateColorPicker(parameter, onValueChanged),
ParameterType.File => CreateFilePicker(parameter, onValueChanged),
ParameterType.Json => CreateJsonEditor(parameter, onValueChanged),
_ => CreateTextBox(parameter, onValueChanged)
};
// Настройка общих свойств
control.Tag = parameter;
control.IsEnabled = !parameter.IsRequired || parameter.DefaultValue == null;
// Добавление подсказки
if (!string.IsNullOrEmpty(parameter.Description))
{
ToolTipService.SetToolTip(control, parameter.Description);
}
return control;
}
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 textBox = new TextBox
{
Header = parameter.DisplayName,
PlaceholderText = parameter.Watermark ?? $"Введите {parameter.DisplayName.ToLower()}",
Text = parameter.DefaultValue?.ToString() ?? string.Empty
};
textBox.TextChanged += (s, e) => onValueChanged(textBox.Text);
return textBox;
}
private FrameworkElement CreateNumberBox(ScriptParameter parameter, Action<object> onValueChanged, bool isDecimal = false)
{
var numberBox = new NumberBox
{
Header = parameter.DisplayName,
PlaceholderText = parameter.Watermark ?? $"Введите {parameter.DisplayName.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);
}
return numberBox;
}
private FrameworkElement CreateDatePicker(ScriptParameter parameter, Action<object> onValueChanged)
{
var datePicker = new DatePicker
{
Header = parameter.DisplayName,
Date = parameter.DefaultValue is DateTime defaultDate ?
DateTimeOffset.Parse(defaultDate.ToString()) : DateTimeOffset.Now
};
datePicker.DateChanged += (s, e) => onValueChanged(e.NewDate.DateTime);
return datePicker;
}
private FrameworkElement CreateCheckBox(ScriptParameter parameter, Action<object> onValueChanged)
{
var checkBox = new CheckBox
{
Content = parameter.DisplayName,
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 comboBox = new ComboBox
{
Header = parameter.DisplayName,
PlaceholderText = parameter.Watermark ?? $"Выберите {parameter.DisplayName.ToLower()}",
DisplayMemberPath = parameter.DisplayMember,
SelectedValuePath = parameter.ValueMember
};
// Загрузка данных асинхронно
LoadComboBoxDataAsync(comboBox, parameter).ConfigureAwait(false);
comboBox.SelectionChanged += (s, e) =>
{
if (comboBox.SelectedValue != null)
onValueChanged(comboBox.SelectedValue);
};
return comboBox;
}
private async Task LoadComboBoxDataAsync(ComboBox comboBox, ScriptParameter parameter)
{
if (string.IsNullOrEmpty(parameter.TableQuery)) return;
try
{
// Используем сервис выполнения SQL для загрузки данных
var executionService = _serviceProvider.GetService<ISqlExecutionService>();
var result = await executionService.ExecuteAsync(
parameter.TableQuery,
new Dictionary<string, object>(),
GetConnectionString());
if (result.IsSuccess && result.Data.Tables.Count > 0)
{
comboBox.ItemsSource = result.Data.Tables[0].DefaultView;
// Установка значения по умолчанию
if (parameter.DefaultValue != null)
{
comboBox.SelectedValue = parameter.DefaultValue;
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error loading combo box data for {Parameter}", parameter.Name);
}
}
private FrameworkElement CreateListBox(ScriptParameter parameter, Action<object> onValueChanged)
{
var listBox = new ListBox
{
Header = parameter.DisplayName,
SelectionMode = ListViewSelectionMode.Multiple
};
// Загрузка данных для ListBox
// Аналогично ComboBox
listBox.SelectionChanged += (s, e) =>
{
var selectedValues = listBox.SelectedItems.Cast<DataRowView>()
.Select(r => r[parameter.ValueMember])
.ToList();
onValueChanged(selectedValues);
};
return listBox;
}
private FrameworkElement CreateColorPicker(ScriptParameter parameter, Action<object> onValueChanged)
{
var colorPicker = new ColorPicker
{
Header = parameter.DisplayName,
ColorSpectrumShape = ColorSpectrumShape.Box,
IsMoreButtonVisible = true,
IsColorSliderVisible = true,
IsColorChannelTextInputVisible = true,
IsHexInputVisible = true
};
if (parameter.DefaultValue is string defaultColor)
{
if (ColorHelper.TryParse(defaultColor, out var color))
{
colorPicker.Color = color;
}
}
colorPicker.ColorChanged += (s, e) =>
onValueChanged($"#{e.NewColor.R:X2}{e.NewColor.G:X2}{e.NewColor.B:X2}");
return colorPicker;
}
private FrameworkElement CreateFilePicker(ScriptParameter parameter, Action<object> onValueChanged)
{
var stackPanel = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 };
var textBox = new TextBox
{
Header = parameter.DisplayName,
PlaceholderText = "Путь к файлу",
Width = 200,
IsReadOnly = true
};
var button = new Button
{
Content = "Выбрать",
VerticalAlignment = VerticalAlignment.Bottom
};
button.Click += async (s, e) =>
{
var openPicker = new FileOpenPicker();
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);
}
};
stackPanel.Children.Add(textBox);
stackPanel.Children.Add(button);
return stackPanel;
}
private FrameworkElement CreateJsonEditor(ScriptParameter parameter, Action<object> onValueChanged)
{
var textBox = new TextBox
{
Header = parameter.DisplayName,
PlaceholderText = "Введите JSON",
AcceptsReturn = true,
TextWrapping = TextWrapping.Wrap,
Height = 100,
FontFamily = new FontFamily("Consolas")
};
if (parameter.DefaultValue != null)
{
textBox.Text = JsonSerializer.Serialize(parameter.DefaultValue,
new JsonSerializerOptions { WriteIndented = true });
}
textBox.TextChanged += (s, e) =>
{
try
{
var json = JsonSerializer.Deserialize<JsonElement>(textBox.Text);
onValueChanged(json);
}
catch
{
// Игнорируем ошибки парсинга JSON
}
};
return textBox;
}
private FrameworkElement CreateFallbackControl(ScriptParameter parameter, Action<object> onValueChanged)
{
return new TextBox
{
Header = parameter.DisplayName,
Text = $"Ошибка создания контрола для типа {parameter.Type}",
IsReadOnly = true,
Foreground = new SolidColorBrush(Colors.Red)
};
}
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)
{
switch (control)
{
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;
}
}
private string GetConnectionString()
{
// Получение строки подключения из конфигурации
var configuration = _serviceProvider.GetService<IConfiguration>();
return configuration.GetConnectionString("Default") ??
configuration["Database:DefaultConnection"];
}
}

View File

@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>SQLVision.UI</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Platforms>x86;x64;ARM64</Platforms>
<RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers>
<UseWinUI>true</UseWinUI>
<WindowsPackageType>None</WindowsPackageType>
<EnableMsixTooling>true</EnableMsixTooling>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<!-- Все зависимости -->
<ProjectReference Include="..\SQLVision.Services\SQLVision.Services.csproj" />
<ProjectReference Include="..\SQLVision.Visualizers\SQLVision.Visualizers.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.4.230913002" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.2428" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,528 @@
using SQLVision.Core.Enums;
using SQLVision.Core.Interfaces;
using SQLVision.Core.Models;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Windows.ApplicationModel.DataTransfer;
namespace SQLVision.UI.ViewModels;
public class MainViewModel : ObservableObject
{
private readonly IScriptManager _scriptManager;
private readonly ISqlExecutionService _executionService;
private readonly IExportService _exportService;
private readonly IControlFactory _controlFactory;
private readonly IVisualizerFactory _visualizerFactory;
private readonly ILogger<MainViewModel> _logger;
private readonly ObservableCollection<ScriptMetadata> _scripts = new();
private readonly ObservableCollection<ScriptCategory> _scriptCategories = new();
private readonly ObservableCollection<ExecutionHistoryItem> _history = new();
private readonly ObservableCollection<ResultTabViewModel> _resultTabs = new();
private ScriptMetadata _selectedScript;
private bool _isBusy;
private string _statusMessage;
private ResultTabViewModel _selectedResultTab;
private string _searchText;
public ObservableCollection<ScriptCategory> ScriptCategories => _scriptCategories;
public ObservableCollection<ExecutionHistoryItem> History => _history;
public ObservableCollection<ResultTabViewModel> ResultTabs => _resultTabs;
public ScriptMetadata SelectedScript
{
get => _selectedScript;
set
{
if (SetProperty(ref _selectedScript, value))
{
OnSelectedScriptChanged();
}
}
}
public bool IsBusy
{
get => _isBusy;
set => SetProperty(ref _isBusy, value);
}
public string StatusMessage
{
get => _statusMessage;
set => SetProperty(ref _statusMessage, value);
}
public ResultTabViewModel SelectedResultTab
{
get => _selectedResultTab;
set => SetProperty(ref _selectedResultTab, value);
}
public string SearchText
{
get => _searchText;
set
{
if (SetProperty(ref _searchText, value))
{
FilterScripts();
}
}
}
public ObservableCollection<ParameterViewModel> Parameters { get; } = new();
public IAsyncRelayCommand LoadScriptsCommand { get; }
public IAsyncRelayCommand ExecuteCommand { get; }
public IAsyncRelayCommand ExportCommand { get; }
public IRelayCommand CopySqlCommand { get; }
public IRelayCommand ClearResultsCommand { get; }
public IRelayCommand SaveParametersCommand { get; }
public IRelayCommand LoadParametersCommand { get; }
public MainViewModel(
IScriptManager scriptManager,
ISqlExecutionService executionService,
IExportService exportService,
IControlFactory controlFactory,
IVisualizerFactory visualizerFactory,
ILogger<MainViewModel> logger)
{
_scriptManager = scriptManager;
_executionService = executionService;
_exportService = exportService;
_controlFactory = controlFactory;
_visualizerFactory = visualizerFactory;
_logger = logger;
LoadScriptsCommand = new AsyncRelayCommand(LoadScriptsAsync);
ExecuteCommand = new AsyncRelayCommand(ExecuteScriptAsync, CanExecuteScript);
ExportCommand = new AsyncRelayCommand(ExportResultsAsync, CanExportResults);
CopySqlCommand = new RelayCommand(CopySqlToClipboard);
ClearResultsCommand = new RelayCommand(ClearResults);
SaveParametersCommand = new RelayCommand(SaveParameters);
LoadParametersCommand = new RelayCommand(LoadParameters);
// Подписка на события
_scriptManager.ScriptChanged += OnScriptChanged;
_scriptManager.ScriptsReloaded += OnScriptsReloaded;
}
private async Task LoadScriptsAsync()
{
try
{
IsBusy = true;
StatusMessage = "Загрузка скриптов...";
var scripts = await _scriptManager.LoadScriptsAsync();
_scripts.Clear();
foreach (var script in scripts)
{
_scripts.Add(script);
}
CategorizeScripts();
StatusMessage = $"Загружено {_scripts.Count} скриптов";
}
catch (Exception ex)
{
_logger.LogError(ex, "Error loading scripts");
StatusMessage = $"Ошибка загрузки: {ex.Message}";
}
finally
{
IsBusy = false;
}
}
private void CategorizeScripts()
{
_scriptCategories.Clear();
var categories = _scripts
.GroupBy(s => s.Category ?? "Без категории")
.OrderBy(g => g.Key);
foreach (var category in categories)
{
var scriptCategory = new ScriptCategory
{
Name = category.Key,
Scripts = new ObservableCollection<ScriptMetadata>(category.OrderBy(s => s.FileName))
};
_scriptCategories.Add(scriptCategory);
}
}
private void FilterScripts()
{
if (string.IsNullOrWhiteSpace(SearchText))
{
// Показать все скрипты
foreach (var category in _scriptCategories)
{
foreach (var script in category.Scripts)
{
script.IsVisible = true;
}
}
}
else
{
var searchLower = SearchText.ToLower();
foreach (var category in _scriptCategories)
{
foreach (var script in category.Scripts)
{
script.IsVisible =
script.FileName.ToLower().Contains(searchLower) ||
script.Description?.ToLower().Contains(searchLower) == true ||
script.Tags.Any(t => t.ToLower().Contains(searchLower));
}
}
}
}
private void OnSelectedScriptChanged()
{
Parameters.Clear();
if (_selectedScript == null) return;
// Создание ViewModel для каждого параметра
foreach (var param in _selectedScript.Parameters.OrderBy(p => p.Order))
{
var paramVm = new ParameterViewModel(param, _controlFactory);
paramVm.ValueChanged += OnParameterValueChanged;
Parameters.Add(paramVm);
}
// Восстановление сохраненных значений
LoadSavedParameters();
}
private void OnParameterValueChanged(object sender, EventArgs e)
{
ExecuteCommand.NotifyCanExecuteChanged();
// Обновление зависимых параметров
if (sender is ParameterViewModel changedParam)
{
UpdateDependentParameters(changedParam);
}
}
private void UpdateDependentParameters(ParameterViewModel changedParam)
{
foreach (var paramVm in Parameters)
{
if (paramVm.Parameter.DependsOn == changedParam.Parameter.Name)
{
paramVm.UpdateDependencies(GetCurrentParameterValues());
}
}
}
private Dictionary<string, object> GetCurrentParameterValues()
{
return Parameters.ToDictionary(
p => p.Parameter.Name,
p => p.Value ?? p.Parameter.DefaultValue);
}
private bool CanExecuteScript()
{
if (_selectedScript == null) return false;
// Проверка обязательных параметров
foreach (var paramVm in Parameters)
{
if (paramVm.Parameter.IsRequired &&
(paramVm.Value == null || string.IsNullOrWhiteSpace(paramVm.Value.ToString())))
{
return false;
}
}
return true;
}
private async Task ExecuteScriptAsync()
{
try
{
IsBusy = true;
StatusMessage = "Выполнение скрипта...";
var parameters = GetCurrentParameterValues();
var result = await _executionService.ExecuteAsync(_selectedScript, parameters);
// Добавление в историю
var historyItem = new ExecutionHistoryItem
{
ScriptId = _selectedScript.Id,
ScriptName = _selectedScript.FileName,
ExecutionTime = DateTime.Now,
Duration = result.ExecutionTime,
Success = result.IsSuccess,
Parameters = new Dictionary<string, object>(parameters),
RowCount = result.RowCount,
ErrorMessage = result.ErrorMessage,
ExecutedSql = result.ExecutedSql
};
_history.Insert(0, historyItem);
// Очистка старых записей истории
while (_history.Count > 1000)
{
_history.RemoveAt(_history.Count - 1);
}
if (result.IsSuccess)
{
CreateResultTabs(result);
StatusMessage = $"Выполнено за {result.ExecutionTime.TotalSeconds:F2} сек. Получено строк: {result.RowCount}";
}
else
{
StatusMessage = $"Ошибка: {result.ErrorMessage}";
ShowErrorDialog(result.ErrorMessage);
}
// Сохранение параметров
SaveParameters();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error executing script");
StatusMessage = $"Ошибка: {ex.Message}";
ShowErrorDialog(ex.Message);
}
finally
{
IsBusy = false;
}
}
private void CreateResultTabs(ExecutionResult result)
{
// Удаляем старые вкладки
ResultTabs.Clear();
for (int i = 0; i < result.Data.Tables.Count; i++)
{
var table = result.Data.Tables[i];
var outputDef = i < _selectedScript.Outputs.Count
? _selectedScript.Outputs[i]
: CreateDefaultOutputDefinition(table, i);
var visualizer = _visualizerFactory.GetVisualizer(outputDef.Type);
var content = visualizer.Visualize(table, outputDef);
var tabVm = new ResultTabViewModel
{
Title = outputDef.Description,
Content = content,
DataTable = table,
OutputDefinition = outputDef,
CanExport = true,
CanCopy = true
};
ResultTabs.Add(tabVm);
}
if (ResultTabs.Any())
{
SelectedResultTab = ResultTabs[0];
}
}
private OutputDefinition CreateDefaultOutputDefinition(DataTable table, int index)
{
return new OutputDefinition
{
Type = OutputType.Table,
Description = $"Результат {index + 1}",
DataTableName = table.TableName
};
}
private bool CanExportResults()
{
return SelectedResultTab != null &&
SelectedResultTab.DataTable != null &&
SelectedResultTab.DataTable.Rows.Count > 0;
}
private async Task ExportResultsAsync()
{
if (SelectedResultTab?.DataTable == null) return;
try
{
var savePicker = new FileSavePicker();
savePicker.SuggestedStartLocation = PickerLocationId.DocumentsLibrary;
savePicker.FileTypeChoices.Add("Excel файл", new List<string> { ".xlsx" });
savePicker.FileTypeChoices.Add("CSV файл", new List<string> { ".csv" });
savePicker.FileTypeChoices.Add("JSON файл", new List<string> { ".json" });
savePicker.SuggestedFileName = $"{_selectedScript.FileName}_{DateTime.Now:yyyyMMdd_HHmmss}";
var file = await savePicker.PickSaveFileAsync();
if (file != null)
{
var options = new ExportOptions
{
Format = Path.GetExtension(file.Path).TrimStart('.').ToUpper(),
IncludeHeaders = true,
AutoFilter = true
};
await _exportService.ExportAsync(SelectedResultTab.DataTable, file.Path, options);
StatusMessage = $"Экспортировано в {file.Path}";
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error exporting results");
StatusMessage = $"Ошибка экспорта: {ex.Message}";
}
}
private void CopySqlToClipboard()
{
if (_selectedScript == null) return;
var parameters = GetCurrentParameterValues();
var sql = FormatSqlWithParameters(_selectedScript.ProcessedSql, parameters);
var package = new DataPackage();
package.SetText(sql);
Clipboard.SetContent(package);
StatusMessage = "SQL скопирован в буфер обмена";
}
private string FormatSqlWithParameters(string sql, Dictionary<string, object> parameters)
{
var result = new StringBuilder(sql);
foreach (var param in parameters)
{
var paramName = $"@{param.Key}";
var paramValue = FormatParameterForDisplay(param.Value);
result = result.Replace(paramName, paramValue);
}
return result.ToString();
}
private string FormatParameterForDisplay(object value)
{
if (value == null) return "NULL";
return value switch
{
string str => $"N'{str.Replace("'", "''")}'",
DateTime dt => $"'{dt:yyyy-MM-dd HH:mm:ss}'",
bool b => b ? "1" : "0",
_ => value.ToString()
};
}
private void ClearResults()
{
ResultTabs.Clear();
StatusMessage = "Результаты очищены";
}
private void SaveParameters()
{
if (_selectedScript == null) return;
var parameters = GetCurrentParameterValues();
var settings = ApplicationData.Current.LocalSettings;
var dict = new Dictionary<string, object>();
foreach (var param in parameters)
{
dict[param.Key] = param.Value;
}
var json = JsonSerializer.Serialize(dict);
settings.Values[$"ScriptParams_{_selectedScript.Id}"] = json;
StatusMessage = "Параметры сохранены";
}
private void LoadParameters()
{
if (_selectedScript == null) return;
var settings = ApplicationData.Current.LocalSettings;
if (settings.Values.TryGetValue($"ScriptParams_{_selectedScript.Id}", out var jsonObj))
{
try
{
var savedParams = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonObj.ToString());
foreach (var paramVm in Parameters)
{
if (savedParams.TryGetValue(paramVm.Parameter.Name, out var value))
{
paramVm.Value = value;
}
}
StatusMessage = "Параметры загружены";
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error loading saved parameters");
}
}
}
private void ShowErrorDialog(string message)
{
// Реализация диалога ошибки
// Можно использовать ContentDialog или другое UI решение
}
private void OnScriptChanged(object sender, ScriptChangedEventArgs e)
{
// Обновление UI при изменении скрипта
DispatcherQueue.GetForCurrentThread().TryEnqueue(() =>
{
CategorizeScripts();
if (e.ChangeType == ScriptChangeType.Deleted &&
_selectedScript?.FullPath == e.FilePath)
{
SelectedScript = null;
}
});
}
private void OnScriptsReloaded(object sender, ScriptsReloadedEventArgs e)
{
DispatcherQueue.GetForCurrentThread().TryEnqueue(CategorizeScripts);
}
}

View File

@@ -0,0 +1,124 @@
using Microsoft.UI.Xaml;
using SQLVision.Core.Enums;
using SQLVision.Core.Interfaces;
using SQLVision.Core.Models;
using System;
using System.Collections.Generic;
using System.Linq;
namespace SQLVision.UI.ViewModels;
public class ParameterViewModel : ObservableObject
{
private readonly IControlFactory _controlFactory;
private FrameworkElement _control;
private object _value;
private bool _isEnabled = true;
private string _validationError;
public ScriptParameter Parameter { get; }
public string Name => Parameter.Name;
public string DisplayName => Parameter.DisplayName ?? Parameter.Name;
public string Description => Parameter.Description;
public ParameterType Type => Parameter.Type;
public FrameworkElement Control
{
get
{
if (_control == null)
{
_control = _controlFactory.CreateControl(Parameter, OnValueChanged);
}
return _control;
}
}
public object Value
{
get => _value ?? Parameter.DefaultValue;
set
{
if (SetProperty(ref _value, value))
{
ValidateValue();
ValueChanged?.Invoke(this, EventArgs.Empty);
}
}
}
public bool IsEnabled
{
get => _isEnabled;
set => SetProperty(ref _isEnabled, value);
}
public string ValidationError
{
get => _validationError;
private set => SetProperty(ref _validationError, value);
}
public bool HasError => !string.IsNullOrEmpty(ValidationError);
public event EventHandler ValueChanged;
public ParameterViewModel(ScriptParameter parameter, IControlFactory controlFactory)
{
Parameter = parameter ?? throw new ArgumentNullException(nameof(parameter));
_controlFactory = controlFactory ?? throw new ArgumentNullException(nameof(controlFactory));
_value = parameter.DefaultValue;
}
private void OnValueChanged(object value)
{
Value = value;
}
private void ValidateValue()
{
if (Parameter.IsValid(Value))
{
ValidationError = null;
}
else
{
ValidationError = "Неверное значение параметра";
}
}
public void UpdateDependencies(Dictionary<string, object> currentValues)
{
if (string.IsNullOrEmpty(Parameter.DependsOn))
{
IsEnabled = true;
return;
}
if (!currentValues.TryGetValue(Parameter.DependsOn, out var dependencyValue))
{
IsEnabled = false;
return;
}
// Проверка условий зависимости
if (Parameter.DependencyValues != null)
{
var isEnabled = Parameter.DependencyValues
.Any(kvp => object.Equals(kvp.Value, dependencyValue));
IsEnabled = isEnabled;
}
else
{
IsEnabled = dependencyValue != null &&
!string.IsNullOrWhiteSpace(dependencyValue.ToString());
}
if (!IsEnabled)
{
Value = null;
}
}
}

View File

@@ -0,0 +1,70 @@
using Microsoft.UI.Xaml;
using SQLVision.Core.Models;
using System.Data;
namespace SQLVision.UI.ViewModels;
public class ResultTabViewModel : ObservableObject
{
private string _title;
private FrameworkElement _content;
private DataTable _dataTable;
private OutputDefinition _outputDefinition;
private bool _canExport;
private bool _canCopy;
public string Title
{
get => _title;
set => SetProperty(ref _title, value);
}
public FrameworkElement Content
{
get => _content;
set => SetProperty(ref _content, value);
}
public DataTable DataTable
{
get => _dataTable;
set => SetProperty(ref _dataTable, value);
}
public OutputDefinition OutputDefinition
{
get => _outputDefinition;
set => SetProperty(ref _outputDefinition, value);
}
public bool CanExport
{
get => _canExport;
set => SetProperty(ref _canExport, value);
}
public bool CanCopy
{
get => _canCopy;
set => SetProperty(ref _canCopy, value);
}
public IRelayCommand ExportCommand { get; }
public IRelayCommand CopyDataCommand { get; }
public ResultTabViewModel()
{
ExportCommand = new RelayCommand(Export);
CopyDataCommand = new RelayCommand(CopyData);
}
private void Export()
{
// Экспорт данных вкладки
}
private void CopyData()
{
// Копирование данных в буфер обмена
}
}

View File

@@ -0,0 +1,29 @@
using SQLVision.Core.Models;
using System.Collections.ObjectModel;
namespace SQLVision.UI.ViewModels;
public class ScriptCategory : ObservableObject
{
private string _name;
private ObservableCollection<ScriptMetadata> _scripts;
private bool _isExpanded = true;
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
public ObservableCollection<ScriptMetadata> Scripts
{
get => _scripts;
set => SetProperty(ref _scripts, value);
}
public bool IsExpanded
{
get => _isExpanded;
set => SetProperty(ref _isExpanded, value);
}
}

View File

@@ -0,0 +1,359 @@
<Window
x:Class="SQLVision.UI.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="using:SQLVision.UI.Controls"
xmlns:charts="using:LiveChartsCore.SkiaSharpView.WinUI"
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
xmlns:converters="using:SQLVision.UI.Converters"
mc:Ignorable="d"
Title="SQLVision"
Width="1200"
Height="800"
MinWidth="800"
MinHeight="600">
<Window.Resources>
<converters:BooleanToVisibilityConverter x:Key="BoolToVisibility"/>
<converters:InverseBooleanConverter x:Key="InverseBool"/>
<Style TargetType="Button" x:Key="IconButtonStyle">
<Setter Property="Margin" Value="2"/>
<Setter Property="Padding" Value="8,4"/>
<Setter Property="MinWidth" Value="80"/>
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Панель инструментов -->
<CommandBar Grid.Row="0" DefaultLabelPosition="Right">
<AppBarButton
Icon="Refresh"
Label="Обновить скрипты"
Command="{x:Bind ViewModel.LoadScriptsCommand}"/>
<AppBarSeparator/>
<AppBarButton
Icon="Play"
Label="Выполнить"
Command="{x:Bind ViewModel.ExecuteCommand}"
IsEnabled="{x:Bind ViewModel.ExecuteCommand.IsRunning, Converter={StaticResource InverseBool}, Mode=OneWay}"/>
<AppBarButton
Icon="Save"
Label="Экспорт"
Command="{x:Bind ViewModel.ExportCommand}"/>
<AppBarButton
Icon="Copy"
Label="Копировать SQL"
Command="{x:Bind ViewModel.CopySqlCommand}"/>
<AppBarSeparator/>
<AppBarButton
Icon="Clear"
Label="Очистить результаты"
Command="{x:Bind ViewModel.ClearResultsCommand}"/>
<AppBarSeparator/>
<AppBarButton
Icon="SaveLocal"
Label="Сохранить параметры"
Command="{x:Bind ViewModel.SaveParametersCommand}"/>
<AppBarButton
Icon="OpenLocal"
Label="Загрузить параметры"
Command="{x:Bind ViewModel.LoadParametersCommand}"/>
<CommandBar.Content>
<TextBlock
Text="SQLVision"
FontSize="18"
FontWeight="SemiBold"
Margin="12,0"/>
</CommandBar.Content>
</CommandBar>
<!-- Основное содержимое -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="300" MinWidth="250"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Левая панель: скрипты и параметры -->
<Grid Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Поиск -->
<AutoSuggestBox
Grid.Row="0"
PlaceholderText="Поиск скриптов..."
Text="{x:Bind ViewModel.SearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Margin="8">
<AutoSuggestBox.QueryIcon>
<SymbolIcon Symbol="Find"/>
</AutoSuggestBox.QueryIcon>
</AutoSuggestBox>
<!-- Дерево скриптов -->
<TreeView
Grid.Row="1"
ItemsSource="{x:Bind ViewModel.ScriptCategories, Mode=OneWay}"
SelectedItem="{x:Bind ViewModel.SelectedScript, Mode=TwoWay}"
Margin="8">
<TreeView.ItemTemplate>
<DataTemplate x:DataType="viewmodels:ScriptCategory">
<TreeViewItem
ItemsSource="{x:Bind Scripts}"
IsExpanded="{x:Bind IsExpanded, Mode=TwoWay}">
<TreeViewItem.Header>
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="{x:Bind Name}" FontWeight="SemiBold"/>
<TextBlock
Text="{x:Bind Scripts.Count}"
Foreground="Gray"
FontSize="12"/>
</StackPanel>
</TreeViewItem.Header>
<TreeViewItem.ItemTemplate>
<DataTemplate x:DataType="models:ScriptMetadata">
<TreeViewItem>
<StackPanel Orientation="Vertical" Spacing="2">
<TextBlock
Text="{x:Bind FileName}"
TextWrapping="Wrap"
FontWeight="Normal"/>
<TextBlock
Text="{x:Bind Description}"
FontSize="11"
Foreground="Gray"
TextWrapping="Wrap"
MaxLines="2"
TextTrimming="CharacterEllipsis"/>
</StackPanel>
</TreeViewItem>
</DataTemplate>
</TreeViewItem.ItemTemplate>
</TreeViewItem>
</DataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<!-- Параметры скрипта -->
<Border
Grid.Row="2"
BorderBrush="{ThemeResource SystemControlForegroundBaseLowBrush}"
BorderThickness="0,1,0,0"
Padding="8"
MaxHeight="400">
<ScrollViewer>
<StackPanel
x:Name="ParametersPanel"
Spacing="12"
Visibility="{x:Bind ViewModel.SelectedScript, Converter={StaticResource NullToVisibilityConverter}, Mode=OneWay}">
<TextBlock
Text="Параметры"
FontSize="16"
FontWeight="SemiBold"
Margin="0,0,0,8"/>
<ItemsControl ItemsSource="{x:Bind ViewModel.Parameters, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="viewmodels:ParameterViewModel">
<StackPanel Spacing="4" Margin="0,0,0,8">
<ContentPresenter Content="{x:Bind Control}"/>
<TextBlock
Text="{x:Bind ValidationError}"
Foreground="Red"
FontSize="11"
Visibility="{x:Bind HasError, Converter={StaticResource BoolToVisibility}}"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>
</Border>
</Grid>
<!-- Splitter -->
<GridSplitter
Grid.Column="1"
Width="4"
HorizontalAlignment="Stretch"
Background="Transparent"/>
<!-- Правая панель: результаты -->
<Grid Grid.Column="2">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Вкладки с результатами -->
<muxc:TabView
Grid.Row="0"
ItemsSource="{x:Bind ViewModel.ResultTabs, Mode=OneWay}"
SelectedItem="{x:Bind ViewModel.SelectedResultTab, Mode=TwoWay}"
TabWidthMode="SizeToContent"
CanReorderTabs="True"
CanCloseTabs="True"
TabCloseRequested="OnTabCloseRequested">
<muxc:TabView.TabItemTemplate>
<DataTemplate x:DataType="viewmodels:ResultTabViewModel">
<muxc:TabViewItem
Header="{x:Bind Title}"
IconSource="{x:Bind IconSource}">
<ScrollViewer
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<ContentPresenter Content="{x:Bind Content}"/>
</ScrollViewer>
<muxc:TabViewItem.ContextFlyout>
<MenuFlyout>
<MenuFlyoutItem
Text="Экспорт"
Command="{x:Bind ExportCommand}"
Icon="Save"/>
<MenuFlyoutItem
Text="Копировать данные"
Command="{x:Bind CopyDataCommand}"
Icon="Copy"/>
<MenuFlyoutSeparator/>
<MenuFlyoutItem
Text="Закрыть"
Click="OnCloseTabClick"/>
</MenuFlyout>
</muxc:TabViewItem.ContextFlyout>
</muxc:TabViewItem>
</DataTemplate>
</muxc:TabView.TabItemTemplate>
</muxc:TabView>
<!-- Панель истории -->
<Grid
Grid.Row="1"
Visibility="{x:Bind ViewModel.History.Count, Converter={StaticResource CountToVisibilityConverter}, Mode=OneWay}">
<Expander
Header="История выполненных запросов"
IsExpanded="False"
Margin="8">
<ListView
ItemsSource="{x:Bind ViewModel.History, Mode=OneWay}"
MaxHeight="200"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="models:ExecutionHistoryItem">
<Grid Padding="8" BorderBrush="LightGray" BorderThickness="0,0,0,1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock
Text="{x:Bind ScriptName}"
FontWeight="SemiBold"/>
<TextBlock
Text="{x:Bind ExecutionTime, StringFormat='{}{0:dd.MM.yyyy HH:mm:ss}'}"
FontSize="11"
Foreground="Gray"/>
</StackPanel>
<TextBlock
Grid.Column="1"
Text="{x:Bind Duration, StringFormat='{}{0:mm\\:ss}'}"
Margin="8,0"
VerticalAlignment="Center"/>
<TextBlock
Grid.Column="2"
Text="{x:Bind RowCount, StringFormat='{}Строк: {0}'}"
VerticalAlignment="Center"/>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Expander>
</Grid>
</Grid>
</Grid>
<!-- Статус бар -->
<StatusBar Grid.Row="2">
<StatusBarItem>
<ProgressRing
Width="16"
Height="16"
IsActive="{x:Bind ViewModel.IsBusy, Mode=OneWay}"/>
</StatusBarItem>
<StatusBarItem>
<TextBlock Text="{x:Bind ViewModel.StatusMessage, Mode=OneWay}"/>
</StatusBarItem>
<StatusBarItem HorizontalAlignment="Right">
<TextBlock>
<Run Text="Скриптов:"/>
<Run Text="{x:Bind ViewModel.ScriptCategories.Sum(c => c.Scripts.Count), Mode=OneWay}"/>
<Run Text="|"/>
<Run Text="Вкладок:"/>
<Run Text="{x:Bind ViewModel.ResultTabs.Count, Mode=OneWay}"/>
</TextBlock>
</StatusBarItem>
</StatusBar>
<!-- Прогресс выполнения -->
<Grid
Grid.Row="0"
Grid.RowSpan="3"
Background="#CC000000"
Visibility="{x:Bind ViewModel.IsBusy, Converter={StaticResource BoolToVisibility}, Mode=OneWay}">
<Border
Background="{ThemeResource SystemControlBackgroundAltHighBrush}"
CornerRadius="8"
Padding="24"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<StackPanel Spacing="16" HorizontalAlignment="Center">
<ProgressRing Width="40" Height="40" IsActive="True"/>
<TextBlock
Text="Выполнение запроса..."
HorizontalAlignment="Center"/>
</StackPanel>
</Border>
</Grid>
</Grid>
</Window>

View File

@@ -0,0 +1,169 @@
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using SQLVision.Core.Models;
using SQLVision.Services;
using System;
using System.Collections.Generic;
using System.IO;
using Windows.Graphics;
namespace SQLVision.UI.Views
{
public sealed partial class MainWindow : Window
{
// Õðàíèì òåêóùèå ìåòàäàííûå âûáðàííîãî ñêðèïòà
private ScriptMetadata currentMetadata;
public MainWindow()
{
this.InitializeComponent();
SetWindowSize(1200, 800);
LoadScripts();
}
private void SetWindowSize(int width, int height)
{
var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
var windowId = Microsoft.UI.Win32Interop.GetWindowIdFromWindow(hwnd);
var appWindow = AppWindow.GetFromWindowId(windowId);
appWindow.Resize(new SizeInt32(width, height));
}
private void LoadScripts()
{
var folder = Path.Combine(AppContext.BaseDirectory, "Scripts");
if (!Directory.Exists(folder)) return;
foreach (var file in Directory.GetFiles(folder, "*.sql", SearchOption.AllDirectories))
{
var node = new TreeViewNode
{
Content = new ScriptTreeItem
{
DisplayName = Path.GetFileName(file),
FilePath = file
}
};
ScriptsTree.RootNodes.Add(node);
}
ScriptsTree.ItemInvoked += ScriptsTree_ItemInvoked;
}
private void ScriptsTree_ItemInvoked(TreeView sender, TreeViewItemInvokedEventArgs args)
{
if (args.InvokedItem is TreeViewNode node && node.Content is ScriptTreeItem item)
{
var file = item.FilePath;
var lines = File.ReadAllLines(file);
var parser = new SqlScriptParser();
currentMetadata = parser.Parse(lines); // ñîõðàíÿåì â ïîëå
RenderParameters(currentMetadata);
OutputTabs.TabItems.Clear();
foreach (var output in currentMetadata.Outputs)
{
var tab = new TabViewItem
{
Header = output.Description,
Content = new TextBlock { Text = $"Âûâîä: {output.Type}" }
};
OutputTabs.TabItems.Add(tab);
}
}
}
private void RenderParameters(ScriptMetadata metadata)
{
ParametersPanel.Items.Clear();
foreach (var param in metadata.Parameters)
{
FrameworkElement control = null;
switch (param.Type)
{
case ParameterType.Int:
control = new TextBox { Header = param.Description, Text = param.DefaultValue ?? string.Empty };
break;
case ParameterType.String:
control = new TextBox { Header = param.Description, Text = param.DefaultValue ?? string.Empty };
break;
case ParameterType.DateTime:
control = new DatePicker
{
Header = param.Description,
SelectedDate = DateTime.TryParse(param.DefaultValue, out var dt) ? dt : DateTime.Now
};
break;
case ParameterType.Bool:
control = new CheckBox
{
Content = param.Description,
IsChecked = param.DefaultValue?.ToLower() == "true"
};
break;
case ParameterType.Table:
var combo = new ComboBox { Header = param.Description };
combo.Items.Add("Ìàãàçèí 1");
combo.Items.Add("Ìàãàçèí 2");
combo.Items.Add("Ìàãàçèí 3");
control = combo;
break;
}
param.Control = control;
ParametersPanel.Items.Add(control);
}
}
private Dictionary<string, object> CollectParameterValues()
{
var values = new Dictionary<string, object>();
if (currentMetadata == null) return values;
foreach (var param in currentMetadata.Parameters)
{
switch (param.Type)
{
case ParameterType.Int:
if (param.Control is TextBox tbInt && int.TryParse(tbInt.Text, out var intVal))
values[param.Name] = intVal;
break;
case ParameterType.String:
if (param.Control is TextBox tbStr)
values[param.Name] = tbStr.Text;
break;
case ParameterType.DateTime:
if (param.Control is DatePicker dp && dp.SelectedDate.HasValue)
values[param.Name] = dp.SelectedDate.Value;
break;
case ParameterType.Bool:
if (param.Control is CheckBox cb)
values[param.Name] = cb.IsChecked ?? false;
break;
case ParameterType.Table:
if (param.Control is ComboBox combo && combo.SelectedItem != null)
values[param.Name] = combo.SelectedItem.ToString();
break;
}
}
return values;
}
private void ExecuteButton_Click(object sender, RoutedEventArgs e)
{
var values = CollectParameterValues();
// Çäåñü ìîæíî ñôîðìèðîâàòü SQL ñ ïîäñòàíîâêîé ïàðàìåòðîâ
foreach (var kv in values)
{
Console.WriteLine($"{kv.Key} = {kv.Value}");
}
}
}
}

View 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;
}
}

View 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;
}
}

View 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);
}

View 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);
}

View 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);
}

View 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>

View 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;
}
}

View 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;
}

View 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;
}

View 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;
}

View 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>

20
SQLVision.slnx Normal file
View File

@@ -0,0 +1,20 @@
<Solution>
<Configurations>
<Platform Name="ARM64" />
<Platform Name="x64" />
<Platform Name="x86" />
</Configurations>
<Folder Name="/Элементы решения/" />
<Project Path="SQLVision.Core/SQLVision.Core.csproj" />
<Project Path="SQLVision.Services/SQLVision.Services.csproj" />
<Project Path="SQLVision.UI/SQLVision.UI.csproj">
<Platform Solution="Debug|x64" Project="x64" />
</Project>
<Project Path="SQLVision.Visualizers/SQLVision.Visualizers.csproj" />
<Project Path="SQLVision/SQLVision.csproj" Id="ad5352de-2482-4124-8033-4e873ae1d181">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
<Platform Solution="*|x86" Project="x86" />
<Deploy />
</Project>
</Solution>

29
SQLVision/App.xaml Normal file
View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<Application
x:Class="SQLVision.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converts="using:SQLVision.Converters">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
</ResourceDictionary.MergedDictionaries>
<!-- Fluent Icons -->
<FontFamily x:Key="FluentIconsFont">Segoe Fluent Icons</FontFamily>
<!-- Converters -->
<converts:ParameterTypeToVisibilityConverter x:Key="ParameterTypeToVisibilityConverter"/>
<converts:OutputTypeToVisibilityConverter x:Key="OutputTypeToVisibilityConverter"/>
<converts:ObjectToDateTimeOffsetConverter x:Key="ObjectToDateTimeOffsetConverter"/>
<converts:ObjectToNullableBoolConverter x:Key="ObjectToNullableBoolConverter"/>
<converts:DataTableToEnumerableConverter x:Key="DataTableToEnumerableConverter"/>
<converts:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
</ResourceDictionary>
</Application.Resources>
</Application>

35
SQLVision/App.xaml.cs Normal file
View File

@@ -0,0 +1,35 @@
using Microsoft.UI.Xaml;
using SQLVision.Views;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace SQLVision
{
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
public partial class App : Application
{
private Window? _window;
/// <summary>
/// Initializes the singleton application object. This is the first line of authored code
/// executed, and as such is the logical equivalent of main() or WinMain().
/// </summary>
public App()
{
InitializeComponent();
}
/// <summary>
/// Invoked when the application is launched.
/// </summary>
/// <param name="args">Details about the launch request and process.</param>
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
_window = new MainWindow();
_window.Activate();
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,14 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
using System;
namespace SQLVision.Converters;
public sealed class BoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
=> value is bool b && b ? Visibility.Visible : Visibility.Collapsed;
public object ConvertBack(object value, Type targetType, object parameter, string language)
=> throw new NotImplementedException();
}

View File

@@ -0,0 +1,34 @@
using Microsoft.UI.Xaml.Data;
using System;
using System.Collections.Generic;
using System.Data;
namespace SQLVision.Converters;
public sealed class DataTableToEnumerableConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is DataTable dataTable)
{
// Преобразуем DataTable в список словарей или объектов
var result = new List<Dictionary<string, object>>();
foreach (DataRow row in dataTable.Rows)
{
var dict = new Dictionary<string, object>();
foreach (DataColumn col in dataTable.Columns)
{
dict[col.ColumnName] = row[col];
}
result.Add(dict);
}
return result;
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
=> throw new NotImplementedException();
}

View File

@@ -0,0 +1,23 @@
using Microsoft.UI.Xaml.Data;
using System;
namespace SQLVision.Converters;
public sealed class ObjectToDateTimeOffsetConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is DateTime dt)
return new DateTimeOffset(dt);
return DateTimeOffset.Now;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
if (value is DateTimeOffset dto)
return dto.DateTime;
return null;
}
}

View File

@@ -0,0 +1,34 @@
using Microsoft.UI.Xaml.Data;
using System;
namespace SQLVision.Converters;
public sealed class ObjectToNullableBoolConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is bool b)
return b;
if (value is string s)
{
if (s.Equals("true", StringComparison.OrdinalIgnoreCase))
{
return true;
}
else
{
return false;
}
}
return false;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
if (value is bool b)
return b;
return false;
}
}

View File

@@ -0,0 +1,24 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
using SQLVision.Enums;
using System;
namespace SQLVision.Converters;
public sealed class OutputTypeToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is not OutputType type || parameter is not string target)
return Visibility.Collapsed;
return target switch
{
"Table" => type == OutputType.Table ? Visibility.Visible : Visibility.Collapsed,
_ => Visibility.Collapsed
};
}
public object ConvertBack(object value, Type targetType, object parameter, string language) =>
throw new NotImplementedException();
}

View File

@@ -0,0 +1,27 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
using SQLVision.Enums;
using System;
namespace SQLVision.Converters;
public sealed class ParameterTypeToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is not ParameterType type || parameter is not string target)
return Visibility.Collapsed;
return target switch
{
"Text" => (type == ParameterType.Int || type == ParameterType.String) ? Visibility.Visible : Visibility.Collapsed,
"Date" => type == ParameterType.DateTime ? Visibility.Visible : Visibility.Collapsed,
"Bool" => type == ParameterType.Bool ? Visibility.Visible : Visibility.Collapsed,
"Table" => type == ParameterType.Table ? Visibility.Visible : Visibility.Collapsed,
_ => Visibility.Collapsed
};
}
public object ConvertBack(object value, Type targetType, object parameter, string language) =>
throw new NotImplementedException();
}

View File

@@ -0,0 +1,9 @@
namespace SQLVision.Enums;
public enum OutputType
{
Table,
ChartLine,
ChartBar,
ChartPie
}

View File

@@ -0,0 +1,10 @@
namespace SQLVision.Enums;
public enum ParameterType
{
Int,
String,
DateTime,
Bool,
Table
}

View File

@@ -0,0 +1,12 @@
namespace SQLVision.Models;
public class ConnectionProfile
{
public string Name { get; set; } = "";
public string Server { get; set; } = "";
public string Database { get; set; } = "";
public string AuthType { get; set; } = "Windows";
public string Login { get; set; } = "";
public string Password { get; set; } = "";
public string ConnectionString { get; set; } = "";
}

View File

@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Data;
namespace SQLVision.Models;
public sealed class ExecutionResult
{
public string ScriptName { get; init; } = string.Empty;
public string FinalSql { get; init; } = string.Empty;
public IReadOnlyList<DataTable> Tables { get; init; } = Array.Empty<DataTable>();
}

View File

@@ -0,0 +1,9 @@
using SQLVision.Enums;
namespace SQLVision.Models;
public sealed class OutputDefinition
{
public OutputType Type { get; init; }
public string Title { get; init; } = string.Empty;
}

View File

@@ -0,0 +1,13 @@
using SQLVision.Enums;
namespace SQLVision.Models;
public sealed class ParameterDefinition
{
public string Name { get; init; } = string.Empty;
public ParameterType Type { get; init; }
public string DisplayName { get; init; } = string.Empty;
public string? DefaultValue { get; init; }
public string? TableQuery { get; init; } // для @table
public string? DependsOn { get; init; } // имя другого параметра
}

View File

@@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace SQLVision.Models;
public sealed class ParameterValueSet
{
public string ScriptFilePath { get; set; } = string.Empty;
public Dictionary<string, object?> Values { get; set; } = new();
}

View File

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
namespace SQLVision.Models;
public sealed class ScriptMetadata
{
public string FilePath { get; init; } = string.Empty;
public string Name { get; init; } = string.Empty;
public string? Description { get; init; }
public IReadOnlyList<ParameterDefinition> Parameters { get; init; } =
Array.Empty<ParameterDefinition>();
public IReadOnlyList<OutputDefinition> Outputs { get; init; } =
Array.Empty<OutputDefinition>();
}

View File

@@ -0,0 +1,13 @@
using System.Collections.ObjectModel;
namespace SQLVision.Models;
public sealed class ScriptTreeNode
{
public string Name { get; set; } = "";
public string? FilePath { get; set; }
public ObservableCollection<ScriptTreeNode> Children { get; } = new();
public bool IsFolder => FilePath is null;
}

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap">
<Identity
Name="79ca160c-7108-46fa-9aa2-2ba97d8d499e"
Publisher="CN=frost"
Version="1.0.0.0" />
<mp:PhoneIdentity PhoneProductId="79ca160c-7108-46fa-9aa2-2ba97d8d499e" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<Properties>
<DisplayName>SQLVision</DisplayName>
<PublisherDisplayName>frost</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
</Dependencies>
<Resources>
<Resource Language="x-generate"/>
</Resources>
<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="SQLVision"
Description="SQLVision"
BackgroundColor="transparent"
Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
<uap:SplashScreen Image="Assets\SplashScreen.png" />
</uap:VisualElements>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>

View File

@@ -0,0 +1,10 @@
{
"profiles": {
"SQLVision (Package)": {
"commandName": "MsixPackage"
},
"SQLVision (Unpackaged)": {
"commandName": "Project"
}
}
}

View File

@@ -0,0 +1,62 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>SQLVision</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Platforms>x86;x64;ARM64</Platforms>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<UseWinUI>true</UseWinUI>
<Nullable>enable</Nullable>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<WindowsPackageType>None</WindowsPackageType>
</PropertyGroup>
<ItemGroup>
<None Remove="Views\ConnectionPage.xaml" />
<None Remove="Views\HelpWindow.xaml" />
</ItemGroup>
<ItemGroup>
<Content Include="Assets\SplashScreen.scale-200.png" />
<Content Include="Assets\LockScreenLogo.scale-200.png" />
<Content Include="Assets\Square150x150Logo.scale-200.png" />
<Content Include="Assets\Square44x44Logo.scale-200.png" />
<Content Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" />
<Content Include="Assets\StoreLogo.png" />
<Content Include="Assets\Wide310x150Logo.scale-200.png" />
</ItemGroup>
<ItemGroup>
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7175" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.3" />
<PackageReference Include="ClosedXML" Version="0.105.0" />
<PackageReference Include="LiveChartsCore.SkiaSharpView.WinUI" Version="2.0.0-rc2" />
<PackageReference Include="WinUIEx" Version="2.9.0" />
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
</ItemGroup>
<ItemGroup>
<None Update="Scripts\ServerInfo.sql">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<Page Update="Views\ConnectionPage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Views\HelpWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,63 @@
-- @description "Информация о сервере (с параметрами)"
-- @param IncludeMemory bool "Показывать информацию о памяти" default="true"
-- @param IncludeVersion bool "Показывать информацию о версии SQL Server" default="true"
-- @param ServerTag string "Метка сервера (произвольный текст)" default="Local"
-- @output table "Основная информация о сервере"
-- @output table "Память и конфигурация"
-- @output table "Версия SQL Server"
/*
declare @ServerTag as nvarchar(150)
declare @IncludeMemory as bit
declare @IncludeVersion as bit
*/
-------------------------------
-- Основная информация о сервере
-------------------------------
SELECT
SERVERPROPERTY('MachineName') AS MachineName,
SERVERPROPERTY('ServerName') AS ServerName,
SERVERPROPERTY('InstanceName') AS InstanceName,
SERVERPROPERTY('Edition') AS Edition,
SERVERPROPERTY('ProductLevel') AS ProductLevel,
SERVERPROPERTY('ProductVersion') AS ProductVersion,
SERVERPROPERTY('IsClustered') AS IsClustered,
SERVERPROPERTY('IsHadrEnabled') AS IsHadrEnabled,
SERVERPROPERTY('ComputerNamePhysicalNetBIOS') AS PhysicalName,
@ServerTag AS ServerTag;
-------------------------------
-- Память и конфигурация
-------------------------------
IF (@IncludeMemory = 1)
BEGIN
SELECT
total_physical_memory_kb / 1024 AS TotalMemoryMB,
available_physical_memory_kb / 1024 AS AvailableMemoryMB,
system_memory_state_desc AS MemoryState,
@ServerTag AS ServerTag
FROM sys.dm_os_sys_memory;
END
ELSE
BEGIN
SELECT
'Memory block disabled by parameter' AS Message,
@ServerTag AS ServerTag;
END
-------------------------------
-- Версия SQL Server
-------------------------------
IF (@IncludeVersion = 1)
BEGIN
SELECT
@@VERSION AS VersionString,
@ServerTag AS ServerTag;
END
ELSE
BEGIN
SELECT
'Version block disabled by parameter' AS Message,
@ServerTag AS ServerTag;
END

View File

@@ -0,0 +1,14 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace SQLVision.Selectors;
public sealed class OutputTemplateSelector : DataTemplateSelector
{
public DataTemplate? TableTemplate { get; set; }
protected override DataTemplate SelectTemplateCore(object item)
{
return TableTemplate!;
}
}

View File

@@ -0,0 +1,42 @@
using SQLVision.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
namespace SQLVision.Services;
public class ConnectionStorageService
{
private readonly string _filePath;
public ConnectionStorageService()
{
var folder = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var appFolder = Path.Combine(folder, "SQLVision");
if (!Directory.Exists(appFolder))
Directory.CreateDirectory(appFolder);
_filePath = Path.Combine(appFolder, "connections.json");
}
public List<ConnectionProfile> Load()
{
if (!File.Exists(_filePath))
return new List<ConnectionProfile>();
var json = File.ReadAllText(_filePath);
return JsonSerializer.Deserialize<List<ConnectionProfile>>(json) ?? new();
}
public void Save(List<ConnectionProfile> profiles)
{
var json = JsonSerializer.Serialize(profiles, new JsonSerializerOptions
{
WriteIndented = true
});
File.WriteAllText(_filePath, json);
}
}

View File

@@ -0,0 +1,27 @@
using ClosedXML.Excel;
using System.Collections.Generic;
using System.Data;
namespace SQLVision.Services;
public sealed class ExcelExportService
{
public void ExportTablesToExcel(IReadOnlyList<DataTable> tables, string filePath)
{
using var workbook = new XLWorkbook();
for (int i = 0; i < tables.Count; i++)
{
var table = tables[i];
var sheetName = string.IsNullOrWhiteSpace(table.TableName)
? $"Sheet{i + 1}"
: table.TableName;
var ws = workbook.Worksheets.Add(sheetName);
ws.Cell(1, 1).InsertTable(table);
ws.Columns().AdjustToContents();
}
workbook.SaveAs(filePath);
}
}

View File

@@ -0,0 +1,42 @@
using SQLVision.Enums;
using System;
using System.Collections.Generic;
using System.Globalization;
namespace SQLVision.Services;
public sealed class ParameterResolver
{
public string RenderSqlForDisplay(
string originalSql,
IReadOnlyDictionary<string, object?> parameterValues,
IReadOnlyList<Models.ParameterDefinition> definitions)
{
var sql = originalSql;
foreach (var def in definitions)
{
parameterValues.TryGetValue(def.Name, out var value);
var formatted = FormatValueForSql(def.Type, value);
sql = sql.Replace("@" + def.Name, formatted);
}
return sql;
}
private static string FormatValueForSql(ParameterType type, object? value)
{
if (value is null || value is DBNull)
return "NULL";
return type switch
{
ParameterType.Int => Convert.ToInt32(value, CultureInfo.InvariantCulture).ToString(CultureInfo.InvariantCulture),
ParameterType.Bool => ((bool)value) ? "1" : "0",
ParameterType.DateTime => $"'{((DateTime)value).ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)}'",
ParameterType.String or ParameterType.Table => $"'{value.ToString()?.Replace("'", "''")}'",
_ => $"'{value.ToString()?.Replace("'", "''")}'"
};
}
}

View File

@@ -0,0 +1,106 @@
using SQLVision.Enums;
using SQLVision.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
namespace SQLVision.Services;
public sealed class SqlCommentParser
{
private static readonly Regex DescriptionRegex =
new(@"--\s*@description\s+""(.+)""", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex ParamRegex =
new(@"--\s*@param\s+(\w+)\s+(\w+)\s+""(.+?)""(?:\s+default=""(.*?)"")?(?:\s+@table\s+""(.+?)"")?(?:\s+dependsOn=""(.*?)"")?",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex OutputRegex =
new(@"--\s*@output\s+([\w:]+)\s+""(.+?)""",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
public ScriptMetadata Parse(string filePath)
{
var text = File.ReadAllText(filePath);
var lines = text.Split(Environment.NewLine);
string? description = null;
var parameters = new List<ParameterDefinition>();
var outputs = new List<OutputDefinition>();
foreach (var line in lines)
{
var descMatch = DescriptionRegex.Match(line);
if (descMatch.Success)
{
description = descMatch.Groups[1].Value;
continue;
}
var paramMatch = ParamRegex.Match(line);
if (paramMatch.Success)
{
var name = paramMatch.Groups[1].Value;
var typeStr = paramMatch.Groups[2].Value;
var displayName = paramMatch.Groups[3].Value;
var defaultValue = paramMatch.Groups[4].Success ? paramMatch.Groups[4].Value : null;
var tableQuery = paramMatch.Groups[5].Success ? paramMatch.Groups[5].Value : null;
var dependsOn = paramMatch.Groups[6].Success ? paramMatch.Groups[6].Value : null;
var parameterType = typeStr.ToLower() switch
{
"int" => ParameterType.Int,
"string" => ParameterType.String,
"datetime" => ParameterType.DateTime,
"bool" => ParameterType.Bool,
"table" => ParameterType.Table,
_ => ParameterType.String
};
parameters.Add(new ParameterDefinition
{
Name = name,
Type = parameterType,
DisplayName = displayName,
DefaultValue = defaultValue,
TableQuery = tableQuery,
DependsOn = dependsOn
});
continue;
}
var outputMatch = OutputRegex.Match(line);
if (outputMatch.Success)
{
var typeStr = outputMatch.Groups[1].Value;
var title = outputMatch.Groups[2].Value;
var outputType = typeStr.ToLower() switch
{
"table" => OutputType.Table,
"chart:line" => OutputType.ChartLine,
"chart:bar" => OutputType.ChartBar,
"chart:pie" => OutputType.ChartPie,
_ => OutputType.Table
};
outputs.Add(new OutputDefinition
{
Type = outputType,
Title = title
});
}
}
return new ScriptMetadata
{
FilePath = filePath,
Name = Path.GetFileNameWithoutExtension(filePath),
Description = description,
Parameters = parameters,
Outputs = outputs
};
}
}

View File

@@ -0,0 +1,63 @@
using Microsoft.Data.SqlClient;
using SQLVision.Models;
using System;
using System.Collections.Generic;
using System.Data;
using System.Threading;
using System.Threading.Tasks;
namespace SQLVision.Services;
public sealed class SqlExecutor
{
private string _connectionString;
public SqlExecutor(string connectionString)
{
_connectionString = connectionString;
}
public void SetConnect(string connectionString)
{
_connectionString = connectionString;
}
public async Task<ExecutionResult> ExecuteAsync(
ScriptMetadata metadata,
string sqlText,
IReadOnlyDictionary<ParameterDefinition, object?> parameterValues,
CancellationToken cancellationToken = default)
{
// Здесь sqlText уже с параметрами вида @ParamName
var dataTables = new List<DataTable>();
await using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync(cancellationToken);
await using var command = new SqlCommand(sqlText, connection)
{
CommandType = CommandType.Text
};
foreach (var kv in parameterValues)
{
var paramName = "@" + kv.Key.Name;
command.Parameters.AddWithValue(paramName, kv.Value ?? DBNull.Value);
}
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
do
{
var table = new DataTable();
table.Load(reader);
dataTables.Add(table);
} while (!reader.IsClosed);
return new ExecutionResult
{
ScriptName = metadata.Name,
FinalSql = sqlText,
Tables = dataTables
};
}
}

View File

@@ -0,0 +1,30 @@
using Microsoft.Data.SqlClient;
using System.Data;
using System.Threading;
using System.Threading.Tasks;
namespace SQLVision.Services;
public sealed class TableDataProvider
{
private readonly string _connectionString;
public TableDataProvider(string connectionString)
{
_connectionString = connectionString;
}
public async Task<DataTable> LoadTableAsync(string query, CancellationToken cancellationToken = default)
{
var table = new DataTable();
await using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync(cancellationToken);
await using var command = new SqlCommand(query, connection);
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
table.Load(reader);
return table;
}
}

View File

@@ -0,0 +1,23 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace SQLVision.ViewModels;
public abstract class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected void RaisePropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? name = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
return false;
field = value;
RaisePropertyChanged(name);
return true;
}
}

View File

@@ -0,0 +1,109 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Data.SqlClient;
using SQLVision.Models;
using SQLVision.Services;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
namespace SQLVision.ViewModels;
public partial class ConnectionViewModel : ObservableObject
{
[ObservableProperty] private string serverName = ".";
[ObservableProperty] private string authType = "Windows";
[ObservableProperty] private string login = "";
[ObservableProperty] private string password = "";
[ObservableProperty] private ObservableCollection<string> databases = new();
[ObservableProperty] private string selectedDatabase = "";
[ObservableProperty] private string statusMessage = "";
public bool IsSqlAuth => AuthType == "SQL Server";
public event Action<ConnectionProfile>? ConnectionSaved;
private readonly ConnectionStorageService _storage;
private List<ConnectionProfile> _profiles;
public ConnectionViewModel()
{
_storage = new ConnectionStorageService();
_profiles = _storage.Load();
}
[RelayCommand]
private async Task TestConnection()
{
try
{
var connStr = BuildConnectionString("master");
using var conn = new SqlConnection(connStr);
await conn.OpenAsync();
StatusMessage = "Подключение успешно. Загружаю базы...";
// Загружаем список баз
var cmd = new SqlCommand("SELECT name FROM sys.databases ORDER BY name", conn);
using var reader = await cmd.ExecuteReaderAsync();
Databases.Clear();
while (await reader.ReadAsync())
Databases.Add(reader.GetString(0));
StatusMessage = "Базы данных загружены.";
}
catch (Exception ex)
{
StatusMessage = "Ошибка: " + ex.Message;
}
}
[RelayCommand]
private void Save()
{
var profile = new ConnectionProfile
{
Name = $"{ServerName} ({SelectedDatabase})",
Server = ServerName,
Database = SelectedDatabase,
AuthType = AuthType,
Login = Login,
Password = Password,
ConnectionString = BuildConnectionString(SelectedDatabase)
};
_profiles.RemoveAll(p => p.Name == profile.Name);
_profiles.Add(profile);
_storage.Save(_profiles);
StatusMessage = "Подключение сохранено.";
ConnectionSaved?.Invoke(profile);
}
private string BuildConnectionString(string database)
{
var builder = new SqlConnectionStringBuilder
{
DataSource = ServerName,
InitialCatalog = database,
TrustServerCertificate = true
};
if (IsSqlAuth)
{
builder.UserID = Login;
builder.Password = Password;
builder.IntegratedSecurity = false;
}
else
{
builder.IntegratedSecurity = true;
}
return builder.ConnectionString;
}
}

View File

@@ -0,0 +1,339 @@
using Microsoft.UI.Xaml;
using SQLVision.Models;
using SQLVision.Services;
using SQLVision.Views;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using System.Windows.Input;
namespace SQLVision.ViewModels;
public sealed class MainViewModel : BaseViewModel
{
private readonly SqlCommentParser _parser;
private readonly SqlExecutor _executor;
private readonly ParameterResolver _parameterResolver;
private readonly TableDataProvider _tableDataProvider;
private readonly ExcelExportService _excelExportService;
// -----------------------------
// Подключения
// -----------------------------
private readonly ConnectionStorageService _storage;
public ObservableCollection<ConnectionProfile> SavedConnections { get; } = new();
private ConnectionProfile? _selectedConnection;
public ConnectionProfile? SelectedConnection
{
get => _selectedConnection;
set
{
if (SetProperty(ref _selectedConnection, value))
{
if (value != null)
ConnectionString = value.ConnectionString;
}
}
}
private string _connectionString = "";
public string ConnectionString
{
get => _connectionString;
set => SetProperty(ref _connectionString, value);
}
// -----------------------------
// Скрипты, дерево и параметры
// -----------------------------
public ObservableCollection<ScriptMetadata> Scripts { get; } = new();
// Дерево скриптов для TreeView
public ObservableCollection<ScriptTreeNode> ScriptTree { get; } = new();
private ScriptTreeNode? _selectedScriptNode;
public ScriptTreeNode? SelectedScriptNode
{
get => _selectedScriptNode;
set
{
if (SetProperty(ref _selectedScriptNode, value))
{
if (value?.FilePath is not null)
{
// Находим ScriptMetadata по пути файла
var meta = Scripts.FirstOrDefault(s =>
string.Equals(s.FilePath, value.FilePath, StringComparison.OrdinalIgnoreCase));
if (meta is not null)
SelectedScript = meta;
}
}
}
}
public ObservableCollection<ParameterViewModel> Parameters { get; } = new();
public ObservableCollection<OutputViewModel> Outputs { get; } = new();
private ScriptMetadata? _selectedScript;
public ScriptMetadata? SelectedScript
{
get => _selectedScript;
set
{
if (SetProperty(ref _selectedScript, value))
{
(ExecuteCommand as RelayCommand)?.RaiseCanExecuteChanged();
_ = LoadParametersForSelectedScriptAsync();
}
}
}
private ExecutionResult? _lastResult;
public ExecutionResult? LastResult
{
get => _lastResult;
set
{
if (SetProperty(ref _lastResult, value))
{
(ExportToExcelCommand as RelayCommand)?.RaiseCanExecuteChanged();
_ = LoadParametersForSelectedScriptAsync();
}
}
}
// -----------------------------
// Команды
// -----------------------------
public ICommand ExecuteCommand { get; }
public ICommand ExportToExcelCommand { get; }
public ICommand ShowHelpCommand { get; }
public ICommand OpenConnectionCommand { get; }
// -----------------------------
// Конструктор
// -----------------------------
public MainViewModel(
SqlCommentParser parser,
SqlExecutor executor,
ParameterResolver parameterResolver,
TableDataProvider tableDataProvider,
ExcelExportService excelExportService)
{
_parser = parser;
_executor = executor;
_parameterResolver = parameterResolver;
_tableDataProvider = tableDataProvider;
_excelExportService = excelExportService;
ExecuteCommand = new RelayCommand(async _ => await ExecuteAsync(), _ => SelectedScript is not null);
ExportToExcelCommand = new RelayCommand(_ => ExportToExcel(), _ => LastResult is not null);
ShowHelpCommand = new RelayCommand(_ => ShowHelp());
OpenConnectionCommand = new RelayCommand(_ => OpenConnection());
_storage = new ConnectionStorageService();
foreach (var p in _storage.Load())
SavedConnections.Add(p);
SelectedConnection = SavedConnections.FirstOrDefault();
}
// -----------------------------
// Окно подключения
// -----------------------------
private void OpenConnection()
{
var vm = new ConnectionViewModel();
var page = new ConnectionPage { DataContext = vm };
vm.ConnectionSaved += profile =>
{
var existing = SavedConnections.FirstOrDefault(p => p.Name == profile.Name);
if (existing is not null)
SavedConnections.Remove(existing);
SavedConnections.Add(profile);
_storage.Save(SavedConnections.ToList());
SelectedConnection = profile;
};
var window = new Window
{
Content = page
};
window.Activate();
}
// -----------------------------
// Help
// -----------------------------
private void ShowHelp()
{
var window = new HelpWindow();
window.Activate();
}
// -----------------------------
// Загрузка скриптов и построение дерева
// -----------------------------
public void LoadScripts(string scriptsFolder)
{
Scripts.Clear();
ScriptTree.Clear();
if (!Directory.Exists(scriptsFolder))
return;
// Строим дерево по корневой папке
var rootNode = BuildScriptTree(scriptsFolder);
ScriptTree.Add(rootNode);
}
private ScriptTreeNode BuildScriptTree(string folderPath)
{
var folderNode = new ScriptTreeNode
{
Name = Path.GetFileName(folderPath),
FilePath = null
};
// Подпапки
foreach (var dir in Directory.GetDirectories(folderPath))
{
var subNode = BuildScriptTree(dir);
folderNode.Children.Add(subNode);
}
// Файлы .sql
foreach (var file in Directory.GetFiles(folderPath, "*.sql"))
{
var meta = _parser.Parse(file);
Scripts.Add(meta);
var fileNode = new ScriptTreeNode
{
Name = Path.GetFileName(file),
FilePath = file
};
folderNode.Children.Add(fileNode);
}
return folderNode;
}
// -----------------------------
// Параметры
// -----------------------------
private async Task LoadParametersForSelectedScriptAsync()
{
Parameters.Clear();
Outputs.Clear();
if (SelectedScript is null)
return;
foreach (var p in SelectedScript.Parameters)
{
var vm = new ParameterViewModel(p);
Parameters.Add(vm);
if (p.TableQuery is not null)
{
var table = await _tableDataProvider.LoadTableAsync(p.TableQuery);
vm.LookupTable = table;
}
}
foreach (var o in SelectedScript.Outputs)
Outputs.Add(new OutputViewModel(o));
}
// -----------------------------
// Выполнение SQL
// -----------------------------
private async Task ExecuteAsync()
{
if (SelectedScript is null)
return;
if (string.IsNullOrWhiteSpace(ConnectionString))
{
// TODO: вывести ContentDialog "Подключение не выбрано"
return;
}
var sqlText = await File.ReadAllTextAsync(SelectedScript.FilePath);
var paramValues = Parameters.ToDictionary(
p => p.Definition,
p => p.Value);
// Если SqlExecutor умеет менять строку подключения динамически — здесь можно прокинуть ConnectionString
_executor.SetConnect(ConnectionString);
var result = await _executor.ExecuteAsync(SelectedScript, sqlText, paramValues);
for (int i = 0; i < Outputs.Count; i++)
{
Outputs[i].Table = result.Tables[i];
result.Tables[i].TableName = Outputs[i].Definition.Title;
}
LastResult = result;
RaisePropertyChanged(nameof(Outputs));
SaveParameterValues(SelectedScript.FilePath, paramValues);
}
// -----------------------------
// Экспорт
// -----------------------------
private void ExportToExcel()
{
if (LastResult is null || LastResult.Tables.Count == 0)
return;
var directory = Path.Combine(AppContext.BaseDirectory, "Export");
Directory.CreateDirectory(directory);
var fileName = $"{LastResult.ScriptName}_{DateTime.Now:yyyyMMdd_HHmmss}.xlsx";
var fullPath = Path.Combine(directory, fileName);
_excelExportService.ExportTablesToExcel(LastResult.Tables, fullPath);
}
// -----------------------------
// Сохранение параметров
// -----------------------------
private void SaveParameterValues(string scriptPath, Dictionary<ParameterDefinition, object?> values)
{
var directory = Path.Combine(AppContext.BaseDirectory, "Config", "Parameters");
Directory.CreateDirectory(directory);
var fileName = Path.GetFileNameWithoutExtension(scriptPath) + ".json";
var fullPath = Path.Combine(directory, fileName);
var payload = new ParameterValueSet
{
ScriptFilePath = scriptPath,
Values = values.ToDictionary(t => t.Key.Name, t => t.Value),
};
var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions
{
WriteIndented = true
});
File.WriteAllText(fullPath, json);
}
}

View File

@@ -0,0 +1,16 @@
using SQLVision.Models;
using System.Data;
namespace SQLVision.ViewModels;
public sealed class OutputViewModel : BaseViewModel
{
public OutputDefinition Definition { get; }
public DataTable? Table { get; set; } // для OutputType.Table
// для графиков можно добавить коллекции серий и т.д.
public OutputViewModel(OutputDefinition definition)
{
Definition = definition;
}
}

View File

@@ -0,0 +1,46 @@
using SQLVision.Enums;
using SQLVision.Models;
using System.Data;
namespace SQLVision.ViewModels;
public sealed class ParameterViewModel : BaseViewModel
{
public ParameterDefinition Definition { get; }
private object? _value;
public object? Value
{
get => _value;
set
{
if (!Equals(_value, value))
{
_value = value;
RaisePropertyChanged();
}
}
}
private DataTable? _lookupTable;
public DataTable? LookupTable
{
get => _lookupTable;
set
{
if (!Equals(_lookupTable, value))
{
_lookupTable = value;
RaisePropertyChanged();
}
}
}
public bool IsLookup => Definition.Type == ParameterType.Table;
public ParameterViewModel(ParameterDefinition definition)
{
Definition = definition;
Value = definition.DefaultValue;
}
}

View File

@@ -0,0 +1,24 @@
using System;
using System.Windows.Input;
namespace SQLVision.ViewModels;
public sealed class RelayCommand : ICommand
{
private readonly Action<object?> _execute;
private readonly Predicate<object?>? _canExecute;
public RelayCommand(Action<object?> execute, Predicate<object?>? canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object? parameter) => _canExecute?.Invoke(parameter) ?? true;
public void Execute(object? parameter) => _execute(parameter);
public event EventHandler? CanExecuteChanged;
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}

View File

@@ -0,0 +1,80 @@
<Page
x:Class="SQLVision.Views.ConnectionPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Background="{ThemeResource LayerFillColorDefaultBrush}"
Padding="24">
<StackPanel Spacing="20" Width="420">
<!-- HEADER -->
<StackPanel Orientation="Horizontal" Spacing="12">
<FontIcon FontFamily="{StaticResource FluentIconsFont}"
Glyph="&#xE8B8;"
FontSize="28"/>
<TextBlock Text="Подключение к SQL Server"
Style="{StaticResource VS.HeaderText}"
FontSize="26"
Margin="0"/>
</StackPanel>
<!-- Сервер -->
<StackPanel Spacing="4">
<TextBlock Text="Сервер" FontWeight="SemiBold"/>
<TextBox Text="{Binding ServerName, Mode=TwoWay}"
PlaceholderText="SERVER\\INSTANCE или hostname"/>
</StackPanel>
<!-- Тип аутентификации -->
<StackPanel Spacing="4">
<TextBlock Text="Аутентификация" FontWeight="SemiBold"/>
<ComboBox SelectedItem="{Binding AuthType, Mode=TwoWay}">
<ComboBoxItem Content="Windows"/>
<ComboBoxItem Content="SQL Server"/>
</ComboBox>
</StackPanel>
<!-- SQL Login -->
<StackPanel Spacing="4"
Visibility="{Binding IsSqlAuth, Converter={StaticResource BoolToVisibilityConverter}}">
<TextBlock Text="Логин" FontWeight="SemiBold"/>
<TextBox Text="{Binding Login, Mode=TwoWay}"
PlaceholderText="sa или другой пользователь"/>
<TextBlock Text="Пароль" FontWeight="SemiBold"/>
<PasswordBox Password="{Binding Password, Mode=TwoWay}"/>
</StackPanel>
<!-- База данных -->
<StackPanel Spacing="4">
<TextBlock Text="База данных" FontWeight="SemiBold"/>
<ComboBox ItemsSource="{Binding Databases}"
SelectedItem="{Binding SelectedDatabase, Mode=TwoWay}"
PlaceholderText="Выберите базу"/>
</StackPanel>
<!-- Кнопки -->
<StackPanel Orientation="Horizontal" Spacing="12" Margin="0,12,0,0">
<Button Content="Проверить"
Command="{Binding TestConnectionCommand}"
Style="{StaticResource AccentButtonStyle}"/>
<Button Content="Сохранить"
Command="{Binding SaveCommand}"/>
</StackPanel>
<!-- Статус -->
<TextBlock Text="{Binding StatusMessage}"
Foreground="DarkGreen"
FontWeight="SemiBold"
TextWrapping="Wrap"
Margin="0,8,0,0"/>
</StackPanel>
</Grid>
</Page>

View File

@@ -0,0 +1,31 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace SQLVision.Views
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class ConnectionPage : Page
{
public ConnectionPage()
{
InitializeComponent();
}
}
}

Some files were not shown because too many files have changed in this diff Show More