7 Commits
main ... Lab2

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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.0</Version>
<Authors>Romtec</Authors>
<Product>Component Oriented Platform Contract</Product>
<Description>Common abstractions for component-oriented WinForms host and plugins.</Description>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
</Project>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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.0" />
<PackageReference Include="NikitaKOP" Version="1.0.2" />
<PackageReference Include="TimKOP" Version="1.0.2" />
<PackageReference Include="WinFormsControlLibrary1" Version="1.0.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WinFormsComponentOrientedHost\WinFormsComponentOrientedHost.csproj" />
</ItemGroup>
</Project>

View File

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

View File

@@ -0,0 +1,187 @@
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);
}
}
private TreeView GetTree()
{
var f = typeof(HierarchicalTreeView).GetField("treeView", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
var tv = f?.GetValue(_tree) as TreeView ?? throw new InvalidOperationException("Не удалось получить TreeView из HierarchicalTreeView.");
return tv;
}
}

View File

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

View File

@@ -0,0 +1,59 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using WinFormsComponentOrientedHost.Data;
using WinFormsComponentOrientedHost.Data.Entities;
namespace WinFormsComponentOrientedHost.Auth;
public interface IAuthService
{
Task<User?> AuthenticateAsync(string login, string password);
Task<User> RegisterAsync(string login, string password, int accessLevel = 0);
}
public sealed class AuthService : IAuthService
{
private readonly IAppDbContextFactory _factory;
public AuthService(IAppDbContextFactory factory) => _factory = factory;
public async Task<User?> AuthenticateAsync(string login, string password)
{
var hash = Sha256(password);
await using var db = _factory.CreateDbContext();
return await db.Users.FirstOrDefaultAsync(u => u.login == login && u.passwordHash == hash);
}
public async Task<User> RegisterAsync(string login, string password, int accessLevel = 0)
{
var hash = Sha256(password);
await using var db = _factory.CreateDbContext();
var exists = await db.Users.AnyAsync(u => u.login == login);
if (exists) throw new InvalidOperationException("Пользователь с таким логином уже существует.");
var user = new User
{
id = Guid.NewGuid(),
login = login,
passwordHash = hash,
accessLevel = accessLevel,
createdAt = DateTime.UtcNow
};
db.Users.Add(user);
await db.SaveChangesAsync();
return user;
}
private static string Sha256(string input)
{
using var sha = SHA256.Create();
var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(input ?? string.Empty));
return Convert.ToHexString(bytes);
}
}

View File

