7 Commits

Author SHA1 Message Date
e0b20a07d2 Сделал лабу 3 2025-10-21 13:17:55 +04:00
c0f6d17928 Сделал 2 лабу 2025-09-23 14:07:38 +04:00
34d0d1aea6 Сделал лабу 2025-09-10 14:16:25 +04:00
1fefae50df Доделал 2025-09-07 12:13:24 +04:00
de3cb0a461 Сделал TemplatedListBox 2025-09-07 11:28:21 +04:00
3b5d539290 Сделал второй компонент 2025-09-07 11:08:58 +04:00
a372eeee57 Сделал первый компонент 2025-09-07 10:56:17 +04:00
69 changed files with 4084 additions and 49 deletions

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ComponentOrientedPlatform.Abstractions;
public interface IAppLogger
{
void Info(string message);
void Warn(string message);
void Error(string message, Exception? ex = null);
}

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace ComponentOrientedPlatform.Abstractions;
public interface IComponentContract
{
IComponentMetadata Metadata { get; }
UserControl CreateControl(IHostServices host);
}

View File

@@ -0,0 +1,16 @@
using ComponentOrientedPlatform.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ComponentOrientedPlatform.Abstractions;
public interface IComponentMetadata
{
string Id { get; }
string Title { get; }
ComponentMenuGroup MenuGroup { get; }
AccessLevel AccessRequired { get; }
}

View File

@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ComponentOrientedPlatform.Abstractions;
public interface IHostServices
{
ILicenseProvider License { get; }
IAppLogger Logger { get; }
T? GetService<T>() where T : class;
}

View File

@@ -0,0 +1,15 @@
using ComponentOrientedPlatform.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ComponentOrientedPlatform.Abstractions;
public interface ILicenseProvider
{
AccessLevel CurrentLevel { get; }
bool IsExpired { get; }
string? Owner { get; }
}

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ComponentOrientedPlatform.Abstractions;
public interface IReportDocumentContract
{
public string DocumentFormat { get; }
}

View File

@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ComponentOrientedPlatform.Abstractions;
public interface IReportDocumentWithChartHistogramContract :
IReportDocumentContract
{
/// <summary>
/// Создание документа в асинхронном режиме
/// </summary>
/// <param name="filePath">Путь до файла</param>
/// <param name="header">Заголовок документа</param>
/// <param name="chartTitle">Заголовок диаграммы</param>
/// <param name="series">Список данных для гистограммы</param>
/// <exception cref="ArgumentNullException">Не указан путь до файла</exception>
/// <exception cref="ArgumentNullException">Не задан заголовок документа</exception>
/// <exception cref="ArgumentNullException">Не задан заголовок диаграммы</exception>
/// <exception cref="ArgumentNullException">Список серий не задан</exception>
/// <exception cref="ArgumentOutOfRangeException">Список серий пустой</exception>
/// <returns>Задача по созданию документа</returns>
Task CreateDocumentAsync(
string filePath,
string header, string chartTitle,
List<(int Parameter, double Value)> series
);
}

View File

@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ComponentOrientedPlatform.Abstractions;
public interface IReportDocumentWithChartLineContract : IReportDocumentContract
{
/// <summary>
/// Создание документа в асинхронном режиме
/// </summary>
/// <param name="filePath">Путь до файла</param>
/// <param name="header">Заголовок документа</param>
/// <param name="chartTitle">Заголовок диаграммы</param>
/// <param name="series">Список серий с данными для линейнойдиаграммы</param>
/// <exception cref="ArgumentNullException">Не указан путь дофайла</exception>
/// <exception cref="ArgumentNullException">Не задан заголовок документа</exception>
/// <exception cref="ArgumentNullException">Не задан заголовок диаграммы</exception>
/// <exception cref="ArgumentNullException">Словарь серий незадан</exception>
/// <exception cref="ArgumentOutOfRangeException">Словарь серий пустой</exception>
/// <exception cref="ArgumentNullException">Список серии незадан</exception>
/// <exception cref="ArgumentOutOfRangeException">Список серии пустой</exception>
/// <exception cref="ArgumentException">В разных сериях различаются параметры</exception>
/// <returns>Задача по созданию документа</returns>
Task CreateDocumentAsync(
string filePath,
string header,
string chartTitle,
Dictionary<string, List<(int Parameter, double Value)>> series
);
}

View File

@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ComponentOrientedPlatform.Abstractions;
public interface IReportDocumentWithChartPieContract : IReportDocumentContract
{
/// <summary>
/// Создание документа в асинхронном режиме
/// </summary>
/// <param name="filePath">Путь до файла</param>
/// <param name="header">Заголовок документа</param>
/// <param name="chartTitle">Заголовок диаграммы</param>
/// <param name="series">Список данных для круговой диаграммы</param>
/// <exception cref="ArgumentNullException">Не указан путь до файла</exception>
/// <exception cref="ArgumentNullException">Не задан заголовок документа</exception>
/// <exception cref="ArgumentNullException">Не задан заголовок диаграммы</exception>
/// <exception cref="ArgumentNullException">Список серий не задан</exception>
/// <exception cref="ArgumentOutOfRangeException">Список серий пустой</exception>
/// <returns>Задача по созданию документа</returns>
Task CreateDocumentAsync(
string filePath,
string header,
string chartTitle,
List<(int Parameter, double Value)> series
);
}

View File

@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ComponentOrientedPlatform.Abstractions;
public interface IReportDocumentWithContextImagesContract :
IReportDocumentContract
{
/// <summary>
/// Создание документа в асинхронном режиме
/// </summary>
/// <param name="filePath">Путь до файла</param>
/// <param name="header">Заголовок документа</param>
/// <param name="images">Список изображений</param>
/// <exception cref="ArgumentNullException">Не указан путь дофайла</exception>
/// <exception cref="ArgumentNullException">Не задан заголовокдокумента</exception>
/// <exception cref="ArgumentNullException">Список изображений незадан</exception>
/// <exception cref="ArgumentOutOfRangeException">Передан пустой списокизображений</exception>
/// <exception cref="ArgumentNullException">В списке изображений имеетсяизображение с не заданным набором байт</exception>
/// <exception cref="ArgumentOutOfRangeException">В списке изображенийимеется изображение с пустым набором байт</exception>
/// <returns>Задача по созданию документа</returns>
Task CreateDocumentAsync(
string filePath,
string header,
List<byte[]> images
);
}

View File

@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ComponentOrientedPlatform.Abstractions;
public interface IReportDocumentWithContextTablesContract :
IReportDocumentContract
{
/// <summary>
/// Создание документа в асинхронном режиме
/// </summary>
/// <param name="filePath">Путь до файла</param>
/// <param name="header">Заголовок документа</param>
/// <param name="tables">Список данных для таблиц</param>
/// <exception cref="ArgumentNullException">Не указан путь дофайла</exception>
/// <exception cref="ArgumentNullException">Не задан заголовокдокумента</exception>
/// <exception cref="ArgumentNullException">Список данных для таблиц незадан</exception>
/// <exception cref="ArgumentOutOfRangeException">Передан пустой списокданных для таблиц</exception>
/// <exception cref="ArgumentNullException">В списке данных для таблицимеется запись с не заданным массивом строк</exception>
/// <exception cref="ArgumentOutOfRangeException">В списке данных длятаблиц имеется запись с пустым массивом строк</exception>
/// <returns>Задача по созданию документа</returns>
Task CreateDocumentAsync(
string filePath,
string header,
List<string[,]> tables
);
}

View File

@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ComponentOrientedPlatform.Abstractions;
public interface IReportDocumentWithContextTextsContract :
IReportDocumentContract
{
/// <summary>
/// Создание документа в асинхронном режиме
/// </summary>
/// <param name="filePath">Путь до файла</param>
/// <param name="header">Заголовок документа</param>
/// <param name="paragraphs">Список абзацев текста</param>
/// <exception cref="ArgumentNullException">Не указан путь дофайла</exception>
/// <exception cref="ArgumentNullException">Не задан заголовокдокумента</exception>
/// <exception cref="ArgumentNullException">Список абзацев текста незадан</exception>
/// <exception cref="ArgumentOutOfRangeException">Передан пустой списокабзацев текста</exception>
/// <exception cref="ArgumentNullException">В списке абзацев текста имеетсяабзац с не заданной строкой</exception>
/// <returns>Задача по созданию документа</returns>
Task CreateDocumentAsync(
string filePath,
string header,
List<string> paragraphs
);
}

View File

@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ComponentOrientedPlatform.Abstractions;
public interface IReportDocumentWithTableColumnHeaderContract :
IReportDocumentContract
{
/// <summary>
/// Создание документа в асинхронном режиме
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="filePath">Путь до файла</param>
/// <param name="header">Заголовок документа</param>
/// <param name="rowsHeights">Высота строк</param>
/// <param name="rowUnions">Список строк заголовка для объединения (индекс первой строки, с которой начинается объединенние и количество объединяемых строк)</param>
/// <param name="headers">Список заголовков (указывается индекс ячейки, в которую нужно вставить заголовок, а также название свойства или поля, из которого следует извлекать значение из элемента данных для заполнения ячейки столбца)</param>
/// <param name="data">Данные для заполнения таблицы</param>
/// <exception cref="ArgumentNullException">Не указан путь до файла</exception>
/// <exception cref="ArgumentNullException">Не задан заголовок документа</exception>
/// <exception cref="ArgumentNullException">Список rowsHeights не задан</exception>
/// <exception cref="ArgumentOutOfRangeException">Список rowsHeights пустой</exception>
/// <exception cref="ArgumentNullException">Список rowUnions не задан</exception>
/// <exception cref="ArgumentOutOfRangeException">Список rowUnions пустой</exception>
/// <exception cref="ArgumentNullException">Список заголовков не задан</exception>
/// <exception cref="ArgumentOutOfRangeException">Список заголовков пустой</exception>
/// <exception cref="ArgumentException">Список rowsHeights не совпадает по количеству заголовков второго столбца из списка заголовков</exception>
/// <exception cref="ArgumentException">Имеется несоответсвие списка объединения строк и списка заголовков</exception>
/// <exception cref="ArgumentNullException">Список данных для заполнения не задан</exception>
/// <exception cref="ArgumentOutOfRangeException">Список данных для заполнения пустой</exception>
/// <exception cref="ArgumentException">Список данных для заполнения не совпадает по количеству заголовков второго столбца из списка заголовков</exception>
/// <returns>Задача по созданию документа</returns>
Task CreateDocumentAsync<T>(
string filePath,
string header,
List<int> rowsHeights,
List<(int StartIndex, int Count)> rowUnions,
List<(int ColumnIndex, int RowIndex, string Header, string
PropertyName, string FiledName)> headers,
List<T> data
);
}

View File

@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ComponentOrientedPlatform.Abstractions;
public interface IReportDocumentWithTableColumnRowHeaderContract :
IReportDocumentContract
{
/// <summary>
/// Создание документа в асинхронном режиме
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="filePath">Путь до файла</param>
/// <param name="header">Заголовок документа</param>
/// <param name="columnsWidth">Ширина колонок</param>
/// <param name="rowsHeights">Высота строк</param>
/// <param name="isHeaderFirstRow">Признак, что заголовок заполняется по первой строке, иначе будет заполнятся по первой колонке</param>
/// <param name="headers">Список заголовков (порядок вставки заголовков прямой; также название свойства или поля, из которого следует извлекать значение из элемента данных для заполнения ячейки столбца/строки)</param>
/// <param name="data">Данные для заполнения таблицы</param>
/// <exception cref="ArgumentNullException">Не указан путь до файла</exception>
/// <exception cref="ArgumentNullException">Не задан заголовок документа</exception>
/// <exception cref="ArgumentNullException">Список rowsHeights не задан</exception>
/// <exception cref="ArgumentOutOfRangeException">Список rowsHeights пустой</exception>
/// <exception cref="ArgumentNullException">Список rowsHeights не задан</exception>
/// <exception cref="ArgumentOutOfRangeException">Список rowsHeights пустой</exception>
/// <exception cref="ArgumentNullException">Список заголовков не задан</exception>
/// <exception cref="ArgumentOutOfRangeException">Список заголовков пустой</exception>
/// <exception cref="ArgumentException">Список columnsWidth/rowsHeights не совпадает по количеству заголовков из списка заголовков</exception>
/// <exception cref="ArgumentNullException">Список данных для заполнения не задан</exception>
/// <exception cref="ArgumentOutOfRangeException">Список данных для заполнения пустой</exception>
/// <exception cref="ArgumentException">Список данных для заполнения не совпадает по количеству заголовков из списка заголовков</exception>
/// <returns>Задача по созданию документа</returns>
Task CreateDocumentAsync<T>(
string filePath,
string header,
List<int> columnsWidth,
List<int> rowsHeights,
bool isHeaderFirstRow,
List<(string Header, string PropertyName, string FiledName)> headers,
List<T> data
);
}

