Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e0b20a07d2 | |||
| c0f6d17928 | |||
| 34d0d1aea6 | |||
| 1fefae50df | |||
| de3cb0a461 | |||
| 3b5d539290 | |||
| a372eeee57 |
14
ComponentOrientedPlatform/Abstractions/IAppLogger.cs
Normal file
14
ComponentOrientedPlatform/Abstractions/IAppLogger.cs
Normal 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);
|
||||
}
|
||||
14
ComponentOrientedPlatform/Abstractions/IComponentContract.cs
Normal file
14
ComponentOrientedPlatform/Abstractions/IComponentContract.cs
Normal 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);
|
||||
}
|
||||
16
ComponentOrientedPlatform/Abstractions/IComponentMetadata.cs
Normal file
16
ComponentOrientedPlatform/Abstractions/IComponentMetadata.cs
Normal 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; }
|
||||
}
|
||||
17
ComponentOrientedPlatform/Abstractions/IHostServices.cs
Normal file
17
ComponentOrientedPlatform/Abstractions/IHostServices.cs
Normal 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;
|
||||
}
|
||||
15
ComponentOrientedPlatform/Abstractions/ILicenseProvider.cs
Normal file
15
ComponentOrientedPlatform/Abstractions/ILicenseProvider.cs
Normal 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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
18
ComponentOrientedPlatform/ComponentOrientedPlatform.csproj
Normal file
18
ComponentOrientedPlatform/ComponentOrientedPlatform.csproj
Normal 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>
|
||||
14
ComponentOrientedPlatform/Model/AccessLevel.cs
Normal file
14
ComponentOrientedPlatform/Model/AccessLevel.cs
Normal 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
|
||||
}
|
||||
13
ComponentOrientedPlatform/Model/ComponentMenuGroup.cs
Normal file
13
ComponentOrientedPlatform/Model/ComponentMenuGroup.cs
Normal 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
|
||||
}
|
||||
24
ComponentOrientedPlatform/Model/ComponentMetadata.cs
Normal file
24
ComponentOrientedPlatform/Model/ComponentMetadata.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
167
Directories.Categories.Component/UI/ProductEditForm.cs
Normal file
167
Directories.Categories.Component/UI/ProductEditForm.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
120
Directories.Categories.Component/UI/ProductEditForm.resx
Normal file
120
Directories.Categories.Component/UI/ProductEditForm.resx
Normal 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>
|
||||
214
Directories.Categories.Component/UI/ProductsDirectoryControl.cs
Normal file
214
Directories.Categories.Component/UI/ProductsDirectoryControl.cs
Normal 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();
|
||||
}
|
||||
@@ -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>
|
||||
82
ReportPlugin.Excel.LineChart;/LineChartExcelReport.cs
Normal file
82
ReportPlugin.Excel.LineChart;/LineChartExcelReport.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
40
ReportPlugin.ExcelBigText/BigTextExcelReport.cs
Normal file
40
ReportPlugin.ExcelBigText/BigTextExcelReport.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
18
ReportPlugin.ExcelBigText/ReportPlugin.ExcelBigText.csproj
Normal file
18
ReportPlugin.ExcelBigText/ReportPlugin.ExcelBigText.csproj
Normal 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>
|
||||
160
ReportPlugin.PiePdf/PieChartPdfReport.cs
Normal file
160
ReportPlugin.PiePdf/PieChartPdfReport.cs
Normal 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;
|
||||
}
|
||||
19
ReportPlugin.PiePdf/ReportPlugin.PiePdf.csproj
Normal file
19
ReportPlugin.PiePdf/ReportPlugin.PiePdf.csproj
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
52
WinFormsComponentOrientedHost/Data/AppDbContext.cs
Normal file
52
WinFormsComponentOrientedHost/Data/AppDbContext.cs
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
18
WinFormsComponentOrientedHost/Data/AppDbContextFactory.cs
Normal file
18
WinFormsComponentOrientedHost/Data/AppDbContextFactory.cs
Normal 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);
|
||||
}
|
||||
7
WinFormsComponentOrientedHost/Data/Entities/Category.cs
Normal file
7
WinFormsComponentOrientedHost/Data/Entities/Category.cs
Normal 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;
|
||||
}
|
||||
10
WinFormsComponentOrientedHost/Data/Entities/Product.cs
Normal file
10
WinFormsComponentOrientedHost/Data/Entities/Product.cs
Normal 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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
20
WinFormsComponentOrientedHost/Infrastructure/SimpleLogger.cs
Normal file
20
WinFormsComponentOrientedHost/Infrastructure/SimpleLogger.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
65
WinFormsComponentOrientedHost/Licensing/LicenseProvider.cs
Normal file
65
WinFormsComponentOrientedHost/Licensing/LicenseProvider.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
56
WinFormsComponentOrientedHost/Program.cs
Normal file
56
WinFormsComponentOrientedHost/Program.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
391
WinFormsComponentOrientedHost/UI/FormExtensions.cs
Normal file
391
WinFormsComponentOrientedHost/UI/FormExtensions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
135
WinFormsComponentOrientedHost/UI/FormMain.cs
Normal file
135
WinFormsComponentOrientedHost/UI/FormMain.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
7
WinFormsComponentOrientedHost/appsettings.json
Normal file
7
WinFormsComponentOrientedHost/appsettings.json
Normal 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;"
|
||||
}
|
||||
}
|
||||
37
WinFormsControlLibrary1/ComboSingleAddControl.Designer.cs
generated
Normal file
37
WinFormsControlLibrary1/ComboSingleAddControl.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
51
WinFormsControlLibrary1/ComboSingleAddControl.cs
Normal file
51
WinFormsControlLibrary1/ComboSingleAddControl.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
120
WinFormsControlLibrary1/ComboSingleAddControl.resx
Normal file
120
WinFormsControlLibrary1/ComboSingleAddControl.resx
Normal 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>
|
||||
37
WinFormsControlLibrary1/DateByPatternTextBox.Designer.cs
generated
Normal file
37
WinFormsControlLibrary1/DateByPatternTextBox.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
79
WinFormsControlLibrary1/DateByPatternTextBox.cs
Normal file
79
WinFormsControlLibrary1/DateByPatternTextBox.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
120
WinFormsControlLibrary1/DateByPatternTextBox.resx
Normal file
120
WinFormsControlLibrary1/DateByPatternTextBox.resx
Normal 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>
|
||||
@@ -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) { }
|
||||
}
|
||||
@@ -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) { }
|
||||
}
|
||||
12
WinFormsControlLibrary1/Exceptions/TemplateParseException.cs
Normal file
12
WinFormsControlLibrary1/Exceptions/TemplateParseException.cs
Normal 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) { }
|
||||
}
|
||||
37
WinFormsControlLibrary1/TemplatedListBox.Designer.cs
generated
Normal file
37
WinFormsControlLibrary1/TemplatedListBox.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
169
WinFormsControlLibrary1/TemplatedListBox.cs
Normal file
169
WinFormsControlLibrary1/TemplatedListBox.cs
Normal 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("В шаблоне не должно идти два свойства подряд без текста между ними.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
120
WinFormsControlLibrary1/TemplatedListBox.resx
Normal file
120
WinFormsControlLibrary1/TemplatedListBox.resx
Normal 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>
|
||||
38
WinFormsControlLibrary1/UserControl1.Designer.cs
generated
38
WinFormsControlLibrary1/UserControl1.Designer.cs
generated
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace WinFormsControlLibrary1
|
||||
{
|
||||
public partial class UserControl1 : UserControl
|
||||
{
|
||||
public UserControl1()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
73
WinFormsControlLibrary1/WinFormsControlLibrary1.sln
Normal file
73
WinFormsControlLibrary1/WinFormsControlLibrary1.sln
Normal 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
|
||||
Reference in New Issue
Block a user