Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 63275bd2fa | |||
| c0f6d17928 | |||
| 34d0d1aea6 | |||
| 1fefae50df | |||
| de3cb0a461 | |||
| 3b5d539290 | |||
| a372eeee57 |
14
ComponentOrientedPlatform/Abstractions/IAppLogger.cs
Normal file
14
ComponentOrientedPlatform/Abstractions/IAppLogger.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ComponentOrientedPlatform.Abstractions;
|
||||
|
||||
public interface IAppLogger
|
||||
{
|
||||
void Info(string message);
|
||||
void Warn(string message);
|
||||
void Error(string message, Exception? ex = null);
|
||||
}
|
||||
14
ComponentOrientedPlatform/Abstractions/IComponentContract.cs
Normal file
14
ComponentOrientedPlatform/Abstractions/IComponentContract.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace ComponentOrientedPlatform.Abstractions;
|
||||
|
||||
public interface IComponentContract
|
||||
{
|
||||
IComponentMetadata Metadata { get; }
|
||||
UserControl CreateControl(IHostServices host);
|
||||
}
|
||||
16
ComponentOrientedPlatform/Abstractions/IComponentMetadata.cs
Normal file
16
ComponentOrientedPlatform/Abstractions/IComponentMetadata.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using ComponentOrientedPlatform.Model;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ComponentOrientedPlatform.Abstractions;
|
||||
|
||||
public interface IComponentMetadata
|
||||
{
|
||||
string Id { get; }
|
||||
string Title { get; }
|
||||
ComponentMenuGroup MenuGroup { get; }
|
||||
AccessLevel AccessRequired { get; }
|
||||
}
|
||||
17
ComponentOrientedPlatform/Abstractions/IHostServices.cs
Normal file
17
ComponentOrientedPlatform/Abstractions/IHostServices.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ComponentOrientedPlatform.Abstractions;
|
||||
|
||||
public interface IHostServices
|
||||
{
|
||||
ILicenseProvider License { get; }
|
||||
|
||||
IAppLogger Logger { get; }
|
||||
|
||||
T? GetService<T>() where T : class;
|
||||
}
|
||||
15
ComponentOrientedPlatform/Abstractions/ILicenseProvider.cs
Normal file
15
ComponentOrientedPlatform/Abstractions/ILicenseProvider.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using ComponentOrientedPlatform.Model;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ComponentOrientedPlatform.Abstractions;
|
||||
|
||||
public interface ILicenseProvider
|
||||
{
|
||||
AccessLevel CurrentLevel { get; }
|
||||
bool IsExpired { get; }
|
||||
string? Owner { get; }
|
||||
}
|
||||
18
ComponentOrientedPlatform/ComponentOrientedPlatform.csproj
Normal file
18
ComponentOrientedPlatform/ComponentOrientedPlatform.csproj
Normal file
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows7.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<PackageId>ComponentOrientedPlatform.Contract</PackageId>
|
||||
<Version>1.0.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>
|
||||
14
ComponentOrientedPlatform/Model/AccessLevel.cs
Normal file
14
ComponentOrientedPlatform/Model/AccessLevel.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ComponentOrientedPlatform.Model;
|
||||
|
||||
public enum AccessLevel
|
||||
{
|
||||
Minimal = 0,
|
||||
Basic = 1,
|
||||
Advanced = 2
|
||||
}
|
||||
13
ComponentOrientedPlatform/Model/ComponentMenuGroup.cs
Normal file
13
ComponentOrientedPlatform/Model/ComponentMenuGroup.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ComponentOrientedPlatform.Model;
|
||||
|
||||
public enum ComponentMenuGroup
|
||||
{
|
||||
Directories = 0,
|
||||
Reports = 1
|
||||
}
|
||||
24
ComponentOrientedPlatform/Model/ComponentMetadata.cs
Normal file
24
ComponentOrientedPlatform/Model/ComponentMetadata.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using ComponentOrientedPlatform.Abstractions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ComponentOrientedPlatform.Model;
|
||||
|
||||
public sealed class ComponentMetadata : IComponentMetadata
|
||||
{
|
||||
public string Id { get; }
|
||||
public string Title { get; }
|
||||
public ComponentMenuGroup MenuGroup { get; }
|
||||
public AccessLevel AccessRequired { get; }
|
||||
|
||||
public ComponentMetadata(string id, string title, ComponentMenuGroup group, AccessLevel level)
|
||||
{
|
||||
Id = id ?? throw new ArgumentNullException(nameof(id));
|
||||
Title = title ?? throw new ArgumentNullException(nameof(title));
|
||||
MenuGroup = group;
|
||||
AccessRequired = level;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using ComponentOrientedPlatform.Abstractions;
|
||||
using ComponentOrientedPlatform.Model;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Directories.Categories.Component;
|
||||
|
||||
public sealed class CategoriesDirectoryComponent : IComponentContract
|
||||
{
|
||||
private static readonly IComponentMetadata _meta =
|
||||
new ComponentMetadata("directories.categories", "Категории",
|
||||
ComponentMenuGroup.Directories, AccessLevel.Minimal);
|
||||
|
||||
public IComponentMetadata Metadata => _meta;
|
||||
public UserControl CreateControl(IHostServices host) => new UI.CategoriesDirectoryControl(host);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ComponentOrientedPlatform.Contract" Version="1.0.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>
|
||||
@@ -0,0 +1,24 @@
|
||||
using ComponentOrientedPlatform.Abstractions;
|
||||
using ComponentOrientedPlatform.Model;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Directories.Categories.Component;
|
||||
|
||||
public sealed class ProductsDirectoryComponent : IComponentContract
|
||||
{
|
||||
private static readonly IComponentMetadata _meta =
|
||||
new ComponentMetadata(
|
||||
id: "directories.products",
|
||||
title: "Продукты",
|
||||
group: ComponentMenuGroup.Directories,
|
||||
level: AccessLevel.Basic);
|
||||
|
||||
public IComponentMetadata Metadata => _meta;
|
||||
|
||||
public UserControl CreateControl(IHostServices host)
|
||||
=> new UI.ProductsDirectoryControl(host);
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using ComponentOrientedPlatform.Abstractions; // IHostServices
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using WinFormsComponentOrientedHost.Data; // IAppDbContextFactory
|
||||
using WinFormsComponentOrientedHost.Data.Entities; // Category
|
||||
|
||||
namespace Directories.Categories.Component.UI
|
||||
{
|
||||
public sealed class CategoriesDirectoryControl : UserControl
|
||||
{
|
||||
private readonly IHostServices _host;
|
||||
private readonly IAppDbContextFactory _factory;
|
||||
|
||||
private readonly BindingList<Category> _data = new();
|
||||
private readonly DataGridView _grid = new()
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
AutoGenerateColumns = false,
|
||||
AllowUserToAddRows = false,
|
||||
EditMode = DataGridViewEditMode.EditOnEnter,
|
||||
SelectionMode = DataGridViewSelectionMode.FullRowSelect
|
||||
};
|
||||
|
||||
public CategoriesDirectoryControl(IHostServices host)
|
||||
{
|
||||
_host = host;
|
||||
_factory = host.GetService<IAppDbContextFactory>()
|
||||
?? throw new InvalidOperationException("Db factory not registered.");
|
||||
|
||||
_grid.Columns.Add(new DataGridViewTextBoxColumn
|
||||
{
|
||||
DataPropertyName = nameof(Category.name), // нижний регистр свойств
|
||||
HeaderText = "Категория",
|
||||
AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill
|
||||
});
|
||||
_grid.DataSource = _data;
|
||||
Controls.Add(_grid);
|
||||
|
||||
Load += async (_, __) => await ReloadAsync();
|
||||
|
||||
// хоткеи
|
||||
_grid.KeyDown += Grid_KeyDown;
|
||||
_grid.EditingControlShowing += (s, e) =>
|
||||
{
|
||||
e.Control.KeyDown -= EditingControl_KeyDown;
|
||||
e.Control.KeyDown += EditingControl_KeyDown;
|
||||
};
|
||||
|
||||
// запрет пустого
|
||||
_grid.CellValidating += Grid_CellValidating;
|
||||
|
||||
|
||||
_grid.CurrentCellDirtyStateChanged += (_, __) =>
|
||||
{
|
||||
if (_grid.IsCurrentCellDirty)
|
||||
_grid.CommitEdit(DataGridViewDataErrorContexts.Commit);
|
||||
};
|
||||
|
||||
_grid.RowValidated += async (_, e) =>
|
||||
{
|
||||
// e.RowIndex гарантированно валиден и биндинг уже обновлён
|
||||
await SaveRowAsync(e.RowIndex);
|
||||
};
|
||||
|
||||
// чтобы не падать на биндинговых ошибках во время редактирования
|
||||
_grid.DataError += (s, e) => e.ThrowException = false;
|
||||
}
|
||||
|
||||
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
|
||||
{
|
||||
if (keyData == Keys.Insert) { CreateNew(); return true; }
|
||||
if (keyData == Keys.Delete) { _ = DeleteSelectedAsync(); return true; }
|
||||
return base.ProcessCmdKey(ref msg, keyData);
|
||||
}
|
||||
|
||||
// ---- Публичные действия (для Ctrl+A/U/D из хоста)
|
||||
public void CreateNew()
|
||||
{
|
||||
var item = new Category { name = "" };
|
||||
_data.Add(item);
|
||||
var idx = _data.IndexOf(item);
|
||||
_grid.CurrentCell = _grid.Rows[idx].Cells[0];
|
||||
_grid.BeginEdit(true);
|
||||
}
|
||||
public void EditSelected() { if (_grid.CurrentCell != null) _grid.BeginEdit(true); }
|
||||
public async void DeleteSelected() => await DeleteSelectedAsync();
|
||||
|
||||
// ---- Загрузка
|
||||
public async Task ReloadAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var db = _factory.CreateDbContext();
|
||||
var list = await db.Categories.AsNoTracking().OrderBy(x => x.name).ToListAsync();
|
||||
_data.Clear(); foreach (var c in list) _data.Add(c);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_host.Logger.Error("Categories reload error", ex);
|
||||
MessageBox.Show("Ошибка загрузки категорий.", "Ошибка",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task SaveRowAsync(int rowIndex)
|
||||
{
|
||||
if (rowIndex < 0 || rowIndex >= _data.Count) return;
|
||||
|
||||
// На всякий случай убедимся, что грид закоммитил правки
|
||||
if (_grid.IsCurrentCellInEditMode)
|
||||
_grid.EndEdit(DataGridViewDataErrorContexts.Commit);
|
||||
|
||||
var item = _data[rowIndex];
|
||||
if (item == null) return;
|
||||
|
||||
// Пустые не сохраняем
|
||||
if (string.IsNullOrWhiteSpace(item.name)) return;
|
||||
|
||||
try
|
||||
{
|
||||
await using var db = _factory.CreateDbContext();
|
||||
|
||||
// уникальность
|
||||
bool exists = await db.Categories.AnyAsync(x => x.name == item.name && x.id != item.id);
|
||||
if (exists)
|
||||
{
|
||||
MessageBox.Show("Такая категория уже существует.", "Валидация",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
// Без ReloadAsync: просто подсветим ошибку
|
||||
var row = _grid.Rows[rowIndex];
|
||||
row.ErrorText = "Дубликат названия";
|
||||
return;
|
||||
}
|
||||
|
||||
var tracked = await db.Categories.FirstOrDefaultAsync(x => x.id == item.id);
|
||||
if (tracked is null)
|
||||
{
|
||||
if (item.id == Guid.Empty) item.id = Guid.NewGuid();
|
||||
db.Categories.Add(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
tracked.name = item.name;
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// Локально обновим строку/ошибку; перезагрузка не нужна
|
||||
var r = _grid.Rows[rowIndex];
|
||||
r.ErrorText = string.Empty;
|
||||
_grid.InvalidateRow(rowIndex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_host.Logger.Error("Save category error", ex);
|
||||
MessageBox.Show("Ошибка сохранения.", "Ошибка",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private async Task DeleteSelectedAsync()
|
||||
{
|
||||
if (_grid.CurrentRow?.DataBoundItem is not Category c) return;
|
||||
|
||||
if (MessageBox.Show("Удалить выбранную категорию?",
|
||||
"Подтверждение", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
await using var db = _factory.CreateDbContext();
|
||||
var tracked = await db.Categories.FirstOrDefaultAsync(x => x.id == c.id);
|
||||
if (tracked != null)
|
||||
{
|
||||
db.Categories.Remove(tracked);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
_data.Remove(c);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_host.Logger.Error("Delete category error", ex);
|
||||
MessageBox.Show("Ошибка удаления.", "Ошибка",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Вспомогательные обработчики
|
||||
private void Grid_KeyDown(object? s, KeyEventArgs e)
|
||||
{
|
||||
if (e.KeyCode == Keys.Insert) { CreateNew(); e.Handled = true; }
|
||||
else if (e.KeyCode == Keys.Delete) { _ = DeleteSelectedAsync(); e.Handled = true; }
|
||||
}
|
||||
|
||||
private void EditingControl_KeyDown(object? sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.KeyCode == Keys.Insert) { CreateNew(); e.Handled = true; }
|
||||
else if (e.KeyCode == Keys.Delete) { _ = DeleteSelectedAsync(); e.Handled = true; }
|
||||
}
|
||||
|
||||
private void Grid_CellValidating(object? s, DataGridViewCellValidatingEventArgs e)
|
||||
{
|
||||
if (e.ColumnIndex != 0) return;
|
||||
var text = (e.FormattedValue?.ToString() ?? "").Trim();
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
_grid.Rows[e.RowIndex].ErrorText = "Название не может быть пустым";
|
||||
e.Cancel = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_grid.Rows[e.RowIndex].ErrorText = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
167
Directories.Categories.Component/UI/ProductEditForm.cs
Normal file
167
Directories.Categories.Component/UI/ProductEditForm.cs
Normal file
@@ -0,0 +1,167 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using ComponentOrientedPlatform.Abstractions; // если у тебя Contract.Abstractions — замени этот using
|
||||
using WinFormsComponentOrientedHost.Data.Entities; // Product
|
||||
using WinFormsControlLibrary1; // ComboSingleAddControl
|
||||
using KopComponent; // InputComponent
|
||||
|
||||
namespace Directories.Categories.Component.UI
|
||||
{
|
||||
public sealed class ProductEditForm : Form
|
||||
{
|
||||
private readonly IHostServices _host;
|
||||
private readonly Product _model;
|
||||
private readonly List<string> _categories;
|
||||
|
||||
private readonly TextBox _tbName = new() { Dock = DockStyle.Fill };
|
||||
private readonly TextBox _tbDesc = new() { Dock = DockStyle.Fill, Multiline = true, Height = 80 };
|
||||
private readonly ComboSingleAddControl _cbCategory = new() { Dock = DockStyle.Fill };
|
||||
private readonly InputComponent _nbQuantity = new() { Dock = DockStyle.Fill };
|
||||
|
||||
private readonly Button _btnOk = new() { Text = "Сохранить", DialogResult = DialogResult.OK, AutoSize = true };
|
||||
private readonly Button _btnCancel = new() { Text = "Отмена", DialogResult = DialogResult.Cancel, AutoSize = true };
|
||||
|
||||
private bool _dirty = false;
|
||||
private bool _initializing = true;
|
||||
|
||||
public ProductEditForm(IHostServices host, IEnumerable<string> categories, Product model)
|
||||
{
|
||||
_host = host;
|
||||
_model = model;
|
||||
_categories = categories.Distinct().OrderBy(s => s).ToList();
|
||||
|
||||
Text = (_model.id == default) ? "Создание продукта" : "Редактирование продукта";
|
||||
StartPosition = FormStartPosition.CenterParent;
|
||||
Width = 640;
|
||||
Height = 420;
|
||||
FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
|
||||
// --- Таблица полей ---
|
||||
var table = new TableLayoutPanel
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
ColumnCount = 2,
|
||||
RowCount = 4,
|
||||
Padding = new Padding(10)
|
||||
};
|
||||
table.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 140));
|
||||
table.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
||||
for (int i = 0; i < 4; i++) table.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
|
||||
table.Controls.Add(new Label { Text = "Название:", AutoSize = true }, 0, 0);
|
||||
table.Controls.Add(_tbName, 1, 0);
|
||||
|
||||
table.Controls.Add(new Label { Text = "Описание:", AutoSize = true }, 0, 1);
|
||||
table.Controls.Add(_tbDesc, 1, 1);
|
||||
|
||||
table.Controls.Add(new Label { Text = "Категория:", AutoSize = true }, 0, 2);
|
||||
table.Controls.Add(_cbCategory, 1, 2);
|
||||
|
||||
table.Controls.Add(new Label { Text = "Количество:", AutoSize = true }, 0, 3);
|
||||
table.Controls.Add(_nbQuantity, 1, 3);
|
||||
|
||||
// --- Нижняя панель с кнопками ---
|
||||
var bottomBar = new FlowLayoutPanel
|
||||
{
|
||||
Dock = DockStyle.Bottom,
|
||||
FlowDirection = FlowDirection.RightToLeft,
|
||||
Padding = new Padding(10),
|
||||
AutoSize = true,
|
||||
WrapContents = false
|
||||
};
|
||||
bottomBar.Controls.Add(_btnCancel);
|
||||
bottomBar.Controls.Add(_btnOk);
|
||||
|
||||
// ВАЖНО: сначала добавляем table (Fill), затем bottomBar (Bottom)
|
||||
Controls.Add(table);
|
||||
Controls.Add(bottomBar);
|
||||
|
||||
// Делает Enter=ОК, Esc=Отмена
|
||||
this.AcceptButton = _btnOk;
|
||||
this.CancelButton = _btnCancel;
|
||||
|
||||
// --- Данные в контролы ---
|
||||
_cbCategory.ClearItems();
|
||||
foreach (var c in _categories) _cbCategory.AddValue(c);
|
||||
|
||||
_tbName.Text = _model.name ?? string.Empty;
|
||||
_tbDesc.Text = _model.description ?? string.Empty;
|
||||
|
||||
_tbName.TextChanged += OnAnyValueChanged;
|
||||
_tbDesc.TextChanged += OnAnyValueChanged;
|
||||
_cbCategory.SelectedValueChanged += OnAnyValueChanged;
|
||||
|
||||
_initializing = false;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_model.categoryText))
|
||||
_cbCategory.SelectedValue = _model.categoryText;
|
||||
|
||||
_nbQuantity.TextValue = _model.quantity;
|
||||
|
||||
_btnOk.Click += (_, __) =>
|
||||
{
|
||||
var name = (_tbName.Text ?? "").Trim();
|
||||
var desc = string.IsNullOrWhiteSpace(_tbDesc.Text) ? null : _tbDesc.Text.Trim();
|
||||
var cat = _cbCategory.SelectedValue ?? "";
|
||||
var qty = _nbQuantity.TextValue;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
MessageBox.Show(this, "Название не может быть пустым.", "Валидация",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
this.DialogResult = DialogResult.None;
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(cat))
|
||||
{
|
||||
MessageBox.Show(this, "Нужно выбрать категорию.", "Валидация",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
this.DialogResult = DialogResult.None;
|
||||
return;
|
||||
}
|
||||
if (qty is < 0)
|
||||
{
|
||||
MessageBox.Show(this, "Количество не может быть отрицательным.", "Валидация",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
this.DialogResult = DialogResult.None;
|
||||
return;
|
||||
}
|
||||
|
||||
_model.name = name;
|
||||
_model.description = desc;
|
||||
_model.categoryText = cat;
|
||||
_model.quantity = qty;
|
||||
_dirty = false;
|
||||
};
|
||||
this.FormClosing += (s, e) =>
|
||||
{
|
||||
if (this.DialogResult == DialogResult.OK) return;
|
||||
|
||||
if (_dirty)
|
||||
{
|
||||
var ans = MessageBox.Show(
|
||||
this,
|
||||
"Есть несохранённые изменения. Закрыть без сохранения?",
|
||||
"Подтверждение",
|
||||
MessageBoxButtons.YesNo,
|
||||
MessageBoxIcon.Warning,
|
||||
MessageBoxDefaultButton.Button2);
|
||||
|
||||
if (ans == DialogResult.No)
|
||||
{
|
||||
e.Cancel = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
private void OnAnyValueChanged(object? sender, EventArgs e)
|
||||
{
|
||||
if (_initializing) return;
|
||||
_dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
120
Directories.Categories.Component/UI/ProductEditForm.resx
Normal file
120
Directories.Categories.Component/UI/ProductEditForm.resx
Normal file
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
214
Directories.Categories.Component/UI/ProductsDirectoryControl.cs
Normal file
214
Directories.Categories.Component/UI/ProductsDirectoryControl.cs
Normal file
@@ -0,0 +1,214 @@
|
||||
using ComponentOrientedPlatform.Abstractions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using WinFormsComponentOrientedHost.Data;
|
||||
using WinFormsComponentOrientedHost.Data.Entities;
|
||||
using WinFormsControlLibrary;
|
||||
|
||||
namespace Directories.Categories.Component.UI;
|
||||
|
||||
public sealed class ProductsDirectoryControl : UserControl
|
||||
{
|
||||
private readonly IHostServices _host;
|
||||
private readonly IAppDbContextFactory _factory;
|
||||
|
||||
private readonly ToolStrip _toolbar = new();
|
||||
private readonly ToolStripButton _btnAdd = new("Добавить");
|
||||
private readonly ToolStripButton _btnEdit = new("Изменить");
|
||||
private readonly ToolStripButton _btnDelete = new("Удалить");
|
||||
|
||||
private readonly ContextMenuStrip _ctx = new();
|
||||
private readonly ToolStripMenuItem _miAdd = new("Добавить");
|
||||
private readonly ToolStripMenuItem _miEdit = new("Изменить");
|
||||
private readonly ToolStripMenuItem _miDelete = new("Удалить");
|
||||
|
||||
private readonly HierarchicalTreeView _tree = new() { Dock = DockStyle.Fill };
|
||||
|
||||
public ProductsDirectoryControl(IHostServices host)
|
||||
{
|
||||
_host = host;
|
||||
_factory = host.GetService<IAppDbContextFactory>()
|
||||
?? throw new InvalidOperationException("IAppDbContextFactory is not registered.");
|
||||
|
||||
_toolbar.Items.AddRange(new ToolStripItem[] { _btnAdd, _btnEdit, _btnDelete });
|
||||
_btnAdd.Click += (_, __) => CreateNew();
|
||||
_btnEdit.Click += (_, __) => EditSelected();
|
||||
_btnDelete.Click += (_, __) => DeleteSelected();
|
||||
|
||||
_ctx.Items.AddRange(new ToolStripItem[] { _miAdd, _miEdit, _miDelete });
|
||||
_miAdd.Click += (_, __) => CreateNew();
|
||||
_miEdit.Click += (_, __) => EditSelected();
|
||||
_miDelete.Click += (_, __) => DeleteSelected();
|
||||
|
||||
_tree.ContextMenuStrip = _ctx;
|
||||
_tree.SetHierarchy("CategoryText", "Quantity", "Name", "Id");
|
||||
|
||||
var grid = new TableLayoutPanel { Dock = DockStyle.Fill, RowCount = 2 };
|
||||
grid.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
grid.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
|
||||
grid.Controls.Add(_toolbar, 0, 0);
|
||||
grid.Controls.Add(_tree, 0, 1);
|
||||
Controls.Add(grid);
|
||||
|
||||
Load += async (_, __) => await ReloadAsync();
|
||||
}
|
||||
|
||||
private async Task ReloadAsync()
|
||||
{
|
||||
await using var db = _factory.CreateDbContext();
|
||||
var list = await db.Products
|
||||
.AsNoTracking()
|
||||
.OrderBy(p => p.categoryText)
|
||||
.ThenBy(p => p.quantity)
|
||||
.ThenBy(p => p.name)
|
||||
.ToListAsync();
|
||||
|
||||
// Простой пересбор дерева: чистим и добавляем по одному
|
||||
_tree.SetHierarchy("CategoryText", "Quantity", "Name", "Id");
|
||||
foreach (var p in list)
|
||||
_tree.AddObject(p);
|
||||
}
|
||||
|
||||
public async void CreateNew()
|
||||
{
|
||||
await using var db = _factory.CreateDbContext();
|
||||
|
||||
// Подтянем список категорий для формы (Name)
|
||||
var categories = await db.Categories
|
||||
.AsNoTracking()
|
||||
.OrderBy(c => c.name)
|
||||
.Select(c => c.name)
|
||||
.ToListAsync();
|
||||
|
||||
var product = new Product();
|
||||
using var dlg = new ProductEditForm(_host, categories, product);
|
||||
if (dlg.ShowDialog(this) == DialogResult.OK)
|
||||
{
|
||||
// валидация
|
||||
if (string.IsNullOrWhiteSpace(product.name))
|
||||
{
|
||||
MessageBox.Show("Название не может быть пустым.", "Валидация",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(product.categoryText))
|
||||
{
|
||||
MessageBox.Show("Нужно выбрать категорию.", "Валидация",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
if (product.quantity is < 0)
|
||||
{
|
||||
MessageBox.Show("Количество не может быть отрицательным.", "Валидация",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
db.Products.Add(product);
|
||||
await db.SaveChangesAsync();
|
||||
await ReloadAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async void EditSelected()
|
||||
{
|
||||
// Получим выбранный продукт по узлу Id (последний уровень)
|
||||
var selected = _tree.GetSelectedObject<Product>();
|
||||
if (selected == null)
|
||||
{
|
||||
MessageBox.Show("Выберите конечный элемент (Id).", "Подсказка",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
await using var db = _factory.CreateDbContext();
|
||||
var tracked = await db.Products.FirstOrDefaultAsync(p => p.id == selected.id);
|
||||
if (tracked == null)
|
||||
{
|
||||
MessageBox.Show("Запись не найдена (могла быть удалена).", "Информация",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
await ReloadAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var categories = await db.Categories
|
||||
.AsNoTracking()
|
||||
.OrderBy(c => c.name)
|
||||
.Select(c => c.name)
|
||||
.ToListAsync();
|
||||
|
||||
// Редактируем копию, чтобы не затрагивать tracked до подтверждения
|
||||
var copy = new Product
|
||||
{
|
||||
id = tracked.id,
|
||||
name = tracked.name,
|
||||
description = tracked.description,
|
||||
categoryText = tracked.categoryText,
|
||||
quantity = tracked.quantity
|
||||
};
|
||||
|
||||
using var dlg = new ProductEditForm(_host, categories, copy);
|
||||
if (dlg.ShowDialog(this) == DialogResult.OK)
|
||||
{
|
||||
// валидация
|
||||
if (string.IsNullOrWhiteSpace(copy.name))
|
||||
{
|
||||
MessageBox.Show("Название не может быть пустым.", "Валидация",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(copy.categoryText))
|
||||
{
|
||||
MessageBox.Show("Нужно выбрать категорию.", "Валидация",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
if (copy.quantity is < 0)
|
||||
{
|
||||
MessageBox.Show("Количество не может быть отрицательным.", "Валидация",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
tracked.name = copy.name;
|
||||
tracked.description = copy.description;
|
||||
tracked.categoryText = copy.categoryText;
|
||||
tracked.quantity = copy.quantity;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
await ReloadAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async void DeleteSelected()
|
||||
{
|
||||
var selected = _tree.GetSelectedObject<Product>();
|
||||
if (selected == null)
|
||||
{
|
||||
MessageBox.Show("Выберите конечный элемент (Id).", "Подсказка",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
if (MessageBox.Show("Удалить выбранный продукт?", "Подтверждение",
|
||||
MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes)
|
||||
return;
|
||||
|
||||
await using var db = _factory.CreateDbContext();
|
||||
var tracked = await db.Products.FirstOrDefaultAsync(p => p.id == selected.id);
|
||||
if (tracked != null)
|
||||
{
|
||||
db.Products.Remove(tracked);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
await ReloadAsync();
|
||||
}
|
||||
|
||||
public void CreateNew(object? _ = null) => CreateNew();
|
||||
public void EditSelected(object? _ = null) => EditSelected();
|
||||
public void DeleteSelected(object? _ = null) => DeleteSelected();
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,24 @@
|
||||
using ComponentOrientedPlatform.Abstractions;
|
||||
using ComponentOrientedPlatform.Model;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Reports.ProductsByCategory.Component;
|
||||
|
||||
public sealed class ReportsProductsByCategoryComponent : IComponentContract
|
||||
{
|
||||
private static readonly IComponentMetadata _meta =
|
||||
new ComponentMetadata(
|
||||
id: "reports.productsByCategory",
|
||||
title: "Продукты по категориям",
|
||||
group: ComponentMenuGroup.Reports,
|
||||
level: AccessLevel.Advanced);
|
||||
|
||||
public IComponentMetadata Metadata => _meta;
|
||||
|
||||
public UserControl CreateControl(IHostServices host)
|
||||
=> new UI.ProductsByCategoryReportControl(host);
|
||||
}
|
||||
@@ -0,0 +1,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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
59
WinFormsComponentOrientedHost/Auth/AuthService.cs
Normal file
59
WinFormsComponentOrientedHost/Auth/AuthService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
82
WinFormsComponentOrientedHost/Auth/LoginForm.cs
Normal file
82
WinFormsComponentOrientedHost/Auth/LoginForm.cs
Normal 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();
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
120
WinFormsComponentOrientedHost/Auth/LoginForm.resx
Normal file
120
WinFormsComponentOrientedHost/Auth/LoginForm.resx
Normal file
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
113
WinFormsComponentOrientedHost/Auth/RegistrationForm.cs
Normal file
113
WinFormsComponentOrientedHost/Auth/RegistrationForm.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
120
WinFormsComponentOrientedHost/Auth/RegistrationForm.resx
Normal file
120
WinFormsComponentOrientedHost/Auth/RegistrationForm.resx
Normal file
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
31
WinFormsComponentOrientedHost/Auth/RuntimeLicenseProvider.cs
Normal file
31
WinFormsComponentOrientedHost/Auth/RuntimeLicenseProvider.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
63
WinFormsComponentOrientedHost/Data/AppDbContext.cs
Normal file
63
WinFormsComponentOrientedHost/Data/AppDbContext.cs
Normal 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");
|
||||
});
|
||||
}
|
||||
}
|
||||
18
WinFormsComponentOrientedHost/Data/AppDbContextFactory.cs
Normal file
18
WinFormsComponentOrientedHost/Data/AppDbContextFactory.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace WinFormsComponentOrientedHost.Data;
|
||||
|
||||
|
||||
public interface IAppDbContextFactory : IDbContextFactory<AppDbContext> { }
|
||||
|
||||
public sealed class AppDbContextFactory : IAppDbContextFactory
|
||||
{
|
||||
private readonly string _cs;
|
||||
public AppDbContextFactory(string connectionString) => _cs = connectionString;
|
||||
public AppDbContext CreateDbContext() => new AppDbContext(_cs);
|
||||
}
|
||||
7
WinFormsComponentOrientedHost/Data/Entities/Category.cs
Normal file
7
WinFormsComponentOrientedHost/Data/Entities/Category.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace WinFormsComponentOrientedHost.Data.Entities;
|
||||
|
||||
public sealed class Category
|
||||
{
|
||||
public Guid id { get; set; } = Guid.NewGuid();
|
||||
public string name { get; set; } = string.Empty;
|
||||
}
|
||||
10
WinFormsComponentOrientedHost/Data/Entities/Product.cs
Normal file
10
WinFormsComponentOrientedHost/Data/Entities/Product.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace WinFormsComponentOrientedHost.Data.Entities;
|
||||
|
||||
public sealed class Product
|
||||
{
|
||||
public Guid id { get; set; } = Guid.NewGuid();
|
||||
public string name { get; set; } = string.Empty;
|
||||
public string? description { get; set; }
|
||||
public string categoryText { get; set; } = string.Empty;
|
||||
public double? quantity { get; set; }
|
||||
}
|
||||
16
WinFormsComponentOrientedHost/Data/Entities/User.cs
Normal file
16
WinFormsComponentOrientedHost/Data/Entities/User.cs
Normal 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;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using ComponentOrientedPlatform.Abstractions;
|
||||
|
||||
namespace WinFormsComponentOrientedHost;
|
||||
|
||||
public sealed class HostServicesImpl : IHostServices
|
||||
{
|
||||
private readonly Dictionary<Type, object> _services = new();
|
||||
|
||||
public ILicenseProvider License { get; }
|
||||
public IAppLogger Logger { get; }
|
||||
|
||||
public HostServicesImpl(ILicenseProvider license, IAppLogger logger)
|
||||
{
|
||||
License = license;
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
public void Register<T>(T service) where T : class
|
||||
{
|
||||
_services[typeof(T)] = service;
|
||||
}
|
||||
|
||||
public T? GetService<T>() where T : class
|
||||
=> _services.TryGetValue(typeof(T), out var s) ? (T)s : null;
|
||||
}
|
||||
20
WinFormsComponentOrientedHost/Infrastructure/SimpleLogger.cs
Normal file
20
WinFormsComponentOrientedHost/Infrastructure/SimpleLogger.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using ComponentOrientedPlatform.Abstractions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace WinFormsComponentOrientedHost.Infrastructure;
|
||||
|
||||
public sealed class SimpleLogger : IAppLogger
|
||||
{
|
||||
public void Info(string message) => Debug.WriteLine($"[INFO] {message}");
|
||||
public void Warn(string message) => Debug.WriteLine($"[WARN] {message}");
|
||||
public void Error(string message, Exception? ex = null)
|
||||
{
|
||||
Debug.WriteLine($"[ERR ] {message}");
|
||||
if (ex != null) Debug.WriteLine(ex.ToString());
|
||||
}
|
||||
}
|
||||
65
WinFormsComponentOrientedHost/Licensing/LicenseProvider.cs
Normal file
65
WinFormsComponentOrientedHost/Licensing/LicenseProvider.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using ComponentOrientedPlatform.Abstractions;
|
||||
using ComponentOrientedPlatform.Model;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace WinFormsComponentOrientedHost.Licensing;
|
||||
|
||||
public sealed class LicenseProvider : ILicenseProvider
|
||||
{
|
||||
private readonly LicenseModel _model;
|
||||
|
||||
private LicenseProvider(LicenseModel model)
|
||||
{
|
||||
_model = model;
|
||||
}
|
||||
|
||||
public AccessLevel CurrentLevel => _model.Level;
|
||||
public bool IsExpired => _model.Expires.HasValue && _model.Expires.Value.Date < DateTime.UtcNow.Date;
|
||||
public string? Owner => _model.Owner;
|
||||
|
||||
public static ILicenseProvider FromConfig(IConfiguration cfg, IAppLogger log)
|
||||
{
|
||||
var path = cfg["License:Path"];
|
||||
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
|
||||
{
|
||||
log.Warn("License file not found, fallback to Minimal.");
|
||||
return new LicenseProvider(new LicenseModel { Level = AccessLevel.Minimal });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(path);
|
||||
|
||||
var opts = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
opts.Converters.Add(new JsonStringEnumConverter(allowIntegerValues: true));
|
||||
|
||||
var model = JsonSerializer.Deserialize<LicenseModel>(json, opts)
|
||||
?? new LicenseModel { Level = AccessLevel.Minimal };
|
||||
|
||||
return new LicenseProvider(model);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.Error("License read error", ex);
|
||||
return new LicenseProvider(new LicenseModel { Level = AccessLevel.Minimal });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private sealed class LicenseModel
|
||||
{
|
||||
public AccessLevel Level { get; set; } = AccessLevel.Minimal;
|
||||
public DateTime? Expires { get; set; }
|
||||
public string? Owner { get; set; }
|
||||
}
|
||||
}
|
||||
74
WinFormsComponentOrientedHost/Program.cs
Normal file
74
WinFormsComponentOrientedHost/Program.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
130
WinFormsComponentOrientedHost/UI/FormMain.cs
Normal file
130
WinFormsComponentOrientedHost/UI/FormMain.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
7
WinFormsComponentOrientedHost/appsettings.json
Normal file
7
WinFormsComponentOrientedHost/appsettings.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"Plugins": { "Path": ".\\Plugins" },
|
||||
"License": { "Path": ".\\Licenses\\license.json" },
|
||||
"Database": {
|
||||
"ConnectionString": "Host=localhost;Port=5432;Database=kop_db;Username=postgres;Password=1234;"
|
||||
}
|
||||
}
|
||||
37
WinFormsControlLibrary1/ComboSingleAddControl.Designer.cs
generated
Normal file
37
WinFormsControlLibrary1/ComboSingleAddControl.Designer.cs
generated
Normal file
@@ -0,0 +1,37 @@
|
||||
namespace WinFormsControlLibrary1
|
||||
{
|
||||
partial class ComboSingleAddControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Обязательная переменная конструктора.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Освободить все используемые ресурсы.
|
||||
/// </summary>
|
||||
/// <param name="disposing">истинно, если управляемый ресурс должен быть удален; иначе ложно.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Код, автоматически созданный конструктором компонентов
|
||||
|
||||
/// <summary>
|
||||
/// Требуемый метод для поддержки конструктора — не изменяйте
|
||||
/// содержимое этого метода с помощью редактора кода.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
components = new System.ComponentModel.Container();
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
51
WinFormsControlLibrary1/ComboSingleAddControl.cs
Normal file
51
WinFormsControlLibrary1/ComboSingleAddControl.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace WinFormsControlLibrary1;
|
||||
|
||||
|
||||
public partial class ComboSingleAddControl : UserControl
|
||||
{
|
||||
private readonly ComboBox _combo = new() { Dock = DockStyle.Fill };
|
||||
public EventHandler? SelectedValueChanged;
|
||||
public ComboSingleAddControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
Controls.Add(_combo);
|
||||
_combo.SelectedIndexChanged += (_, __) => SelectedValueChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
|
||||
public string SelectedValue
|
||||
{
|
||||
get => _combo.SelectedItem.ToString() ?? string.Empty;
|
||||
set
|
||||
{
|
||||
if(string.IsNullOrEmpty(value)) { _combo.SelectedIndex = -1; return; }
|
||||
int id = _combo.Items.IndexOf(value);
|
||||
if(id >= 0) _combo.SelectedIndex = id;
|
||||
}
|
||||
}
|
||||
|
||||
public void AddValue(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return;
|
||||
if (_combo.Items.Contains(value)) return;
|
||||
_combo.Items.Add(value);
|
||||
if(_combo.SelectedIndex < 0) _combo.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
public void ClearItems()
|
||||
{
|
||||
_combo.Items.Clear();
|
||||
_combo.SelectedIndex = -1;
|
||||
}
|
||||
}
|
||||
120
WinFormsControlLibrary1/ComboSingleAddControl.resx
Normal file
120
WinFormsControlLibrary1/ComboSingleAddControl.resx
Normal file
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
37
WinFormsControlLibrary1/DateByPatternTextBox.Designer.cs
generated
Normal file
37
WinFormsControlLibrary1/DateByPatternTextBox.Designer.cs
generated
Normal file
@@ -0,0 +1,37 @@
|
||||
namespace WinFormsControlLibrary1
|
||||
{
|
||||
partial class DateByPatternTextBox
|
||||
{
|
||||
/// <summary>
|
||||
/// Обязательная переменная конструктора.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Освободить все используемые ресурсы.
|
||||
/// </summary>
|
||||
/// <param name="disposing">истинно, если управляемый ресурс должен быть удален; иначе ложно.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Код, автоматически созданный конструктором компонентов
|
||||
|
||||
/// <summary>
|
||||
/// Требуемый метод для поддержки конструктора — не изменяйте
|
||||
/// содержимое этого метода с помощью редактора кода.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
components = new System.ComponentModel.Container();
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
79
WinFormsControlLibrary1/DateByPatternTextBox.cs
Normal file
79
WinFormsControlLibrary1/DateByPatternTextBox.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using WinFormsControlLibrary1.Exceptions;
|
||||
using static System.Net.Mime.MediaTypeNames;
|
||||
|
||||
namespace WinFormsControlLibrary1;
|
||||
|
||||
public partial class DateByPatternTextBox : UserControl
|
||||
{
|
||||
private readonly TextBox patternTextBox = new() { Dock = DockStyle.Fill };
|
||||
private readonly ToolTip _tip = new();
|
||||
private string? _pattern;
|
||||
private Regex? _regex;
|
||||
public event EventHandler? ValueChanged;
|
||||
|
||||
|
||||
public DateByPatternTextBox()
|
||||
{
|
||||
InitializeComponent();
|
||||
Controls.Add(patternTextBox);
|
||||
patternTextBox.TextChanged += (_, __) => ValueChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
|
||||
public string? Pattern
|
||||
{
|
||||
get => _pattern;
|
||||
set
|
||||
{
|
||||
_pattern = value;
|
||||
_regex = string.IsNullOrWhiteSpace(value) ? null : new Regex(value, RegexOptions.Compiled);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetToolTip(string example)
|
||||
{
|
||||
_tip.SetToolTip(patternTextBox, example);
|
||||
}
|
||||
|
||||
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
|
||||
public string Value
|
||||
{
|
||||
get
|
||||
{
|
||||
if(_regex is null)
|
||||
{
|
||||
throw new ComponentValidationException("Не задан шаблон дaты (Pattern).");
|
||||
}
|
||||
var text = patternTextBox.Text.Trim();
|
||||
if (!_regex.IsMatch(text))
|
||||
{
|
||||
throw new ComponentValidationException("Введённая дата не соответствует заданному формату.");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (_regex is null)
|
||||
{
|
||||
throw new ComponentValidationException("Не задан шаблон дaты (Pattern).");
|
||||
}
|
||||
var text = value.Trim();
|
||||
if (!_regex.IsMatch(text))
|
||||
{
|
||||
throw new ComponentValidationException("Введённая дата не соответствует заданному формату.");
|
||||
}
|
||||
patternTextBox.Text = value ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
120
WinFormsControlLibrary1/DateByPatternTextBox.resx
Normal file
120
WinFormsControlLibrary1/DateByPatternTextBox.resx
Normal file
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace WinFormsControlLibrary1.Exceptions;
|
||||
|
||||
public class ComponentValidationException : Exception
|
||||
{
|
||||
public ComponentValidationException(string message) : base(message) { }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace WinFormsControlLibrary1.Exceptions;
|
||||
|
||||
public class TemplateConfigurationException : Exception
|
||||
{
|
||||
public TemplateConfigurationException(string message) : base(message) { }
|
||||
}
|
||||
12
WinFormsControlLibrary1/Exceptions/TemplateParseException.cs
Normal file
12
WinFormsControlLibrary1/Exceptions/TemplateParseException.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace WinFormsControlLibrary1.Exceptions;
|
||||
|
||||
internal class TemplateParseException : Exception
|
||||
{
|
||||
public TemplateParseException(string message) : base(message) { }
|
||||
}
|
||||
37
WinFormsControlLibrary1/TemplatedListBox.Designer.cs
generated
Normal file
37
WinFormsControlLibrary1/TemplatedListBox.Designer.cs
generated
Normal file
@@ -0,0 +1,37 @@
|
||||
namespace WinFormsControlLibrary1
|
||||
{
|
||||
partial class TemplatedListBox
|
||||
{
|
||||
/// <summary>
|
||||
/// Обязательная переменная конструктора.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Освободить все используемые ресурсы.
|
||||
/// </summary>
|
||||
/// <param name="disposing">истинно, если управляемый ресурс должен быть удален; иначе ложно.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Код, автоматически созданный конструктором компонентов
|
||||
|
||||
/// <summary>
|
||||
/// Требуемый метод для поддержки конструктора — не изменяйте
|
||||
/// содержимое этого метода с помощью редактора кода.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
components = new System.ComponentModel.Container();
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
169
WinFormsControlLibrary1/TemplatedListBox.cs
Normal file
169
WinFormsControlLibrary1/TemplatedListBox.cs
Normal file
@@ -0,0 +1,169 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using System.Xml.Linq;
|
||||
using WinFormsControlLibrary1.Exceptions;
|
||||
|
||||
namespace WinFormsControlLibrary1;
|
||||
|
||||
public partial class TemplatedListBox : UserControl
|
||||
{
|
||||
private readonly ListBox _lb = new() { Dock = DockStyle.Fill };
|
||||
|
||||
private string _template = "";
|
||||
private string _open = "{";
|
||||
private string _close = "}";
|
||||
|
||||
public event EventHandler? SelectedIndexChanged;
|
||||
|
||||
public TemplatedListBox()
|
||||
{
|
||||
InitializeComponent();
|
||||
Controls.Add(_lb);
|
||||
_lb.HorizontalScrollbar = true;
|
||||
_lb.SelectedIndexChanged += (_, __) => SelectedIndexChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public void SetTemplate(string template, string open = "{", string close = "}")
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(template))
|
||||
throw new TemplateConfigurationException("Шаблон не должен быть пустым.");
|
||||
|
||||
_template = template;
|
||||
_open = open;
|
||||
_close = close;
|
||||
|
||||
if (template[0] == open[0] || template[template.Length - 1] == close[0])
|
||||
throw new TemplateConfigurationException("Шаблон не должен начинаться или заканчиваться свойством.");
|
||||
|
||||
CheckNoDoubleProperties(template, open, close);
|
||||
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_lb.Items.Clear();
|
||||
}
|
||||
|
||||
public void AddItem<T>(T item)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_template))
|
||||
throw new TemplateConfigurationException("Сначала вызовите SetTemplate.");
|
||||
|
||||
var text = Render(item);
|
||||
_lb.Items.Add(text);
|
||||
}
|
||||
|
||||
private string Render(object obj)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_template)) return string.Empty;
|
||||
|
||||
string result = _template;
|
||||
var type = obj.GetType();
|
||||
|
||||
foreach (var p in type.GetProperties())
|
||||
{
|
||||
if (!p.CanRead) continue;
|
||||
|
||||
string name = p.Name;
|
||||
|
||||
string val = Convert.ToString(p.GetValue(obj)) ?? string.Empty;
|
||||
|
||||
result = result.Replace($"{_open}{name}{_close}", val);
|
||||
}
|
||||
|
||||
foreach (var f in type.GetFields())
|
||||
{
|
||||
string name = f.Name;
|
||||
|
||||
string val = Convert.ToString(f.GetValue(obj)) ?? string.Empty;
|
||||
|
||||
result = result.Replace($"{_open}{name}{_close}", val);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public T? GetSelectedObject<T>() where T : class, new()
|
||||
{
|
||||
if (_lb.SelectedIndex == -1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
string row = _lb.SelectedItem!.ToString()!;
|
||||
T obj = new T();
|
||||
var type = typeof(T);
|
||||
|
||||
List<string> substrings = new List<string>();
|
||||
List<string> props = ExtractPlaceholders(_template, _open, _close);
|
||||
|
||||
string template = _template;
|
||||
|
||||
substrings.AddRange(template.Split(_open + _close));
|
||||
|
||||
int int1 = 0; int int2 = 0;
|
||||
|
||||
for (int i = 0; i < props.Count; i++)
|
||||
{
|
||||
int1 = row.IndexOf(substrings[i]) + substrings[i].Length;
|
||||
int2 = row.IndexOf(substrings[i + 1]);
|
||||
if (substrings[i + 1] == "")
|
||||
{
|
||||
int2 = row.Length;
|
||||
}
|
||||
var value = row[int1..int2];
|
||||
|
||||
var p = type.GetProperty(props[i]);
|
||||
|
||||
if (p is null) continue;
|
||||
|
||||
p.SetValue(obj, Convert.ChangeType(value, p.PropertyType));
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
private List<string> ExtractPlaceholders(string template, string open, string close)
|
||||
{
|
||||
var names = new List<string>();
|
||||
int i = 0;
|
||||
while (i < template.Length)
|
||||
{
|
||||
if (template[i] == open[0])
|
||||
{
|
||||
int j = template.IndexOf(close, i + 1, StringComparison.Ordinal);
|
||||
string name = template.Substring(i + 1, j - i - 1).Trim();
|
||||
names.Add(name);
|
||||
i = j + 1;
|
||||
}
|
||||
else i++;
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
private static void CheckNoDoubleProperties(string template, string open, string close)
|
||||
{
|
||||
for (int i = 0; i < template.Length; i++)
|
||||
{
|
||||
if (template[i] == close[0])
|
||||
{
|
||||
int k = i + 1;
|
||||
while (k < template.Length && char.IsWhiteSpace(template[k])) k++;
|
||||
if (k < template.Length && template[k] == open[0])
|
||||
throw new TemplateConfigurationException("В шаблоне не должно идти два свойства подряд без текста между ними.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
120
WinFormsControlLibrary1/TemplatedListBox.resx
Normal file
120
WinFormsControlLibrary1/TemplatedListBox.resx
Normal file
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
38
WinFormsControlLibrary1/UserControl1.Designer.cs
generated
38
WinFormsControlLibrary1/UserControl1.Designer.cs
generated
@@ -1,38 +0,0 @@
|
||||
namespace WinFormsControlLibrary1
|
||||
{
|
||||
partial class UserControl1
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Component Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
components = new System.ComponentModel.Container();
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(800, 450);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace WinFormsControlLibrary1
|
||||
{
|
||||
public partial class UserControl1 : UserControl
|
||||
{
|
||||
public UserControl1()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0-windows</TargetFramework>
|
||||
<TargetFramework>net8.0-windows7.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Title>MyComponentsLibrary</Title>
|
||||
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||
<Authors>Romtec</Authors>
|
||||
<Version>1.0.5</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
49
WinFormsControlLibrary1/WinFormsControlLibrary1.sln
Normal file
49
WinFormsControlLibrary1/WinFormsControlLibrary1.sln
Normal 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
|
||||
Reference in New Issue
Block a user