View File

@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ComponentOrientedPlatform.Abstractions;
public interface IReportDocumentWithTableRowHeaderContract :
IReportDocumentContract
{
/// <summary>
/// Создание документа в асинхронном режиме
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="filePath">Путь до файла</param>
/// <param name="header">Заголовок документа</param>
/// <param name="columnsWidth">Ширина колонок</param>
/// <param name="columnUnions">Список колонок заголовка для объединения (индекс первой колонки, с которой начинается объединенние и количество объединяемых колонок)</param>
/// <param name="headers">Список заголовков (указывается индекс ячейки, в которую нужно вставить заголовок, а также название свойства или поля, из которого следует извлекать значение из элемента данных для заполнения ячейки строки)</param>
/// <param name="data">Данные для заполнения таблицы</param>
/// <exception cref="ArgumentNullException">Не указан путь до файла</exception>
/// <exception cref="ArgumentNullException">Не задан заголовок документа</exception>
/// <exception cref="ArgumentNullException">Список columnsWidth не задан</exception>
/// <exception cref="ArgumentOutOfRangeException">Список columnsWidth пустой</exception>
/// <exception cref="ArgumentNullException">Список columnUnions не задан</exception>
/// <exception cref="ArgumentOutOfRangeException">Список columnUnions пустой</exception>
/// <exception cref="ArgumentNullException">Список заголовков незадан</exception>
/// <exception cref="ArgumentOutOfRangeException">Список заголовков пустой</exception>
/// <exception cref="ArgumentException">Список columnsWidth не совпадает по количеству заголовков второй строки из списка заголовков</exception>
/// <exception cref="ArgumentException">Имеется несоответсвие списка объединения колонок и списка заголовков</exception>
/// <exception cref="ArgumentNullException">Список данных для заполнения не задан</exception>
/// <exception cref="ArgumentOutOfRangeException">Список данных для заполнения пустой</exception>
/// <exception cref="ArgumentException">Список данных для заполнения не совпадает по количеству заголовков второй строки из списка заголовков</exception>
/// <returns>Задача по созданию документа</returns>
Task CreateDocumentAsync<T>(
string filePath,
string header,
List<int> columnsWidth,
List<(int StartIndex, int Count)> columnUnions,
List<(int ColumnIndex, int RowIndex, string Header, string
PropertyName, string FiledName)> headers,
List<T> data);
}

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows7.0</TargetFramework>
<Nullable>enable</Nullable>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<UseWindowsForms>true</UseWindowsForms>
<PackageId>ComponentOrientedPlatform.Contract</PackageId>
<Version>1.0.1</Version>
<Authors>Romtec</Authors>
<Product>Component Oriented Platform Contract</Product>
<Description>Common abstractions for component-oriented WinForms host and plugins.</Description>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ComponentOrientedPlatform.Model;
public enum AccessLevel
{
Minimal = 0,
Basic = 1,
Advanced = 2
}

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ComponentOrientedPlatform.Model;
public enum ComponentMenuGroup
{
Directories = 0,
Reports = 1
}

View File

@@ -0,0 +1,24 @@
using ComponentOrientedPlatform.Abstractions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ComponentOrientedPlatform.Model;
public sealed class ComponentMetadata : IComponentMetadata
{
public string Id { get; }
public string Title { get; }
public ComponentMenuGroup MenuGroup { get; }
public AccessLevel AccessRequired { get; }
public ComponentMetadata(string id, string title, ComponentMenuGroup group, AccessLevel level)
{
Id = id ?? throw new ArgumentNullException(nameof(id));
Title = title ?? throw new ArgumentNullException(nameof(title));
MenuGroup = group;
AccessRequired = level;
}
}

View File

@@ -0,0 +1,19 @@
using ComponentOrientedPlatform.Abstractions;
using ComponentOrientedPlatform.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Directories.Categories.Component;
public sealed class CategoriesDirectoryComponent : IComponentContract
{
private static readonly IComponentMetadata _meta =
new ComponentMetadata("directories.categories", "Категории",
ComponentMenuGroup.Directories, AccessLevel.Minimal);
public IComponentMetadata Metadata => _meta;
public UserControl CreateControl(IHostServices host) => new UI.CategoriesDirectoryControl(host);
}

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ComponentOrientedPlatform.Contract" Version="1.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="NikitaKOP" Version="1.0.2" />
<PackageReference Include="TimKOP" Version="1.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WinFormsComponentOrientedHost\WinFormsComponentOrientedHost.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,24 @@
using ComponentOrientedPlatform.Abstractions;
using ComponentOrientedPlatform.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Directories.Categories.Component;
public sealed class ProductsDirectoryComponent : IComponentContract
{
private static readonly IComponentMetadata _meta =
new ComponentMetadata(
id: "directories.products",
title: "Продукты",
group: ComponentMenuGroup.Directories,
level: AccessLevel.Basic);
public IComponentMetadata Metadata => _meta;
public UserControl CreateControl(IHostServices host)
=> new UI.ProductsDirectoryControl(host);
}

View File