@@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WinFormsComponentOrientedHost.Data.Entities;
namespace WinFormsComponentOrientedHost.Auth;
public sealed class LoginForm : Form
{
private readonly IAuthService _auth;
private readonly TextBox _tbLogin = new() { Dock = DockStyle.Fill };
private readonly TextBox _tbPass = new() { Dock = DockStyle.Fill, UseSystemPasswordChar = true };
private readonly Button _btnLogin = new() { Text = "Войти", AutoSize = true };
private readonly Button _btnReg = new() { Text = "Регистрация", AutoSize = true };
private readonly Button _btnCancel = new() { Text = "Отмена", AutoSize = true, DialogResult = DialogResult.Cancel };
public User? AuthenticatedUser { get; private set; }
public LoginForm(IAuthService auth)
{
_auth = auth;
Text = "Вход";
StartPosition = FormStartPosition.CenterScreen;
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = MinimizeBox = false;
Width = 400; Height = 200;
var table = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 2, RowCount = 3, Padding = new Padding(10) };
table.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 120));
table.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
table.Controls.Add(new Label { Text = "Логин:", AutoSize = true }, 0, 0);
table.Controls.Add(_tbLogin, 1, 0);
table.Controls.Add(new Label { Text = "Пароль:", AutoSize = true }, 0, 1);
table.Controls.Add(_tbPass, 1, 1);
var buttons = new FlowLayoutPanel { FlowDirection = FlowDirection.RightToLeft, Dock = DockStyle.Fill, AutoSize = true };
buttons.Controls.Add(_btnLogin);
buttons.Controls.Add(_btnReg);
buttons.Controls.Add(_btnCancel);
table.Controls.Add(buttons, 0, 2);
table.SetColumnSpan(buttons, 2);
Controls.Add(table);
AcceptButton = _btnLogin;
CancelButton = _btnCancel;
_btnLogin.Click += async (_, __) =>
{
try
{
var user = await _auth.AuthenticateAsync(_tbLogin.Text.Trim(), _tbPass.Text);
if (user == null)
{
MessageBox.Show(this, "Неверный логин или пароль.", "Вход", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
AuthenticatedUser = user;
DialogResult = DialogResult.OK;
}
catch (Exception ex)
{
MessageBox.Show(this, "Ошибка входа: " + ex.Message, "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
};
_btnReg.Click += async (_, __) =>
{
using var reg = new RegistrationForm(_auth);
if (reg.ShowDialog(this) == DialogResult.OK && !string.IsNullOrWhiteSpace(reg.RegisteredLogin))
{
_tbLogin.Text = reg.RegisteredLogin;
_tbPass.Focus();
}
};
}
}

View File

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

View File

@@ -0,0 +1,113 @@
using System;
using System.Windows.Forms;
using WinFormsComponentOrientedHost.Auth;
namespace WinFormsComponentOrientedHost.Auth
{
public sealed class RegistrationForm : Form
{
private readonly IAuthService _auth;
private readonly TextBox _tbLogin = new() { Dock = DockStyle.Fill };
private readonly TextBox _tbPass1 = new() { Dock = DockStyle.Fill, UseSystemPasswordChar = true };
private readonly TextBox _tbPass2 = new() { Dock = DockStyle.Fill, UseSystemPasswordChar = true };
private readonly ComboBox _cbLevel = new() { Dock = DockStyle.Fill, DropDownStyle = ComboBoxStyle.DropDownList };
private readonly Button _btnOk = new() { Text = "Зарегистрировать", AutoSize = true, DialogResult = DialogResult.OK };
private readonly Button _btnCancel = new() { Text = "Отмена", AutoSize = true, DialogResult = DialogResult.Cancel };
public string? RegisteredLogin { get; private set; }
public RegistrationForm(IAuthService auth)
{
_auth = auth;
Text = "Регистрация";
StartPosition = FormStartPosition.CenterParent;
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = MinimizeBox = false;
Width = 420; Height = 260;
_cbLevel.Items.AddRange(new object[] {
new LevelItem("Minimal (0) — только базовые справочники", 0),
new LevelItem("Basic (1) — + продукты", 1),
new LevelItem("Advanced (2) — + отчёты", 2)
});
_cbLevel.SelectedIndex = 1; // по умолчанию Basic
var table = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 2, RowCount = 5, Padding = new Padding(10) };
table.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 150));
table.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
table.Controls.Add(new Label { Text = "Логин:", AutoSize = true }, 0, 0);
table.Controls.Add(_tbLogin, 1, 0);
table.Controls.Add(new Label { Text = "Пароль:", AutoSize = true }, 0, 1);
table.Controls.Add(_tbPass1, 1, 1);
table.Controls.Add(new Label { Text = "Повтор пароля:", AutoSize = true }, 0, 2);
table.Controls.Add(_tbPass2, 1, 2);
table.Controls.Add(new Label { Text = "Уровень доступа:", AutoSize = true }, 0, 3);
table.Controls.Add(_cbLevel, 1, 3);
var buttons = new FlowLayoutPanel { Dock = DockStyle.Fill, FlowDirection = FlowDirection.RightToLeft, AutoSize = true };
buttons.Controls.Add(_btnOk);
buttons.Controls.Add(_btnCancel);
table.Controls.Add(buttons, 0, 4);
table.SetColumnSpan(buttons, 2);
Controls.Add(table);
AcceptButton = _btnOk;
CancelButton = _btnCancel;
_btnOk.Click += async (_, __) =>
{
var login = (_tbLogin.Text ?? "").Trim();
var p1 = _tbPass1.Text ?? "";
var p2 = _tbPass2.Text ?? "";
var level = ((_cbLevel.SelectedItem as LevelItem)?.Value) ?? 0;
// Валидация
if (string.IsNullOrWhiteSpace(login))
{
MessageBox.Show("Укажите логин.", "Регистрация", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
if (p1.Length < 4)
{
MessageBox.Show("Пароль должен быть не короче 4 символов.", "Регистрация", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
if (p1 != p2)
{
MessageBox.Show("Пароли не совпадают.", "Регистрация", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
try
{
_btnOk.Enabled = false; _btnCancel.Enabled = false; Cursor = Cursors.WaitCursor;
await _auth.RegisterAsync(login, p1, level);
RegisteredLogin = login;
this.DialogResult = DialogResult.OK;
this.Close();
}
catch (Exception ex)
{
MessageBox.Show($"Ошибка регистрации: {ex.Message}", "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
Cursor = Cursors.Default; _btnOk.Enabled = true; _btnCancel.Enabled = true;
}
};
}
private sealed record LevelItem(string Text, int Value)
{
public override string ToString() => Text;
}
}
}

View File

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

View File

@@ -0,0 +1,31 @@
using ComponentOrientedPlatform.Abstractions;
using ComponentOrientedPlatform.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WinFormsComponentOrientedHost.Auth;
public sealed class RuntimeLicenseProvider : ILicenseProvider
{
public AccessLevel CurrentLevel { get; }
public DateTime? Expires => null;
public string Owner => _owner;
public bool IsExpired => throw new NotImplementedException();
private readonly string _owner;
public RuntimeLicenseProvider(int accessLevel, string owner)
{
CurrentLevel = accessLevel switch
{
<= 0 => AccessLevel.Minimal,
1 => AccessLevel.Basic,
_ => AccessLevel.Advanced
};
_owner = owner;
}
}

View File

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

View File

@@ -0,0 +1,63 @@
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>();
public DbSet<User> Users => Set<User>();
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);
});
modelBuilder.Entity<User>(b =>
{
b.ToTable("users", "public");
b.HasKey(x => x.id);
b.Property(x => x.login).IsRequired().HasMaxLength(100);
b.HasIndex(x => x.login).IsUnique();
b.Property(x => x.passwordHash).IsRequired().HasColumnName("password_hash");
b.Property(x => x.accessLevel).IsRequired().HasColumnName("access_level");
b.Property(x => x.createdAt).HasColumnName("created_at");
});
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WinFormsComponentOrientedHost.Data.Entities;
public sealed class User
{
public Guid id { get; set; }
public string login { get; set; } = string.Empty;
public string passwordHash { get; set; } = string.Empty;
public int accessLevel { get; set; }
public DateTime createdAt { get; set; } = DateTime.UtcNow;
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,74 @@
using System;
using System.Windows.Forms;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using ComponentOrientedPlatform.Abstractions; // ILicenseProvider
using WinFormsComponentOrientedHost.Composition; // ReflectionLoader
using WinFormsComponentOrientedHost.Infrastructure; // SimpleLogger, HostServicesImpl
using WinFormsComponentOrientedHost.Licensing; // (<28><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>)
using WinFormsComponentOrientedHost.UI; // FormMain
using WinFormsComponentOrientedHost.Data; // AppDbContextFactory
using WinFormsComponentOrientedHost.Auth; // AuthService, LoginForm, RuntimeLicenseProvider
using WinFormsComponentOrientedHost.Data.Entities; // User (<28><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>!)
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>
var builder = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false);
Configuration = builder.Build();
var logger = new SimpleLogger();
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> DbContext
var cs = Configuration["Database:ConnectionString"] ?? throw new Exception("No DB connection string");
var dbFactory = new WinFormsComponentOrientedHost.Data.AppDbContextFactory(cs);
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> (<28><><EFBFBD> EnsureCreated)
try
{
using var db = dbFactory.CreateDbContext();
db.Database.Migrate();
}
catch (Exception ex)
{
logger.Error("DB migrate failed", ex);
}
// === <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> ===
var authService = new AuthService(dbFactory);
using var loginForm = new LoginForm(authService);
if (loginForm.ShowDialog() != DialogResult.OK)
return;
User user = loginForm.AuthenticatedUser!;
var runtimeLicense = new RuntimeLicenseProvider(user.accessLevel, owner: user.login);
// HostServices c <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
var host = new HostServicesImpl(runtimeLicense, logger);
host.Register<WinFormsComponentOrientedHost.Data.IAppDbContextFactory>(dbFactory);
// <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); // <20><><EFBFBD><EFBFBD> <20> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> host <20> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> (pluginsPath, logger)
// <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>
Application.Run(new FormMain(Configuration, host, loader));
}
}
}

