Сделал 2 лабу

This commit is contained in:
2025-09-23 14:07:38 +04:00
parent 34d0d1aea6
commit c0f6d17928
37 changed files with 2033 additions and 3 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,7 +26,7 @@ public partial class ComboSingleAddControl : UserControl
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
public string SelectedValue
{
get => _combo.SelectedIndex.ToString() ?? string.Empty;
get => _combo.SelectedItem.ToString() ?? string.Empty;
set
{
if(string.IsNullOrEmpty(value)) { _combo.SelectedIndex = -1; return; }

View File

@@ -8,7 +8,7 @@
<Title>MyComponentsLibrary</Title>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<Authors>Romtec</Authors>
<Version>1.0.4</Version>
<Version>1.0.5</Version>
</PropertyGroup>
</Project>

View File

@@ -1,10 +1,18 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.36401.2 d17.14
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
@@ -15,6 +23,22 @@ Global
{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