@@ -0,0 +1,223 @@
using System;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using ComponentOrientedPlatform.Abstractions; // IHostServices
using Microsoft.EntityFrameworkCore;
using WinFormsComponentOrientedHost.Data; // IAppDbContextFactory
using WinFormsComponentOrientedHost.Data.Entities; // Category
namespace Directories.Categories.Component.UI
{
public sealed class CategoriesDirectoryControl : UserControl
{
private readonly IHostServices _host;
private readonly IAppDbContextFactory _factory;
private readonly BindingList<Category> _data = new();
private readonly DataGridView _grid = new()
{
Dock = DockStyle.Fill,
AutoGenerateColumns = false,
AllowUserToAddRows = false,
EditMode = DataGridViewEditMode.EditOnEnter,
SelectionMode = DataGridViewSelectionMode.FullRowSelect
};
public CategoriesDirectoryControl(IHostServices host)
{
_host = host;
_factory = host.GetService<IAppDbContextFactory>()
?? throw new InvalidOperationException("Db factory not registered.");
_grid.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = nameof(Category.name), // нижний регистр свойств
HeaderText = "Категория",
AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill
});
_grid.DataSource = _data;
Controls.Add(_grid);
Load += async (_, __) => await ReloadAsync();
// хоткеи
_grid.KeyDown += Grid_KeyDown;
_grid.EditingControlShowing += (s, e) =>
{
e.Control.KeyDown -= EditingControl_KeyDown;
e.Control.KeyDown += EditingControl_KeyDown;
};
// запрет пустого
_grid.CellValidating += Grid_CellValidating;
_grid.CurrentCellDirtyStateChanged += (_, __) =>
{
if (_grid.IsCurrentCellDirty)
_grid.CommitEdit(DataGridViewDataErrorContexts.Commit);
};
_grid.RowValidated += async (_, e) =>
{
// e.RowIndex гарантированно валиден и биндинг уже обновлён
await SaveRowAsync(e.RowIndex);
};
// чтобы не падать на биндинговых ошибках во время редактирования
_grid.DataError += (s, e) => e.ThrowException = false;
}
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
if (keyData == Keys.Insert) { CreateNew(); return true; }
if (keyData == Keys.Delete) { _ = DeleteSelectedAsync(); return true; }
return base.ProcessCmdKey(ref msg, keyData);
}
// ---- Публичные действия (для Ctrl+A/U/D из хоста)
public void CreateNew()
{
var item = new Category { name = "" };
_data.Add(item);
var idx = _data.IndexOf(item);
_grid.CurrentCell = _grid.Rows[idx].Cells[0];
_grid.BeginEdit(true);
}
public void EditSelected() { if (_grid.CurrentCell != null) _grid.BeginEdit(true); }
public async void DeleteSelected() => await DeleteSelectedAsync();
// ---- Загрузка
public async Task ReloadAsync()
{
try
{
await using var db = _factory.CreateDbContext();
var list = await db.Categories.AsNoTracking().OrderBy(x => x.name).ToListAsync();
_data.Clear(); foreach (var c in list) _data.Add(c);
}
catch (Exception ex)
{
_host.Logger.Error("Categories reload error", ex);
MessageBox.Show("Ошибка загрузки категорий.", "Ошибка",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private async Task SaveRowAsync(int rowIndex)
{
if (rowIndex < 0 || rowIndex >= _data.Count) return;
// На всякий случай убедимся, что грид закоммитил правки
if (_grid.IsCurrentCellInEditMode)
_grid.EndEdit(DataGridViewDataErrorContexts.Commit);
var item = _data[rowIndex];
if (item == null) return;
// Пустые не сохраняем
if (string.IsNullOrWhiteSpace(item.name)) return;
try
{
await using var db = _factory.CreateDbContext();
// уникальность
bool exists = await db.Categories.AnyAsync(x => x.name == item.name && x.id != item.id);
if (exists)
{
MessageBox.Show("Такая категория уже существует.", "Валидация",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
// Без ReloadAsync: просто подсветим ошибку
var row = _grid.Rows[rowIndex];
row.ErrorText = "Дубликат названия";
return;
}
var tracked = await db.Categories.FirstOrDefaultAsync(x => x.id == item.id);
if (tracked is null)
{
if (item.id == Guid.Empty) item.id = Guid.NewGuid();
db.Categories.Add(item);
}
else
{
tracked.name = item.name;
}
await db.SaveChangesAsync();
// Локально обновим строку/ошибку; перезагрузка не нужна
var r = _grid.Rows[rowIndex];
r.ErrorText = string.Empty;
_grid.InvalidateRow(rowIndex);
}
catch (Exception ex)
{
_host.Logger.Error("Save category error", ex);
MessageBox.Show("Ошибка сохранения.", "Ошибка",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private async Task DeleteSelectedAsync()
{
if (_grid.CurrentRow?.DataBoundItem is not Category c) return;
if (MessageBox.Show("Удалить выбранную категорию?",
"Подтверждение", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes)
return;
try
{
await using var db = _factory.CreateDbContext();
var tracked = await db.Categories.FirstOrDefaultAsync(x => x.id == c.id);
if (tracked != null)
{
db.Categories.Remove(tracked);
await db.SaveChangesAsync();
}
_data.Remove(c);
}
catch (Exception ex)
{
_host.Logger.Error("Delete category error", ex);
MessageBox.Show("Ошибка удаления.", "Ошибка",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
// ---- Вспомогательные обработчики
private void Grid_KeyDown(object? s, KeyEventArgs e)
{
if (e.KeyCode == Keys.Insert) { CreateNew(); e.Handled = true; }
else if (e.KeyCode == Keys.Delete) { _ = DeleteSelectedAsync(); e.Handled = true; }
}
private void EditingControl_KeyDown(object? sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Insert) { CreateNew(); e.Handled = true; }
else if (e.KeyCode == Keys.Delete) { _ = DeleteSelectedAsync(); e.Handled = true; }
}
private void Grid_CellValidating(object? s, DataGridViewCellValidatingEventArgs e)
{
if (e.ColumnIndex != 0) return;
var text = (e.FormattedValue?.ToString() ?? "").Trim();
if (string.IsNullOrEmpty(text))
{
_grid.Rows[e.RowIndex].ErrorText = "Название не может быть пустым";
e.Cancel = true;
}
else
{
_grid.Rows[e.RowIndex].ErrorText = "";
}
}
}
}

View File

@@ -0,0 +1,167 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using ComponentOrientedPlatform.Abstractions; // если у тебя Contract.Abstractions — замени этот using
using WinFormsComponentOrientedHost.Data.Entities; // Product
using WinFormsControlLibrary1; // ComboSingleAddControl
using KopComponent; // InputComponent
namespace Directories.Categories.Component.UI
{
public sealed class ProductEditForm : Form
{
private readonly IHostServices _host;
private readonly Product _model;
private readonly List<string> _categories;
private readonly TextBox _tbName = new() { Dock = DockStyle.Fill };
private readonly TextBox _tbDesc = new() { Dock = DockStyle.Fill, Multiline = true, Height = 80 };
private readonly ComboSingleAddControl _cbCategory = new() { Dock = DockStyle.Fill };
private readonly InputComponent _nbQuantity = new() { Dock = DockStyle.Fill };
private readonly Button _btnOk = new() { Text = "Сохранить", DialogResult = DialogResult.OK, AutoSize = true };
private readonly Button _btnCancel = new() { Text = "Отмена", DialogResult = DialogResult.Cancel, AutoSize = true };
private bool _dirty = false;
private bool _initializing = true;
public ProductEditForm(IHostServices host, IEnumerable<string> categories, Product model)
{
_host = host;
_model = model;
_categories = categories.Distinct().OrderBy(s => s).ToList();
Text = (_model.id == default) ? "Создание продукта" : "Редактирование продукта";
StartPosition = FormStartPosition.CenterParent;
Width = 640;
Height = 420;
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
MinimizeBox = false;
// --- Таблица полей ---
var table = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 2,
RowCount = 4,
Padding = new Padding(10)
};
table.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 140));
table.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
for (int i = 0; i < 4; i++) table.RowStyles.Add(new RowStyle(SizeType.AutoSize));
table.Controls.Add(new Label { Text = "Название:", AutoSize = true }, 0, 0);
table.Controls.Add(_tbName, 1, 0);
table.Controls.Add(new Label { Text = "Описание:", AutoSize = true }, 0, 1);
table.Controls.Add(_tbDesc, 1, 1);
table.Controls.Add(new Label { Text = "Категория:", AutoSize = true }, 0, 2);
table.Controls.Add(_cbCategory, 1, 2);
table.Controls.Add(new Label { Text = "Количество:", AutoSize = true }, 0, 3);
table.Controls.Add(_nbQuantity, 1, 3);
// --- Нижняя панель с кнопками ---
var bottomBar = new FlowLayoutPanel
{
Dock = DockStyle.Bottom,
FlowDirection = FlowDirection.RightToLeft,
Padding = new Padding(10),
AutoSize = true,
WrapContents = false
};
bottomBar.Controls.Add(_btnCancel);
bottomBar.Controls.Add(_btnOk);
// ВАЖНО: сначала добавляем table (Fill), затем bottomBar (Bottom)
Controls.Add(table);
Controls.Add(bottomBar);
// Делает Enter=ОК, Esc=Отмена
this.AcceptButton = _btnOk;
this.CancelButton = _btnCancel;
// --- Данные в контролы ---
_cbCategory.ClearItems();
foreach (var c in _categories) _cbCategory.AddValue(c);
_tbName.Text = _model.name ?? string.Empty;
_tbDesc.Text = _model.description ?? string.Empty;
_tbName.TextChanged += OnAnyValueChanged;
_tbDesc.TextChanged += OnAnyValueChanged;
_cbCategory.SelectedValueChanged += OnAnyValueChanged;
_initializing = false;
if (!string.IsNullOrWhiteSpace(_model.categoryText))
_cbCategory.SelectedValue = _model.categoryText;
_nbQuantity.TextValue = _model.quantity;
_btnOk.Click += (_, __) =>
{
var name = (_tbName.Text ?? "").Trim();
var desc = string.IsNullOrWhiteSpace(_tbDesc.Text) ? null : _tbDesc.Text.Trim();
var cat = _cbCategory.SelectedValue ?? "";
var qty = _nbQuantity.TextValue;
if (string.IsNullOrWhiteSpace(name))
{
MessageBox.Show(this, "Название не может быть пустым.", "Валидация",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
this.DialogResult = DialogResult.None;
return;
}
if (string.IsNullOrWhiteSpace(cat))
{
MessageBox.Show(this, "Нужно выбрать категорию.", "Валидация",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
this.DialogResult = DialogResult.None;
return;
}
if (qty is < 0)
{
MessageBox.Show(this, "Количество не может быть отрицательным.", "Валидация",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
this.DialogResult = DialogResult.None;
return;
}
_model.name = name;
_model.description = desc;
_model.categoryText = cat;
_model.quantity = qty;
_dirty = false;
};
this.FormClosing += (s, e) =>
{
if (this.DialogResult == DialogResult.OK) return;
if (_dirty)
{
var ans = MessageBox.Show(
this,
"Есть несохранённые изменения. Закрыть без сохранения?",
"Подтверждение",
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning,
MessageBoxDefaultButton.Button2);
if (ans == DialogResult.No)
{
e.Cancel = true;
}
}
};
}
private void OnAnyValueChanged(object? sender, EventArgs e)
{
if (_initializing) return;
_dirty = true;
}
}
}

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -0,0 +1,214 @@
using ComponentOrientedPlatform.Abstractions;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WinFormsComponentOrientedHost.Data;
using WinFormsComponentOrientedHost.Data.Entities;
using WinFormsControlLibrary;
namespace Directories.Categories.Component.UI;
public sealed class ProductsDirectoryControl : UserControl
{
private readonly IHostServices _host;
private readonly IAppDbContextFactory _factory;
private readonly ToolStrip _toolbar = new();
private readonly ToolStripButton _btnAdd = new("Добавить");
private readonly ToolStripButton _btnEdit = new("Изменить");
private readonly ToolStripButton _btnDelete = new("Удалить");
private readonly ContextMenuStrip _ctx = new();
private readonly ToolStripMenuItem _miAdd = new("Добавить");
private readonly ToolStripMenuItem _miEdit = new("Изменить");
private readonly ToolStripMenuItem _miDelete = new("Удалить");
private readonly HierarchicalTreeView _tree = new() { Dock = DockStyle.Fill };
public ProductsDirectoryControl(IHostServices host)
{
_host = host;
_factory = host.GetService<IAppDbContextFactory>()
?? throw new InvalidOperationException("IAppDbContextFactory is not registered.");
_toolbar.Items.AddRange(new ToolStripItem[] { _btnAdd, _btnEdit, _btnDelete });
_btnAdd.Click += (_, __) => CreateNew();
_btnEdit.Click += (_, __) => EditSelected();
_btnDelete.Click += (_, __) => DeleteSelected();
_ctx.Items.AddRange(new ToolStripItem[] { _miAdd, _miEdit, _miDelete });
_miAdd.Click += (_, __) => CreateNew();
_miEdit.Click += (_, __) => EditSelected();
_miDelete.Click += (_, __) => DeleteSelected();
_tree.ContextMenuStrip = _ctx;
_tree.SetHierarchy("CategoryText", "Quantity", "Name", "Id");
var grid = new TableLayoutPanel { Dock = DockStyle.Fill, RowCount = 2 };
grid.RowStyles.Add(new RowStyle(SizeType.AutoSize));
grid.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
grid.Controls.Add(_toolbar, 0, 0);
grid.Controls.Add(_tree, 0, 1);
Controls.Add(grid);
Load += async (_, __) => await ReloadAsync();
}
private async Task ReloadAsync()
{
await using var db = _factory.CreateDbContext();
var list = await db.Products
.AsNoTracking()
.OrderBy(p => p.categoryText)
.ThenBy(p => p.quantity)
.ThenBy(p => p.name)
.ToListAsync();
// Простой пересбор дерева: чистим и добавляем по одному
_tree.SetHierarchy("CategoryText", "Quantity", "Name", "Id");
foreach (var p in list)
_tree.AddObject(p);
}
public async void CreateNew()
{
await using var db = _factory.CreateDbContext();
// Подтянем список категорий для формы (Name)
var categories = await db.Categories
.AsNoTracking()
.OrderBy(c => c.name)
.Select(c => c.name)
.ToListAsync();
var product = new Product();
using var dlg = new ProductEditForm(_host, categories, product);
if (dlg.ShowDialog(this) == DialogResult.OK)
{
// валидация
if (string.IsNullOrWhiteSpace(product.name))
{
MessageBox.Show("Название не может быть пустым.", "Валидация",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
if (string.IsNullOrWhiteSpace(product.categoryText))
{
MessageBox.Show("Нужно выбрать категорию.", "Валидация",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
if (product.quantity is < 0)
{
MessageBox.Show("Количество не может быть отрицательным.", "Валидация",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
db.Products.Add(product);
await db.SaveChangesAsync();
await ReloadAsync();
}
}
public async void EditSelected()
{
// Получим выбранный продукт по узлу Id (последний уровень)
var selected = _tree.GetSelectedObject<Product>();
if (selected == null)
{
MessageBox.Show("Выберите конечный элемент (Id).", "Подсказка",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
await using var db = _factory.CreateDbContext();
var tracked = await db.Products.FirstOrDefaultAsync(p => p.id == selected.id);
if (tracked == null)
{
MessageBox.Show("Запись не найдена (могла быть удалена).", "Информация",
MessageBoxButtons.OK, MessageBoxIcon.Information);
await ReloadAsync();
return;
}
var categories = await db.Categories
.AsNoTracking()
.OrderBy(c => c.name)
.Select(c => c.name)
.ToListAsync();
// Редактируем копию, чтобы не затрагивать tracked до подтверждения
var copy = new Product
{
id = tracked.id,
name = tracked.name,
description = tracked.description,
categoryText = tracked.categoryText,
quantity = tracked.quantity
};
using var dlg = new ProductEditForm(_host, categories, copy);
if (dlg.ShowDialog(this) == DialogResult.OK)
{
// валидация
if (string.IsNullOrWhiteSpace(copy.name))
{
MessageBox.Show("Название не может быть пустым.", "Валидация",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
if (string.IsNullOrWhiteSpace(copy.categoryText))
{
MessageBox.Show("Нужно выбрать категорию.", "Валидация",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
if (copy.quantity is < 0)
{
MessageBox.Show("Количество не может быть отрицательным.", "Валидация",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
tracked.name = copy.name;
tracked.description = copy.description;
tracked.categoryText = copy.categoryText;
tracked.quantity = copy.quantity;
await db.SaveChangesAsync();
await ReloadAsync();
}
}
public async void DeleteSelected()
{
var selected = _tree.GetSelectedObject<Product>();
if (selected == null)
{
MessageBox.Show("Выберите конечный элемент (Id).", "Подсказка",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
if (MessageBox.Show("Удалить выбранный продукт?", "Подтверждение",
MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes)
return;
await using var db = _factory.CreateDbContext();
var tracked = await db.Products.FirstOrDefaultAsync(p => p.id == selected.id);
if (tracked != null)
{
db.Products.Remove(tracked);
await db.SaveChangesAsync();
}
await ReloadAsync();
}
public void CreateNew(object? _ = null) => CreateNew();
public void EditSelected(object? _ = null) => EditSelected();
public void DeleteSelected(object? _ = null) => DeleteSelected();
}

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -0,0 +1,82 @@
using ComponentOrientedPlatform.Abstractions;
using OfficeOpenXml;
using OfficeOpenXml.Drawing.Chart;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ReportPlugin.Excel.LineChart;
public sealed class LineChartExcelReport : IReportDocumentWithChartLineContract
{
public string DocumentFormat => "xlsx";
private List<string>? _xLabels;
public void SetCategories(List<string> labels) => _xLabels = labels;
public async Task CreateDocumentAsync(
string filePath,
string header,
string chartTitle,
Dictionary<string, List<(int Parameter, double Value)>> series)
{
if (string.IsNullOrWhiteSpace(filePath)) throw new ArgumentNullException(nameof(filePath));
if (string.IsNullOrWhiteSpace(header)) throw new ArgumentNullException(nameof(header));
if (string.IsNullOrWhiteSpace(chartTitle)) throw new ArgumentNullException(nameof(chartTitle));
if (series is null || series.Count == 0) throw new ArgumentOutOfRangeException(nameof(series));
var parameters = series.First().Value.Select(v => v.Parameter).ToArray();
foreach (var s in series.Values)
if (!parameters.SequenceEqual(s.Select(v => v.Parameter)))
throw new ArgumentException("В разных сериях различаются параметры.");
ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
using var package = new ExcelPackage();
var ws = package.Workbook.Worksheets.Add("Диаграмма");
ws.Cells["A1"].Value = header;
ws.Cells["A1"].Style.Font.Bold = true;
ws.Cells["A1"].Style.Font.Size = 18;
ws.Cells[3, 1].Value = "Категория";
int col = 2;
foreach (var key in series.Keys)
{
ws.Cells[3, col].Value = key;
col++;
}
var labels = _xLabels ?? parameters.Select(i => $"Категория {i}").ToList();
int n = parameters.Length;
for (int i = 0; i < n; i++)
{
ws.Cells[4 + i, 1].Value = labels[i];
col = 2;
foreach (var s in series.Values)
{
ws.Cells[4 + i, col].Value = s[i].Value;
col++;
}
}
var chart = ws.Drawings.AddChart(chartTitle, eChartType.ColumnClustered) as ExcelBarChart;
chart.Title.Text = chartTitle;
chart.SetPosition(1, 0, series.Count + 2, 0);
chart.SetSize(900, 420);
var xRange = ws.Cells[4, 1, 3 + n, 1];
col = 2;
foreach (var key in series.Keys)
{
var yRange = ws.Cells[4, col, 3 + n, col];
chart.Series.Add(yRange, xRange).Header = key;
col++;
}
await package.SaveAsAsync(new FileInfo(filePath));
}
}

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<RootNamespace>ReportPlugin.Excel.LineChart_</RootNamespace>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="EPPlus" Version="7.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ComponentOrientedPlatform\ComponentOrientedPlatform.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ClosedXML" Version="0.102.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ComponentOrientedPlatform\ComponentOrientedPlatform.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,223 @@
using ClosedXML.Excel;
using ComponentOrientedPlatform.Abstractions;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace ReportPlugin.Excel.TableTwoHeaderRows;
public sealed class TableTwoHeaderRowsExcelReport : IReportDocumentWithTableColumnRowHeaderContract
{
public string DocumentFormat => "xlsx";
public Task CreateDocumentAsync<T>(
string filePath,
string header,
List<int> columnsWidth,
List<int> rowsHeights,
bool isHeaderFirstRow,
List<(string Header, string PropertyName, string FiledName)> headers,
List<T> data)
{
if (string.IsNullOrWhiteSpace(filePath))
throw new ArgumentNullException(nameof(filePath));
if (string.IsNullOrWhiteSpace(header))
throw new ArgumentNullException(nameof(header));
if (headers is null || headers.Count == 0)
throw new ArgumentOutOfRangeException(nameof(headers));
if (data is null || data.Count == 0)
throw new ArgumentOutOfRangeException(nameof(data));
string leftGroupTitle = headers.FirstOrDefault(h => IsFiled(h.FiledName, "__left_group")).Header
?? "Характеристика";
string left1Title = headers.FirstOrDefault(h => IsFiled(h.FiledName, "__left1")).Header
?? "Атрибут";
string left2Title = headers.FirstOrDefault(h => IsFiled(h.FiledName, "__left2")).Header
?? "Описание";
var titleEntry = headers.FirstOrDefault(h => IsFiled(h.FiledName, "__title"));
PropertyInfo? titleProp = null;
if (!string.IsNullOrWhiteSpace(titleEntry.PropertyName))
titleProp = typeof(T).GetProperty(titleEntry.PropertyName,
BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
var attributeRows = headers
.Where(h => !IsSpecial(h.FiledName))
.ToList();
var type = typeof(T);
var attrProps = attributeRows
.Select(h => string.IsNullOrWhiteSpace(h.PropertyName)
? null
: type.GetProperty(h.PropertyName))
.ToList();
using var wb = new XLWorkbook();
var ws = wb.AddWorksheet("Таблица");
int products = data.Count;
int totalCols = 2 + products;
ws.Cell(1, 1).Value = header;
ws.Range(1, 1, 1, totalCols).Merge()
.Style.Font.SetBold()
.Font.SetFontSize(18)
.Alignment.SetHorizontal(XLAlignmentHorizontalValues.Center)
.Alignment.SetVertical(XLAlignmentVerticalValues.Center);
ws.Row(1).Height = 28;
int topRow = 3;
ws.Range(topRow, 1, topRow, 2).Merge();
ws.Cell(topRow, 1).Value = leftGroupTitle;
for (int p = 0; p < products; p++)
{
string title = $"Элемент {p + 1}";
if (titleProp != null)
{
var v = titleProp.GetValue(data[p], null);
var s = FormatValue(v);
if (!string.IsNullOrWhiteSpace(s)) title = s;
}
ws.Cell(topRow, 3 + p).Value = title;
}
ws.Cell(topRow + 1, 1).Value = left1Title;
ws.Cell(topRow + 1, 2).Value = left2Title;
var head = ws.Range(topRow, 1, topRow + 1, totalCols);
if (isHeaderFirstRow)
head.Style.Fill.BackgroundColor = XLColor.LightGray;
head.Style.Font.Bold = true;
head.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
head.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center;
head.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
head.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
int firstDataRow = topRow + 2;
for (int r = 0; r < attributeRows.Count; r++)
{
var (hdr, propName, filedName) = attributeRows[r];
int row = firstDataRow + r;
var c1 = ws.Cell(row, 1);
var c2 = ws.Cell(row, 2);
c1.Value = hdr ?? string.Empty;
c2.Value = !string.IsNullOrWhiteSpace(filedName) ? filedName : (propName ?? string.Empty);
c1.Style.Fill.BackgroundColor = XLColor.FromArgb(235, 235, 235);
c2.Style.Fill.BackgroundColor = XLColor.FromArgb(235, 235, 235);
c1.Style.Font.Bold = true;
c2.Style.Font.Bold = true;
c1.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
c2.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
c1.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center;
c2.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center;
for (int p = 0; p < products; p++)
{
var cell = ws.Cell(row, 3 + p);
var prop = attrProps[r];
if (prop is null)
{
cell.SetValue(string.Empty);
continue;
}
var val = prop.GetValue(data[p], null);
if (prop.Name.Equals("quantity", StringComparison.OrdinalIgnoreCase))
{
if (val is null) cell.SetValue("отсутствует");
else cell.SetValue(Convert.ToDouble(val, CultureInfo.InvariantCulture));
}
else
{
WriteValue(cell, val);
}
}
if (rowsHeights is not null && r < rowsHeights.Count && rowsHeights[r] > 0)
ws.Row(row).Height = rowsHeights[r];
}
var all = ws.Range(topRow, 1, firstDataRow + attributeRows.Count - 1, totalCols);
all.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
all.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
ApplyColumnWidths(ws, totalCols, columnsWidth, products);
wb.SaveAs(filePath);
return Task.CompletedTask;
}
private static bool IsFiled(string? filed, string key) =>
!string.IsNullOrWhiteSpace(filed) &&
filed.Trim().Equals(key, StringComparison.OrdinalIgnoreCase);
private static bool IsSpecial(string? filed) =>
IsFiled(filed, "__title") || IsFiled(filed, "__left_group") ||
IsFiled(filed, "__left1") || IsFiled(filed, "__left2");
private static void WriteValue(IXLCell cell, object? v)
{
switch (v)
{
case null:
cell.SetValue(string.Empty);
break;
case DateTime dt:
cell.SetValue(dt);
cell.Style.DateFormat.Format = "dd.MM.yyyy";
break;
case DateOnly d:
cell.SetValue(d.ToDateTime(TimeOnly.MinValue));
cell.Style.DateFormat.Format = "dd.MM.yyyy";
break;
default:
cell.SetValue(Convert.ToString(v, CultureInfo.CurrentCulture) ?? string.Empty);
break;
}
}
private static void ApplyColumnWidths(IXLWorksheet ws, int totalCols, List<int>? weights, int products)
{
if (weights is null || weights.Count != totalCols || weights.All(w => w <= 0))
{
ws.Column(1).Width = 18;
ws.Column(2).Width = 28;
for (int p = 0; p < products; p++)
ws.Column(3 + p).Width = 14;
return;
}
double sum = weights.Sum(w => (double)Math.Max(1, w));
double totalWidth = 12.0 * totalCols;
for (int c = 0; c < totalCols; c++)
{
var w = Math.Max(1, weights[c]);
var width = Math.Max(6.0, totalWidth * w / sum);
ws.Column(1 + c).Width = width;
}
}
private static string FormatValue(object? v) =>
v switch
{
null => string.Empty,
DateTime dt => dt.ToString("dd.MM.yyyy", CultureInfo.CurrentCulture),
DateOnly d => d.ToString("dd.MM.yyyy", CultureInfo.CurrentCulture),
_ => Convert.ToString(v, CultureInfo.CurrentCulture) ?? string.Empty
};
}

View File

@@ -0,0 +1,40 @@
using ClosedXML.Excel;
using System;
using System.Collections.Generic;
using ComponentOrientedPlatform.Abstractions;
namespace ReportPlugin.ExcelBigText;
public sealed class BigTextExcelReport : IReportDocumentWithContextTextsContract
{
public string DocumentFormat => "xlsx";
public Task CreateDocumentAsync(string filePath, string header, List<string> paragraphs)
{
if (string.IsNullOrWhiteSpace(filePath)) throw new ArgumentNullException(nameof(filePath));
if (string.IsNullOrWhiteSpace(header)) throw new ArgumentNullException(nameof(header));
if (paragraphs is null) throw new ArgumentNullException(nameof(paragraphs));
if (paragraphs.Count == 0) throw new ArgumentOutOfRangeException(nameof(paragraphs));
using var wb = new XLWorkbook();
var ws = wb.AddWorksheet("Текст");
ws.Cell(1, 1).Value = header;
ws.Range(1, 1, 1, 1).Merge();
ws.Cell(1, 1).Style.Font.SetBold().Font.SetFontSize(18);
ws.Cell(1, 1).Style.Alignment.SetHorizontal(XLAlignmentHorizontalValues.Center);
int row = 3;
foreach (var p in paragraphs)
{
if (p is null) throw new ArgumentNullException(nameof(paragraphs), "В списке абзацев есть null.");
ws.Cell(row, 1).Value = p;
ws.Cell(row, 1).Style.Alignment.WrapText = true;
row++;
}
ws.Column(1).Width = 120;
wb.SaveAs(filePath);
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ClosedXML" Version="0.102.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ComponentOrientedPlatform\ComponentOrientedPlatform.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,160 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using ComponentOrientedPlatform.Abstractions;
using MigraDoc.DocumentObjectModel;
using MigraDoc.DocumentObjectModel.Shapes.Charts;
using MigraDoc.Rendering;
namespace ReportPlugin.PiePdf;
public sealed class PieChartPdfReport : IReportDocumentWithChartPieContract
{
public string DocumentFormat => "pdf";
public async Task CreateDocumentAsync(
string filePath,
string header,
string chartTitle,
List<(int Parameter, double Value)> series)
{
if (string.IsNullOrWhiteSpace(filePath))
throw new ArgumentNullException(nameof(filePath));
if (string.IsNullOrWhiteSpace(header))
throw new ArgumentNullException(nameof(header));
if (string.IsNullOrWhiteSpace(chartTitle))
throw new ArgumentNullException(nameof(chartTitle));
if (series == null)
throw new ArgumentNullException(nameof(series));
if (series.Count == 0)
throw new ArgumentOutOfRangeException(nameof(series), "Список серий пуст.");
var values = series.Select(s => SanitizeValue(s.Value)).ToList();
var labels = Enumerable.Range(1, values.Count).Select(i => $"Категория {i}").ToList();
await GeneratePdfAsync(filePath, header, chartTitle, labels, values);
}
public async Task CreateDocumentAsync(
string filePath,
string header,
string chartTitle,
IReadOnlyList<string> labels,
IReadOnlyList<double> values)
{
if (labels == null) throw new ArgumentNullException(nameof(labels));
if (values == null) throw new ArgumentNullException(nameof(values));
if (labels.Count != values.Count)
throw new ArgumentException("Длины labels и values должны совпадать.");
var vals = values.Select(SanitizeValue).ToList();
await GeneratePdfAsync(filePath, header, chartTitle, labels.ToList(), vals);
}
[Obsolete]
private async Task GeneratePdfAsync(
string filePath,
string header,
string chartTitle,
List<string> labels,
List<double> values)
{
var items = labels.Zip(values, (l, v) => new { Label = (l ?? "").Trim(), Value = v })
.Where(x => x.Value > 0 && !double.IsNaN(x.Value) && !double.IsInfinity(x.Value))
.OrderByDescending(x => x.Value)
.ToList();
if (items.Count == 0)
throw new InvalidOperationException("Нет валидных данных для диаграммы.");
var doc = new Document();
doc.Info.Title = header;
doc.UseCmykColor = false;
var normal = doc.Styles["Normal"];
normal.Font.Name = "Arial";
normal.Font.Size = 10;
var heading1 = doc.Styles["Heading1"];
heading1.Font.Name = normal.Font.Name;
heading1.Font.Size = 16;
heading1.Font.Bold = true;
heading1.ParagraphFormat.SpaceAfter = "0.5cm";
heading1.ParagraphFormat.Alignment = ParagraphAlignment.Center;
var sec = doc.AddSection();
sec.PageSetup.PageFormat = PageFormat.A4;
sec.PageSetup.TopMargin = Unit.FromCentimeter(2);
sec.PageSetup.BottomMargin = Unit.FromCentimeter(2);
sec.PageSetup.LeftMargin = Unit.FromCentimeter(2);
sec.PageSetup.RightMargin = Unit.FromCentimeter(2);
sec.AddParagraph(header, "Heading1");
var sub = sec.AddParagraph($"Сформировано: {DateTime.Now:dd.MM.yyyy HH:mm}", "Normal");
sub.Format.Alignment = ParagraphAlignment.Right;
sub.Format.SpaceAfter = "0.5cm";
var chart = sec.AddChart(ChartType.Pie2D);
chart.LineFormat.Color = Colors.DarkGray;
chart.LineFormat.Width = Unit.FromPoint(0.6);
chart.Width = Unit.FromCentimeter(16);
chart.Height = Unit.FromCentimeter(10);
chart.PlotArea.TopPadding = Unit.FromCentimeter(0.8);
chart.PlotArea.RightPadding = Unit.FromCentimeter(0.8);
chart.PlotArea.BottomPadding = Unit.FromCentimeter(0.8);
chart.PlotArea.LeftPadding = Unit.FromCentimeter(0.8);
var s = chart.SeriesCollection.AddSeries();
s.HasDataLabel = true;
s.DataLabel.Type = DataLabelType.Percent;
s.DataLabel.Position = DataLabelPosition.OutsideEnd;
s.DataLabel.Font.Size = 9;
var palette = new[]
{
Colors.DodgerBlue, Colors.MediumOrchid, Colors.LimeGreen, Colors.Chocolate,
Colors.OrangeRed, Colors.Gold, Colors.SteelBlue, Colors.MediumSeaGreen
};
for (int i = 0; i < items.Count; i++)
{
var p = s.Add(items[i].Value);
p.FillFormat.Color = palette[i % palette.Length];
p.LineFormat.Color = Colors.White;
p.LineFormat.Width = Unit.FromPoint(0.5);
}
var xSeries = chart.XValues.AddXSeries();
foreach (var it in items)
xSeries.Add(it.Label);
var legend = chart.RightArea.AddLegend();
legend.LineFormat.Color = Colors.DarkGray;
legend.LineFormat.Width = Unit.FromPoint(0.6);
legend.Format.Font.Size = 9;
chart.TopArea.AddParagraph(chartTitle).Format.Alignment = ParagraphAlignment.Center;
var sum = items.Sum(x => x.Value);
var note = sec.AddParagraph($"Всего: {sum.ToString("N0", CultureInfo.CurrentCulture)}", "Normal");
note.Format.SpaceBefore = "0.3cm";
note.Format.Alignment = ParagraphAlignment.Left;
var renderer = new PdfDocumentRenderer(unicode: true) { Document = doc };
await Task.Run(() =>
{
renderer.RenderDocument();
Directory.CreateDirectory(Path.GetDirectoryName(filePath)!);
renderer.Save(filePath);
});
}
private static double SanitizeValue(double v)
=> double.IsNaN(v) || double.IsInfinity(v) || v < 0 ? 0d : v;
}

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\ComponentOrientedPlatform\ComponentOrientedPlatform.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="PDFsharp-GDI" Version="6.2.2" />
<PackageReference Include="PDFsharp-MigraDoc-GDI" Version="6.2.2" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ComponentOrientedPlatform.Contract" Version="1.0.1" />
<PackageReference Include="NikitaKOP" Version="1.0.2" />
<PackageReference Include="TimKOP" Version="1.0.2" />
<PackageReference Include="WinFormsControlLibrary1" Version="1.0.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WinFormsComponentOrientedHost\WinFormsComponentOrientedHost.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,24 @@
using ComponentOrientedPlatform.Abstractions;
using ComponentOrientedPlatform.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Reports.ProductsByCategory.Component;
public sealed class ReportsProductsByCategoryComponent : IComponentContract
{
private static readonly IComponentMetadata _meta =
new ComponentMetadata(
id: "reports.productsByCategory",
title: "Продукты по категориям",
group: ComponentMenuGroup.Reports,
level: AccessLevel.Advanced);
public IComponentMetadata Metadata => _meta;
public UserControl CreateControl(IHostServices host)
=> new UI.ProductsByCategoryReportControl(host);
}

View File

@@ -0,0 +1,188 @@
using ComponentOrientedPlatform.Abstractions;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WinFormsComponentOrientedHost.Data;
using WinFormsControlLibrary;
using WinFormsControlLibrary1;
namespace Reports.ProductsByCategory.Component.UI;
public sealed class ProductsByCategoryReportControl : UserControl
{
private readonly IHostServices _host;
private readonly IAppDbContextFactory _factory;
// Верхняя панель фильтров/кнопок
private readonly ComboSingleAddControl _cbCategory = new() { Dock = DockStyle.Fill };
private readonly Button _btnRefresh = new() { Text = "Обновить", AutoSize = true, Dock = DockStyle.Right };
private readonly Button _btnExport = new() { Text = "Сохранить в файл...", AutoSize = true, Dock = DockStyle.Right };
// Иерархический вывод
private readonly HierarchicalTreeView _tree = new() { Dock = DockStyle.Fill };
// Константа для «все категории»
private const string ALL = "— все категории —";
public ProductsByCategoryReportControl(IHostServices host)
{
_host = host;
_factory = host.GetService<IAppDbContextFactory>()
?? throw new InvalidOperationException("Db factory not registered.");
// Топ-панель
var top = new TableLayoutPanel
{
Dock = DockStyle.Top,
ColumnCount = 4,
AutoSize = true,
Padding = new Padding(8),
};
top.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 120));
top.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
top.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
top.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
top.Controls.Add(new Label { Text = "Категория:", AutoSize = true, TextAlign = System.Drawing.ContentAlignment.MiddleLeft }, 0, 0);
top.Controls.Add(_cbCategory, 1, 0);
top.Controls.Add(_btnRefresh, 2, 0);
top.Controls.Add(_btnExport, 3, 0);
// Дерево
_tree.SetHierarchy("Category", "Product", "Id"); // красота: Категория → «Название (кол-во)» → Id
// Размещение
var root = new TableLayoutPanel { Dock = DockStyle.Fill, RowCount = 2 };
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
root.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
root.Controls.Add(top, 0, 0);
root.Controls.Add(_tree, 0, 1);
Controls.Add(root);
// События
Load += async (_, __) => await InitializeAsync();
_btnRefresh.Click += async (_, __) => await ReloadAsync();
_cbCategory.SelectedValueChanged += async (_, __) => await ReloadAsync();
_btnExport.Click += (_, __) => ExportToFile();
}
private async Task InitializeAsync()
{
await FillCategoriesAsync();
await ReloadAsync();
}
private async Task FillCategoriesAsync()
{
try
{
_cbCategory.ClearItems();
_cbCategory.AddValue(ALL);
await using var db = _factory.CreateDbContext();
var cats = await db.Categories
.AsNoTracking()
.OrderBy(c => c.name)
.Select(c => c.name)
.ToListAsync();
foreach (var c in cats) _cbCategory.AddValue(c);
// по умолчанию — все
_cbCategory.SelectedValue = ALL;
}
catch (Exception ex)
{
_host.Logger.Error("Load categories for report error", ex);
MessageBox.Show("Не удалось загрузить список категорий.", "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private async Task ReloadAsync()
{
try
{
await using var db = _factory.CreateDbContext();
// фильтр по категории, если выбран не ALL
var sel = _cbCategory.SelectedValue;
var query = db.Products.AsNoTracking();
if (!string.IsNullOrWhiteSpace(sel) && sel != ALL)
query = query.Where(p => p.categoryText == sel);
var list = await query
.OrderBy(p => p.categoryText)
.ThenBy(p => p.name)
.ToListAsync();
// строим красивую проекцию, чтобы дерево показывало «Название (кол-во)» и аккуратно null
_tree.SetHierarchy("Category", "Product", "Id");
foreach (var p in list)
{
var vm = new
{
Category = string.IsNullOrWhiteSpace(p.categoryText) ? "— без категории —" : p.categoryText,
Product = p.quantity.HasValue
? $"{p.name} (кол-во: {p.quantity:0.###})"
: $"{p.name} (кол-во: —)",
Id = p.id.ToString()
};
_tree.AddObject(vm);
}
}
catch (Exception ex)
{
_host.Logger.Error("Reload report error", ex);
MessageBox.Show("Ошибка формирования отчёта.", "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void ExportToFile()
{
try
{
using var sfd = new SaveFileDialog
{
Filter = "Текстовый файл (*.txt)|*.txt|CSV (*.csv)|*.csv",
FileName = "products_by_category.txt",
OverwritePrompt = true
};
if (sfd.ShowDialog(this) != DialogResult.OK) return;
var lines = new List<string>();
// Обходим дерево: Категория → Продукт → Id
foreach (TreeNode cat in GetTree().Nodes)
{
lines.Add(cat.Text); // Категория
foreach (TreeNode prod in cat.Nodes)
{
lines.Add($" {prod.Text}"); // «Название (кол-во)»
foreach (TreeNode idn in prod.Nodes)
{
lines.Add($" {idn.Text}"); // Id
}
}
}
File.WriteAllLines(sfd.FileName, lines);
MessageBox.Show("Файл успешно сохранён.", "Готово", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
_host.Logger.Error("Export report error", ex);
MessageBox.Show("Не удалось сохранить файл отчёта.", "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
// маленький хелпер, чтобы добраться до внутреннего TreeView твоего HierarchicalTreeView
private TreeView GetTree()
{
var f = typeof(HierarchicalTreeView).GetField("treeView", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
var tv = f?.GetValue(_tree) as TreeView ?? throw new InvalidOperationException("Не удалось получить TreeView из HierarchicalTreeView.");
return tv;
}
}

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -0,0 +1,60 @@
using ComponentOrientedPlatform.Abstractions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace WinFormsComponentOrientedHost.Composition;
public sealed class ReflectionLoader
{
private readonly string _pluginsPath;
private readonly IAppLogger _log;
public ReflectionLoader(string pluginsPath, IAppLogger log)
{
_pluginsPath = Path.GetFullPath(pluginsPath);
_log = log;
Directory.CreateDirectory(_pluginsPath);
}
public IReadOnlyList<IComponentContract> LoadAll()
{
var result = new List<IComponentContract>();
foreach (var dll in Directory.EnumerateFiles(_pluginsPath, "*.dll"))
{
try
{
_log.Info($"Scanning: {dll}");
var asm = Assembly.LoadFrom(dll);
var types = asm.GetTypes()
.Where(t => !t.IsAbstract && typeof(IComponentContract).IsAssignableFrom(t));
foreach (var t in types)
{
try
{
if (Activator.CreateInstance(t) is IComponentContract instance)
{
result.Add(instance);
_log.Info($"Loaded component: {instance.Metadata.Title} ({instance.Metadata.Id})");
}
}
catch (Exception ex)
{
_log.Error($"Failed to instantiate {t.FullName}", ex);
}
}
}
catch (Exception ex)
{
_log.Error($"Failed to load assembly: {dll}", ex);
}
}
return result;
}
}

View File

@@ -0,0 +1,52 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WinFormsComponentOrientedHost.Data.Entities;
namespace WinFormsComponentOrientedHost.Data;
public sealed class AppDbContext : DbContext
{
private readonly string _cs;
public AppDbContext(string connectionString)
{
_cs = connectionString;
}
public DbSet<Category> Categories => Set<Category>();
public DbSet<Product> Products => Set<Product>();
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
optionsBuilder.UseNpgsql(_cs);
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Category>(b =>
{
b.ToTable("categories", schema: "public");
b.HasKey(x => x.id);
b.Property(x => x.name).IsRequired().HasMaxLength(200);
b.HasIndex(x => x.name).IsUnique();
});
modelBuilder.Entity<Product>(b =>
{
b.ToTable("products", schema: "public");
b.HasKey(x => x.id);
b.Property(x => x.name).IsRequired().HasMaxLength(200);
b.Property(x => x.categoryText).IsRequired().HasMaxLength(200);
b.Property(x => x.description).HasMaxLength(2000);
b.Property(x => x.quantity).HasColumnType("numeric(18,3)");
b.HasIndex(x => x.name);
});
}
}

View File

@@ -0,0 +1,18 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WinFormsComponentOrientedHost.Data;
public interface IAppDbContextFactory : IDbContextFactory<AppDbContext> { }
public sealed class AppDbContextFactory : IAppDbContextFactory
{
private readonly string _cs;
public AppDbContextFactory(string connectionString) => _cs = connectionString;
public AppDbContext CreateDbContext() => new AppDbContext(_cs);
}

View File

@@ -0,0 +1,7 @@
namespace WinFormsComponentOrientedHost.Data.Entities;
public sealed class Category
{
public Guid id { get; set; } = Guid.NewGuid();
public string name { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,10 @@
namespace WinFormsComponentOrientedHost.Data.Entities;
public sealed class Product
{
public Guid id { get; set; } = Guid.NewGuid();
public string name { get; set; } = string.Empty;
public string? description { get; set; }
public string categoryText { get; set; } = string.Empty;
public double? quantity { get; set; }
}

View File

@@ -0,0 +1,25 @@
using ComponentOrientedPlatform.Abstractions;
namespace WinFormsComponentOrientedHost;
public sealed class HostServicesImpl : IHostServices
{
private readonly Dictionary<Type, object> _services = new();
public ILicenseProvider License { get; }
public IAppLogger Logger { get; }
public HostServicesImpl(ILicenseProvider license, IAppLogger logger)
{
License = license;
Logger = logger;
}
public void Register<T>(T service) where T : class
{
_services[typeof(T)] = service;
}
public T? GetService<T>() where T : class
=> _services.TryGetValue(typeof(T), out var s) ? (T)s : null;
}

View File

@@ -0,0 +1,20 @@
using ComponentOrientedPlatform.Abstractions;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WinFormsComponentOrientedHost.Infrastructure;
public sealed class SimpleLogger : IAppLogger
{
public void Info(string message) => Debug.WriteLine($"[INFO] {message}");
public void Warn(string message) => Debug.WriteLine($"[WARN] {message}");
public void Error(string message, Exception? ex = null)
{
Debug.WriteLine($"[ERR ] {message}");
if (ex != null) Debug.WriteLine(ex.ToString());
}
}

View File

@@ -0,0 +1,65 @@
using ComponentOrientedPlatform.Abstractions;
using ComponentOrientedPlatform.Model;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace WinFormsComponentOrientedHost.Licensing;
public sealed class LicenseProvider : ILicenseProvider
{
private readonly LicenseModel _model;
private LicenseProvider(LicenseModel model)
{
_model = model;
}
public AccessLevel CurrentLevel => _model.Level;
public bool IsExpired => _model.Expires.HasValue && _model.Expires.Value.Date < DateTime.UtcNow.Date;
public string? Owner => _model.Owner;
public static ILicenseProvider FromConfig(IConfiguration cfg, IAppLogger log)
{
var path = cfg["License:Path"];
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
{
log.Warn("License file not found, fallback to Minimal.");
return new LicenseProvider(new LicenseModel { Level = AccessLevel.Minimal });
}
try
{
var json = File.ReadAllText(path);
var opts = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
opts.Converters.Add(new JsonStringEnumConverter(allowIntegerValues: true));
var model = JsonSerializer.Deserialize<LicenseModel>(json, opts)
?? new LicenseModel { Level = AccessLevel.Minimal };
return new LicenseProvider(model);
}
catch (Exception ex)
{
log.Error("License read error", ex);
return new LicenseProvider(new LicenseModel { Level = AccessLevel.Minimal });
}
}
private sealed class LicenseModel
{
public AccessLevel Level { get; set; } = AccessLevel.Minimal;
public DateTime? Expires { get; set; }
public string? Owner { get; set; }
}
}

View File

@@ -0,0 +1,56 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using WinFormsComponentOrientedHost.Composition;
using WinFormsComponentOrientedHost.Infrastructure;
using WinFormsComponentOrientedHost.Licensing;
using WinFormsComponentOrientedHost.UI;
namespace WinFormsComponentOrientedHost;
internal static class Program
{
public static IConfiguration Configuration { get; private set; } = default!;
[STAThread]
static void Main()
{
ApplicationConfiguration.Initialize();
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
var builder = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false);
Configuration = builder.Build();
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20> Debug/MsgBox
var logger = new SimpleLogger();
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
var license = LicenseProvider.FromConfig(Configuration, logger);
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
var pluginsPath = Configuration["Plugins:Path"] ?? ".\\Plugins";
var loader = new ReflectionLoader(pluginsPath, logger);
// HostServices
var host = new HostServicesImpl(license, logger);
var cs = Configuration["Database:ConnectionString"] ?? throw new Exception("No DB connection string");
var dbFactory = new WinFormsComponentOrientedHost.Data.AppDbContextFactory(cs);
host.Register<WinFormsComponentOrientedHost.Data.IAppDbContextFactory>(dbFactory);
try
{
using var db = dbFactory.CreateDbContext();
db.Database.EnsureCreated();
db.Database.Migrate();
}
catch (Exception ex)
{
logger.Error("DB migrate failed", ex);
}
Application.Run(new FormMain(Configuration, host, loader));
}
}

View File

@@ -0,0 +1,391 @@
using ComponentOrientedPlatform.Abstractions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using System.Reflection;
using WinFormsComponentOrientedHost.Data;
using WinFormsComponentOrientedHost.Data.Entities;
public sealed class FormExtensions : Form
{
public static IConfiguration Configuration { get; private set; } = default!;
private readonly IHostServices _host;
private readonly ComboBox _cbText = new() { DropDownStyle = ComboBoxStyle.DropDownList, Dock = DockStyle.Fill };
private readonly Button _btnText = new() { Text = "Excel (большой текст: в наличии)", Dock = DockStyle.Fill };
private readonly ComboBox _cbLine = new() { DropDownStyle = ComboBoxStyle.DropDownList, Dock = DockStyle.Fill };
private readonly Button _btnLine = new() { Text = "Excel (диаграмма: нет в наличии)", Dock = DockStyle.Fill };
private readonly ComboBox _cbRowCol = new() { DropDownStyle = ComboBoxStyle.DropDownList, Dock = DockStyle.Fill };
private readonly Button _btnRowCol = new() { Text = "Excel (таблица: 1-я строка + 1-й столбец)", Dock = DockStyle.Fill };
private readonly ComboBox _cbPiePdf = new() { DropDownStyle = ComboBoxStyle.DropDownList, Dock = DockStyle.Fill };
private readonly Button _btnPiePdf = new() { Text = "PDF (круговая: нет в наличии)", Dock = DockStyle.Fill };
private readonly List<IReportDocumentWithContextTextsContract> _textPlugins = new();
private readonly List<IReportDocumentWithChartLineContract> _linePlugins = new();
private readonly List<IReportDocumentWithTableColumnRowHeaderContract> _rowColPlugins = new();
private readonly List<IReportDocumentWithChartPieContract> _piePdfPlugins = new();
public FormExtensions(IHostServices host)
{
_host = host;
Text = "Отчёты → Excel/PDF";
Width = 860;
Height = 280;
StartPosition = FormStartPosition.CenterParent;
var layout = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 2,
RowCount = 4,
AutoSize = true,
Padding = new Padding(12),
};
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 60));
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 40));
layout.RowStyles.Add(new RowStyle(SizeType.Absolute, 40));
layout.RowStyles.Add(new RowStyle(SizeType.Absolute, 40));
layout.RowStyles.Add(new RowStyle(SizeType.Absolute, 40));
layout.RowStyles.Add(new RowStyle(SizeType.Absolute, 40));
layout.Controls.Add(_cbText, 0, 0);
layout.Controls.Add(_btnText, 1, 0);
layout.Controls.Add(_cbLine, 0, 1);
layout.Controls.Add(_btnLine, 1, 1);
layout.Controls.Add(_cbRowCol, 0, 2);
layout.Controls.Add(_btnRowCol, 1, 2);
layout.Controls.Add(_cbPiePdf, 0, 3);
layout.Controls.Add(_btnPiePdf, 1, 3);
Controls.Add(layout);
Load += (_, __) => InitializeAndLoadPlugins();
_btnText.Click += async (_, __) => await GenerateExcelBigTextReport();
_btnLine.Click += async (_, __) => await GenerateExcelOutOfStockLine();
_btnRowCol.Click += async (_, __) => await GenerateExcelRowColHeaderTable();
_btnPiePdf.Click += async (_, __) => await GeneratePdfOutOfStockPie();
}
private void InitializeAndLoadPlugins()
{
Configuration = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false)
.Build();
LoadPlugins();
}
private void LoadPlugins()
{
_cbText.Items.Clear();
_cbLine.Items.Clear();
_cbRowCol.Items.Clear();
_cbPiePdf.Items.Clear();
_textPlugins.Clear();
_linePlugins.Clear();
_rowColPlugins.Clear();
_piePdfPlugins.Clear();
var path = Path.Combine(AppContext.BaseDirectory, "Plugins");
if (!Directory.Exists(path))
{
MessageBox.Show(this, $"Папка с плагинами не найдена:\n{path}", "Расширения",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
foreach (var file in Directory.GetFiles(path, "*.dll"))
{
try
{
var asm = Assembly.LoadFrom(file);
foreach (var type in asm.GetTypes())
{
if (type.IsAbstract || type.IsInterface) continue;
if (typeof(IReportDocumentWithContextTextsContract).IsAssignableFrom(type) &&
Activator.CreateInstance(type) is IReportDocumentWithContextTextsContract t1)
{
_textPlugins.Add(t1);
_cbText.Items.Add($"{type.Name} ({t1.DocumentFormat})");
}
if (typeof(IReportDocumentWithChartLineContract).IsAssignableFrom(type) &&
Activator.CreateInstance(type) is IReportDocumentWithChartLineContract t2)
{
_linePlugins.Add(t2);
_cbLine.Items.Add($"{type.Name} ({t2.DocumentFormat})");
}
if (typeof(IReportDocumentWithTableColumnRowHeaderContract).IsAssignableFrom(type) &&
Activator.CreateInstance(type) is IReportDocumentWithTableColumnRowHeaderContract t3)
{
_rowColPlugins.Add(t3);
_cbRowCol.Items.Add($"{type.Name} ({t3.DocumentFormat})");
}
if (typeof(IReportDocumentWithChartPieContract).IsAssignableFrom(type) &&
Activator.CreateInstance(type) is IReportDocumentWithChartPieContract t4)
{
_piePdfPlugins.Add(t4);
_cbPiePdf.Items.Add($"{type.Name} ({t4.DocumentFormat})");
}
}
}
catch { }
}
if (_cbText.Items.Count > 0) _cbText.SelectedIndex = 0;
if (_cbLine.Items.Count > 0) _cbLine.SelectedIndex = 0;
if (_cbRowCol.Items.Count > 0) _cbRowCol.SelectedIndex = 0;
if (_cbPiePdf.Items.Count > 0) _cbPiePdf.SelectedIndex = 0;
}
private async Task GenerateExcelBigTextReport()
{
if (_cbText.SelectedIndex < 0) return;
var plugin = _textPlugins[_cbText.SelectedIndex];
try
{
var cs = Configuration["Database:ConnectionString"]
?? throw new InvalidOperationException("Не задана строка подключения (Database:ConnectionString).");
var dbFactory = new AppDbContextFactory(cs);
using var db = dbFactory.CreateDbContext();
var productsInStock = await db.Set<Product>()
.AsNoTracking()
.Where(p => (p.quantity ?? 0) > 0)
.OrderBy(p => p.categoryText)
.ThenBy(p => p.name)
.ToListAsync();
if (productsInStock.Count == 0)
{
MessageBox.Show(this, "Нет продуктов в наличии.", "Excel (большой текст)",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var paragraphs = productsInStock.Select(p =>
{
var cat = string.IsNullOrWhiteSpace(p.categoryText) ? "(без категории)" : p.categoryText.Trim();
var name = (p.name ?? "").Trim();
var desc = string.IsNullOrWhiteSpace(p.description) ? "(описание отсутствует)" : p.description!.Trim();
return $"[{cat}] {name} — {desc}";
}).ToList();
using var dlg = new SaveFileDialog
{
Filter = "Excel (*.xlsx)|*.xlsx",
FileName = родукты_в_наличии_текст.xlsx",
OverwritePrompt = true
};
if (dlg.ShowDialog(this) != DialogResult.OK) return;
await plugin.CreateDocumentAsync(
filePath: dlg.FileName,
header: "Продукты в наличии — сводный текст",
paragraphs: paragraphs);
MessageBox.Show(this, $"Excel создан. Строк: {paragraphs.Count}.",
"Готово", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
MessageBox.Show(this, "Ошибка при генерации Excel:\n" + ex.Message,
"Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private async Task GenerateExcelOutOfStockLine()
{
if (_cbLine.SelectedIndex < 0) return;
var plugin = _linePlugins[_cbLine.SelectedIndex];
try
{
var cs = Configuration["Database:ConnectionString"]
?? throw new InvalidOperationException("Не задана строка подключения (Database:ConnectionString).");
var dbFactory = new AppDbContextFactory(cs);
using var db = dbFactory.CreateDbContext();
var byCategory = await db.Set<Product>()
.AsNoTracking()
.GroupBy(p => string.IsNullOrWhiteSpace(p.categoryText) ? "(без категории)" : p.categoryText.Trim())
.Select(g => new
{
Category = g.Key,
OutOfStock = g.Count(p => (p.quantity ?? 0) <= 0)
})
.Where(x => x.OutOfStock > 0)
.OrderByDescending(x => x.OutOfStock)
.ToListAsync();
if (byCategory.Count == 0)
{
MessageBox.Show(this, "Все категории в наличии — нечего показывать.",
"Excel (вертикальные столбцы)", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var series = new Dictionary<string, List<(int Parameter, double Value)>>
{
["Нет в наличии"] = byCategory.Select((x, i) => (Parameter: i + 1, Value: (double)x.OutOfStock)).ToList()
};
using var dlg = new SaveFileDialog
{
Filter = "Excel (*.xlsx)|*.xlsx",
FileName = родукты_нет_в_наличии_поатегориям_столбцы.xlsx",
OverwritePrompt = true
};
if (dlg.ShowDialog(this) != DialogResult.OK) return;
var header = "Отсутствующие товары по категориям";
var chartTitle = "Количество отсутствующих товаров (по категориям)";
await plugin.CreateDocumentAsync(dlg.FileName, header, chartTitle, series);
MessageBox.Show(this, "Excel с диаграммой создан.",
"Готово", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
MessageBox.Show(this, "Ошибка при генерации Excel-диаграммы:\n" + ex.Message,
"Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private async Task GenerateExcelRowColHeaderTable()
{
if (_cbRowCol.SelectedIndex < 0) return;
var plugin = _rowColPlugins[_cbRowCol.SelectedIndex];
try
{
var cs = Configuration["Database:ConnectionString"]
?? throw new InvalidOperationException("Не задана строка подключения (Database:ConnectionString).");
var dbFactory = new AppDbContextFactory(cs);
using var db = dbFactory.CreateDbContext();
var items = await db.Set<Product>()
.AsNoTracking()
.OrderBy(p => p.name)
.ToListAsync();
if (items.Count == 0)
{
MessageBox.Show(this, "Нет данных для отчёта.", "Excel (таблица)",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var rowsHeights = new List<int> { 25, 25 };
var headers = new List<(string Header, string PropertyName, string FiledName)>
{
("Id", "id", "Id"),
("Название", "name", "Name"),
("Категория", "categoryText", "Category"),
("Количество", "quantity", "Quantity"),
};
using var dlg = new SaveFileDialog
{
Filter = "Excel (*.xlsx)|*.xlsx",
FileName = родукты_таблица_две_строки.xlsx",
OverwritePrompt = true
};
if (dlg.ShowDialog(this) != DialogResult.OK) return;
var columnsWidth = new List<int>();
bool isHeaderFirstRow = true;
await plugin.CreateDocumentAsync(
filePath: dlg.FileName,
header: "Продукты — шапка: первые два столбца",
columnsWidth: columnsWidth,
rowsHeights: rowsHeights,
isHeaderFirstRow: isHeaderFirstRow,
headers: headers,
data: items);
MessageBox.Show(this, "Excel-таблица создана.",
"Готово", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
MessageBox.Show(this, "Ошибка при генерации Excel-таблицы:\n" + ex.Message,
"Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private async Task GeneratePdfOutOfStockPie()
{
if (_cbPiePdf.SelectedIndex < 0) return;
var plugin = _piePdfPlugins[_cbPiePdf.SelectedIndex];
try
{
var cs = Configuration["Database:ConnectionString"]
?? throw new InvalidOperationException("Не задана строка подключения (Database:ConnectionString).");
var dbFactory = new AppDbContextFactory(cs);
using var db = dbFactory.CreateDbContext();
var byCategory = await db.Set<Product>()
.AsNoTracking()
.GroupBy(p => string.IsNullOrWhiteSpace(p.categoryText) ? "(без категории)" : p.categoryText.Trim())
.Select(g => new
{
Category = g.Key,
OutOfStock = g.Count(p => (p.quantity ?? 0) <= 0)
})
.Where(x => x.OutOfStock > 0)
.OrderByDescending(x => x.OutOfStock)
.ToListAsync();
if (byCategory.Count == 0)
{
MessageBox.Show(this, "Все категории в наличии — круговую диаграмму строить не из чего.",
"PDF (круговая)", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var series = byCategory
.Select((x, i) => (Parameter: i + 1, Value: (double)x.OutOfStock))
.ToList();
using var dlg = new SaveFileDialog
{
Filter = "PDF (*.pdf)|*.pdf",
FileName = родукты_нет_в_наличии_поатегориям_круговая.pdf",
OverwritePrompt = true
};
if (dlg.ShowDialog(this) != DialogResult.OK) return;
var header = "Отсутствующие товары по категориям";
var chartTitle = "Круговая диаграмма: количество отсутствующих товаров";
await plugin.CreateDocumentAsync(dlg.FileName, header, chartTitle, series);
MessageBox.Show(this, "PDF с круговой диаграммой создан.",
"Готово", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
MessageBox.Show(this, "Ошибка при генерации PDF-диаграммы:\n" + ex.Message,
"Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}

View File

@@ -0,0 +1,135 @@
using ComponentOrientedPlatform.Abstractions;
using ComponentOrientedPlatform.Model;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WinFormsComponentOrientedHost.Composition;
namespace WinFormsComponentOrientedHost.UI;
public sealed class FormMain : Form
{
private readonly IHostServices _host;
private readonly ReflectionLoader _loader;
private readonly IConfiguration _cfg;
private readonly MenuStrip _menu = new();
private readonly ToolStripMenuItem _miDirectories = new("Справочники");
private readonly ToolStripMenuItem _miReports = new("Отчёты");
private readonly ToolStripMenuItem _miReportExtensions = new("Расширения");
private readonly TabControl _tabs = new() { Dock = DockStyle.Fill };
private IReadOnlyList<IComponentContract> _components = Array.Empty<IComponentContract>();
public FormMain(IConfiguration cfg, IHostServices host, ReflectionLoader loader)
{
_cfg = cfg;
_host = host;
_loader = loader;
Text = "Component-Oriented Host";
Width = 1200;
Height = 800;
_menu.Items.AddRange(new ToolStripItem[] { _miDirectories, _miReports, _miReportExtensions });
MainMenuStrip = _menu;
var panel = new TableLayoutPanel { Dock = DockStyle.Fill, RowCount = 2 };
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
panel.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
panel.Controls.Add(_menu, 0, 0);
panel.Controls.Add(_tabs, 0, 1);
Controls.Add(panel);
Load += (_, __) => Initialize();
_tabs.MouseDoubleClick += Tabs_MouseDoubleClick;
KeyPreview = true;
KeyDown += FormMain_KeyDown;
_miReportExtensions.Click += (_, __) => new FormExtensions(_host).ShowDialog();
}
private void Initialize()
{
_components = _loader.LoadAll();
var allowed = _components
.Where(c => (int)c.Metadata.AccessRequired <= (int)_host.License.CurrentLevel)
.ToList();
BuildMenu(allowed);
}
private void BuildMenu(IReadOnlyList<IComponentContract> comps)
{
_miDirectories.DropDownItems.Clear();
_miReports.DropDownItems.Clear();
foreach (var c in comps.OrderBy(c => c.Metadata.Title))
{
var item = new ToolStripMenuItem(c.Metadata.Title);
item.Tag = c;
item.Click += (_, __) => OpenComponent(c);
if (c.Metadata.MenuGroup == ComponentMenuGroup.Directories)
_miDirectories.DropDownItems.Add(item);
else
_miReports.DropDownItems.Add(item);
}
}
private void OpenComponent(IComponentContract comp)
{
var ctrl = comp.CreateControl(_host);
ctrl.Dock = DockStyle.Fill;
var page = new TabPage(comp.Metadata.Title) { Tag = comp };
page.Controls.Add(ctrl);
_tabs.TabPages.Add(page);
_tabs.SelectedTab = page;
}
private void Tabs_MouseDoubleClick(object? sender, MouseEventArgs e)
{
for (int i = 0; i < _tabs.TabPages.Count; i++)
{
if (_tabs.GetTabRect(i).Contains(e.Location))
{
_tabs.TabPages.RemoveAt(i);
break;
}
}
}
private void FormMain_KeyDown(object? sender, KeyEventArgs e)
{
if (_tabs.SelectedTab == null) return;
if (!(e.Control && (e.KeyCode is Keys.A or Keys.U or Keys.D))) return;
var ctrl = _tabs.SelectedTab.Controls.Count > 0 ? _tabs.SelectedTab.Controls[0] : null;
if (ctrl == null) return;
var methodName = e.KeyCode switch
{
Keys.A => "CreateNew",
Keys.U => "EditSelected",
Keys.D => "DeleteSelected",
_ => null
};
if (methodName == null) return;
var mi = ctrl.GetType().GetMethod(methodName, Type.EmptyTypes);
if (mi != null)
{
try { mi.Invoke(ctrl, null); }
catch (Exception ex) { _host.Logger.Error($"Hotkey {methodName} error", ex); }
e.Handled = true;
}
}
}

View File

@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ComponentOrientedPlatform.Contract" Version="1.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="NikitaKOP" Version="1.0.2" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
<PackageReference Include="TimKOP" Version="1.0.2" />
<PackageReference Include="WinFormsControlLibrary1" Version="1.0.5" />
<PackageReference Include="ClosedXML" Version="0.102.4" />
<PackageReference Include="EPPlus" Version="7.0.0" />
<PackageReference Include="PDFsharp-GDI" Version="6.2.2" />
<PackageReference Include="PDFsharp-MigraDoc-GDI" Version="6.2.2" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,7 @@
{
"Plugins": { "Path": ".\\Plugins" },
"License": { "Path": ".\\Licenses\\license.json" },
"Database": {
"ConnectionString": "Host=localhost;Port=5432;Database=kop_db;Username=postgres;Password=1234;"
}
}

View File

@@ -0,0 +1,37 @@
namespace WinFormsControlLibrary1
{
partial class ComboSingleAddControl
{
/// <summary>
/// Обязательная переменная конструктора.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Освободить все используемые ресурсы.
/// </summary>
/// <param name="disposing">истинно, если управляемый ресурс должен быть удален; иначе ложно.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Код, автоматически созданный конструктором компонентов
/// <summary>
/// Требуемый метод для поддержки конструктора — не изменяйте
/// содержимое этого метода с помощью редактора кода.
/// </summary>
private void InitializeComponent()
{
components = new System.ComponentModel.Container();
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
}
#endregion
}
}

View File

@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WinFormsControlLibrary1;
public partial class ComboSingleAddControl : UserControl
{
private readonly ComboBox _combo = new() { Dock = DockStyle.Fill };
public EventHandler? SelectedValueChanged;
public ComboSingleAddControl()
{
InitializeComponent();
Controls.Add(_combo);
_combo.SelectedIndexChanged += (_, __) => SelectedValueChanged?.Invoke(this, EventArgs.Empty);
}
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
public string SelectedValue
{
get => _combo.SelectedItem.ToString() ?? string.Empty;
set
{
if(string.IsNullOrEmpty(value)) { _combo.SelectedIndex = -1; return; }
int id = _combo.Items.IndexOf(value);
if(id >= 0) _combo.SelectedIndex = id;
}
}
public void AddValue(string value)
{
if (string.IsNullOrEmpty(value)) return;
if (_combo.Items.Contains(value)) return;
_combo.Items.Add(value);
if(_combo.SelectedIndex < 0) _combo.SelectedIndex = 0;
}
public void ClearItems()
{
_combo.Items.Clear();
_combo.SelectedIndex = -1;
}
}

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -0,0 +1,37 @@
namespace WinFormsControlLibrary1
{
partial class DateByPatternTextBox
{
/// <summary>
/// Обязательная переменная конструктора.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Освободить все используемые ресурсы.
/// </summary>
/// <param name="disposing">истинно, если управляемый ресурс должен быть удален; иначе ложно.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Код, автоматически созданный конструктором компонентов
/// <summary>
/// Требуемый метод для поддержки конструктора — не изменяйте
/// содержимое этого метода с помощью редактора кода.
/// </summary>
private void InitializeComponent()
{
components = new System.ComponentModel.Container();
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
}
#endregion
}
}

View File

@@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Forms;
using WinFormsControlLibrary1.Exceptions;
using static System.Net.Mime.MediaTypeNames;
namespace WinFormsControlLibrary1;
public partial class DateByPatternTextBox : UserControl
{
private readonly TextBox patternTextBox = new() { Dock = DockStyle.Fill };
private readonly ToolTip _tip = new();
private string? _pattern;
private Regex? _regex;
public event EventHandler? ValueChanged;
public DateByPatternTextBox()
{
InitializeComponent();
Controls.Add(patternTextBox);
patternTextBox.TextChanged += (_, __) => ValueChanged?.Invoke(this, EventArgs.Empty);
}
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
public string? Pattern
{
get => _pattern;
set
{
_pattern = value;
_regex = string.IsNullOrWhiteSpace(value) ? null : new Regex(value, RegexOptions.Compiled);
}
}
public void SetToolTip(string example)
{
_tip.SetToolTip(patternTextBox, example);
}
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
public string Value
{
get
{
if(_regex is null)
{
throw new ComponentValidationException("Не задан шаблон дaты (Pattern).");
}
var text = patternTextBox.Text.Trim();
if (!_regex.IsMatch(text))
{
throw new ComponentValidationException("Введённая дата не соответствует заданному формату.");
}
return text;
}
set
{
if (_regex is null)
{
throw new ComponentValidationException("Не задан шаблон дaты (Pattern).");
}
var text = value.Trim();
if (!_regex.IsMatch(text))
{
throw new ComponentValidationException("Введённая дата не соответствует заданному формату.");
}
patternTextBox.Text = value ?? string.Empty;
}
}
}

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WinFormsControlLibrary1.Exceptions;
public class ComponentValidationException : Exception
{
public ComponentValidationException(string message) : base(message) { }
}

View File

@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WinFormsControlLibrary1.Exceptions;
public class TemplateConfigurationException : Exception
{
public TemplateConfigurationException(string message) : base(message) { }
}

View File

@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WinFormsControlLibrary1.Exceptions;
internal class TemplateParseException : Exception
{
public TemplateParseException(string message) : base(message) { }
}

View File

@@ -0,0 +1,37 @@
namespace WinFormsControlLibrary1
{
partial class TemplatedListBox
{
/// <summary>
/// Обязательная переменная конструктора.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Освободить все используемые ресурсы.
/// </summary>
/// <param name="disposing">истинно, если управляемый ресурс должен быть удален; иначе ложно.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Код, автоматически созданный конструктором компонентов
/// <summary>
/// Требуемый метод для поддержки конструктора — не изменяйте
/// содержимое этого метода с помощью редактора кода.
/// </summary>
private void InitializeComponent()
{
components = new System.ComponentModel.Container();
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
}
#endregion
}
}

View File

@@ -0,0 +1,169 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Xml.Linq;
using WinFormsControlLibrary1.Exceptions;
namespace WinFormsControlLibrary1;
public partial class TemplatedListBox : UserControl
{
private readonly ListBox _lb = new() { Dock = DockStyle.Fill };
private string _template = "";
private string _open = "{";
private string _close = "}";
public event EventHandler? SelectedIndexChanged;
public TemplatedListBox()
{
InitializeComponent();
Controls.Add(_lb);
_lb.HorizontalScrollbar = true;
_lb.SelectedIndexChanged += (_, __) => SelectedIndexChanged?.Invoke(this, EventArgs.Empty);
}
public void SetTemplate(string template, string open = "{", string close = "}")
{
if (string.IsNullOrWhiteSpace(template))
throw new TemplateConfigurationException("Шаблон не должен быть пустым.");
_template = template;
_open = open;
_close = close;
if (template[0] == open[0] || template[template.Length - 1] == close[0])
throw new TemplateConfigurationException("Шаблон не должен начинаться или заканчиваться свойством.");
CheckNoDoubleProperties(template, open, close);
}
public void Clear()
{
_lb.Items.Clear();
}
public void AddItem<T>(T item)
{
if (string.IsNullOrEmpty(_template))
throw new TemplateConfigurationException("Сначала вызовите SetTemplate.");
var text = Render(item);
_lb.Items.Add(text);
}
private string Render(object obj)
{
if (string.IsNullOrEmpty(_template)) return string.Empty;
string result = _template;
var type = obj.GetType();
foreach (var p in type.GetProperties())
{
if (!p.CanRead) continue;
string name = p.Name;
string val = Convert.ToString(p.GetValue(obj)) ?? string.Empty;
result = result.Replace($"{_open}{name}{_close}", val);
}
foreach (var f in type.GetFields())
{
string name = f.Name;
string val = Convert.ToString(f.GetValue(obj)) ?? string.Empty;
result = result.Replace($"{_open}{name}{_close}", val);
}
return result;
}
public T? GetSelectedObject<T>() where T : class, new()
{
if (_lb.SelectedIndex == -1)
{
return null;
}
string row = _lb.SelectedItem!.ToString()!;
T obj = new T();
var type = typeof(T);
List<string> substrings = new List<string>();
List<string> props = ExtractPlaceholders(_template, _open, _close);
string template = _template;
substrings.AddRange(template.Split(_open + _close));
int int1 = 0; int int2 = 0;
for (int i = 0; i < props.Count; i++)
{
int1 = row.IndexOf(substrings[i]) + substrings[i].Length;
int2 = row.IndexOf(substrings[i + 1]);
if (substrings[i + 1] == "")
{
int2 = row.Length;
}
var value = row[int1..int2];
var p = type.GetProperty(props[i]);
if (p is null) continue;
p.SetValue(obj, Convert.ChangeType(value, p.PropertyType));
}
return obj;
}
private List<string> ExtractPlaceholders(string template, string open, string close)
{
var names = new List<string>();
int i = 0;
while (i < template.Length)
{
if (template[i] == open[0])
{
int j = template.IndexOf(close, i + 1, StringComparison.Ordinal);
string name = template.Substring(i + 1, j - i - 1).Trim();
names.Add(name);
i = j + 1;
}
else i++;
}
return names;
}
private static void CheckNoDoubleProperties(string template, string open, string close)
{
for (int i = 0; i < template.Length; i++)
{
if (template[i] == close[0])
{
int k = i + 1;
while (k < template.Length && char.IsWhiteSpace(template[k])) k++;
if (k < template.Length && template[k] == open[0])
throw new TemplateConfigurationException("В шаблоне не должно идти два свойства подряд без текста между ними.");
}
}
}
}

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -1,38 +0,0 @@
namespace WinFormsControlLibrary1
{
partial class UserControl1
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Component Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
components = new System.ComponentModel.Container();
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(800, 450);
}
#endregion
}
}

View File

@@ -1,10 +0,0 @@
namespace WinFormsControlLibrary1
{
public partial class UserControl1 : UserControl
{
public UserControl1()
{
InitializeComponent();
}
}
}

View File

@@ -1,10 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0-windows</TargetFramework>
<TargetFramework>net8.0-windows7.0</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
<Title>MyComponentsLibrary</Title>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<Authors>Romtec</Authors>
<Version>1.0.5</Version>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,73 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.36401.2
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinFormsControlLibrary1", "WinFormsControlLibrary1.csproj", "{C8BBBE49-732C-C899-B020-6B87BCBF1CD2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ComponentOrientedPlatform", "..\ComponentOrientedPlatform\ComponentOrientedPlatform.csproj", "{43CEB8C8-3154-4F4F-B332-42F98E5CF92D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinFormsComponentOrientedHost", "..\WinFormsComponentOrientedHost\WinFormsComponentOrientedHost.csproj", "{0666CFCA-1AB2-4AB6-8055-40DF4BA26080}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Directories.Categories.Component", "..\Directories.Categories.Component\Directories.Categories.Component.csproj", "{C7107D13-1E5D-4FAE-AE91-CAF9BA136B3C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reports.ProductsByCategory.Component", "..\Reports.ProductsByCategory.Component\Reports.ProductsByCategory.Component.csproj", "{03DD1893-91F1-4AD9-892B-28A1B538EF9C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReportPlugin.ExcelBigText", "..\ReportPlugin.ExcelBigText\ReportPlugin.ExcelBigText.csproj", "{54447AE5-6A58-4220-8D78-B8C5B16F6137}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReportPlugin.Excel.LineChart;", "..\ReportPlugin.Excel.LineChart;\ReportPlugin.Excel.LineChart;.csproj", "{8975F9A5-133C-4AD0-A4E7-7C72E8A04FAF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReportPlugin.Excel.TableTwoHeaderRows", "..\ReportPlugin.Excel.TableTwoHeaderRows\ReportPlugin.Excel.TableTwoHeaderRows.csproj", "{887354BB-2985-4C1C-9050-DEEB6BF0AAE9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReportPlugin.PiePdf", "..\ReportPlugin.PiePdf\ReportPlugin.PiePdf.csproj", "{861464DB-AEB5-4226-956A-B0632A51F612}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{C8BBBE49-732C-C899-B020-6B87BCBF1CD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C8BBBE49-732C-C899-B020-6B87BCBF1CD2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C8BBBE49-732C-C899-B020-6B87BCBF1CD2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C8BBBE49-732C-C899-B020-6B87BCBF1CD2}.Release|Any CPU.Build.0 = Release|Any CPU
{43CEB8C8-3154-4F4F-B332-42F98E5CF92D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{43CEB8C8-3154-4F4F-B332-42F98E5CF92D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{43CEB8C8-3154-4F4F-B332-42F98E5CF92D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{43CEB8C8-3154-4F4F-B332-42F98E5CF92D}.Release|Any CPU.Build.0 = Release|Any CPU
{0666CFCA-1AB2-4AB6-8055-40DF4BA26080}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0666CFCA-1AB2-4AB6-8055-40DF4BA26080}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0666CFCA-1AB2-4AB6-8055-40DF4BA26080}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0666CFCA-1AB2-4AB6-8055-40DF4BA26080}.Release|Any CPU.Build.0 = Release|Any CPU
{C7107D13-1E5D-4FAE-AE91-CAF9BA136B3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C7107D13-1E5D-4FAE-AE91-CAF9BA136B3C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C7107D13-1E5D-4FAE-AE91-CAF9BA136B3C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C7107D13-1E5D-4FAE-AE91-CAF9BA136B3C}.Release|Any CPU.Build.0 = Release|Any CPU
{03DD1893-91F1-4AD9-892B-28A1B538EF9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{03DD1893-91F1-4AD9-892B-28A1B538EF9C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{03DD1893-91F1-4AD9-892B-28A1B538EF9C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{03DD1893-91F1-4AD9-892B-28A1B538EF9C}.Release|Any CPU.Build.0 = Release|Any CPU
{54447AE5-6A58-4220-8D78-B8C5B16F6137}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{54447AE5-6A58-4220-8D78-B8C5B16F6137}.Debug|Any CPU.Build.0 = Debug|Any CPU
{54447AE5-6A58-4220-8D78-B8C5B16F6137}.Release|Any CPU.ActiveCfg = Release|Any CPU
{54447AE5-6A58-4220-8D78-B8C5B16F6137}.Release|Any CPU.Build.0 = Release|Any CPU
{8975F9A5-133C-4AD0-A4E7-7C72E8A04FAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8975F9A5-133C-4AD0-A4E7-7C72E8A04FAF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8975F9A5-133C-4AD0-A4E7-7C72E8A04FAF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8975F9A5-133C-4AD0-A4E7-7C72E8A04FAF}.Release|Any CPU.Build.0 = Release|Any CPU
{887354BB-2985-4C1C-9050-DEEB6BF0AAE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{887354BB-2985-4C1C-9050-DEEB6BF0AAE9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{887354BB-2985-4C1C-9050-DEEB6BF0AAE9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{887354BB-2985-4C1C-9050-DEEB6BF0AAE9}.Release|Any CPU.Build.0 = Release|Any CPU
{861464DB-AEB5-4226-956A-B0632A51F612}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{861464DB-AEB5-4226-956A-B0632A51F612}.Debug|Any CPU.Build.0 = Debug|Any CPU
{861464DB-AEB5-4226-956A-B0632A51F612}.Release|Any CPU.ActiveCfg = Release|Any CPU
{861464DB-AEB5-4226-956A-B0632A51F612}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {98BBBD7D-7A95-4216-927E-F47C3D82C695}
EndGlobalSection
EndGlobal