View File

@@ -0,0 +1,130 @@
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 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 });
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;
}
private void Initialize()
{
_components = _loader.LoadAll();
var allowed = _components
.Where(c => (int)c.Metadata.AccessRequired <= (int)_host.License.CurrentLevel)
.ToList();
BuildMenu(allowed);
}
private void BuildMenu(IReadOnlyList<IComponentContract> comps)
{
_miDirectories.DropDownItems.Clear();
_miReports.DropDownItems.Clear();
foreach (var c in comps.OrderBy(c => c.Metadata.Title))
{
var item = new ToolStripMenuItem(c.Metadata.Title);
item.Tag = c;
item.Click += (_, __) => OpenComponent(c);
if (c.Metadata.MenuGroup == ComponentMenuGroup.Directories)
_miDirectories.DropDownItems.Add(item);
else
_miReports.DropDownItems.Add(item);
}
}
private void OpenComponent(IComponentContract comp)
{
var ctrl = comp.CreateControl(_host);
ctrl.Dock = DockStyle.Fill;
var page = new TabPage(comp.Metadata.Title) { Tag = comp };
page.Controls.Add(ctrl);
_tabs.TabPages.Add(page);
_tabs.SelectedTab = page;
}
private void Tabs_MouseDoubleClick(object? sender, MouseEventArgs e)
{
for (int i = 0; i < _tabs.TabPages.Count; i++)
{
if (_tabs.GetTabRect(i).Contains(e.Location))
{
_tabs.TabPages.RemoveAt(i);
break;
}
}
}
private void FormMain_KeyDown(object? sender, KeyEventArgs e)
{
if (_tabs.SelectedTab == null) return;
if (!(e.Control && (e.KeyCode is Keys.A or Keys.U or Keys.D))) return;
var ctrl = _tabs.SelectedTab.Controls.Count > 0 ? _tabs.SelectedTab.Controls[0] : null;
if (ctrl == null) return;
var methodName = e.KeyCode switch
{
Keys.A => "CreateNew",
Keys.U => "EditSelected",
Keys.D => "DeleteSelected",
_ => null
};
if (methodName == null) return;
var mi = ctrl.GetType().GetMethod(methodName, Type.EmptyTypes);
if (mi != null)
{
try { mi.Invoke(ctrl, null); }
catch (Exception ex) { _host.Logger.Error($"Hotkey {methodName} error", ex); }
e.Handled = true;
}
}
}

View File

@@ -0,0 +1,30 @@
<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.0" />
<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" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,49 @@

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
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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {98BBBD7D-7A95-4216-927E-F47C3D82C695}
EndGlobalSection
EndGlobal