Aleksaandrooov.DemoExam.WinForms.Template 1.0.2

dotnet new install Aleksaandrooov.DemoExam.WinForms.Template@1.0.2
                    
This package contains a .NET Template Package you can call from the shell/command line.

Aleksaandrooov.DemoExam.WinForms.Template

NuGet-пакет шаблона для создания минимального проекта Windows Forms App на .NET 8 с SQL Server.

После установки пакет добавляет шаблон с коротким именем:

demoexam-winforms

Установка из NuGet

dotnet new install Aleksaandrooov.DemoExam.WinForms.Template@1.0.2

Локальная установка готового пакета

dotnet new install .\Aleksaandrooov.DemoExam.WinForms.Template.1.0.2.nupkg

Создание проекта

dotnet new demoexam-winforms -n ShoeShop `
  --dbServer "192.168.0.106,1433" `
  --dbName "DemoExamShoes" `
  --dbUser "sa" `
  --dbPassword "YOUR_PASSWORD"

После создания откройте ShoeShop.slnx.

Не публикуйте пакет с настоящим паролем в исходных файлах. В шаблоне используются заменяемые параметры подключения.

Целевая платформа проекта: net8.0-windows.

Резервная копия проекта: код по файлам

Этот раздел нужен на случай, если на экзамене не получится скачать или установить шаблон. Создайте обычный проект Windows Forms App, затем создайте папки и файлы по указанным путям и скопируйте содержимое каждого блока.

Бинарные файлы Assets/logo.png и Assets/app.ico в текстовом README не дублируются. Их можно заменить выданными на экзамене ресурсами. Если иконки нет, удалите строку ApplicationIcon из .csproj.

Структура

.template.config/template.json
Database/CreateDatabase.sql
DemoExamTemplate.sln
DemoExamTemplate.slnx
src/DemoExamTemplate/Data/AppConfig.cs
src/DemoExamTemplate/Data/Db.cs
src/DemoExamTemplate/DemoExamTemplate.csproj
src/DemoExamTemplate/Forms/LoginForm.Designer.cs
src/DemoExamTemplate/Forms/LoginForm.cs
src/DemoExamTemplate/Forms/LoginForm.resx
src/DemoExamTemplate/Forms/MainForm.Designer.cs
src/DemoExamTemplate/Forms/MainForm.cs
src/DemoExamTemplate/Forms/MainForm.resx
src/DemoExamTemplate/Forms/OrderEditForm.Designer.cs
src/DemoExamTemplate/Forms/OrderEditForm.cs
src/DemoExamTemplate/Forms/OrderEditForm.resx
src/DemoExamTemplate/Forms/OrdersForm.Designer.cs
src/DemoExamTemplate/Forms/OrdersForm.cs
src/DemoExamTemplate/Forms/OrdersForm.resx
src/DemoExamTemplate/Forms/ProductEditForm.Designer.cs
src/DemoExamTemplate/Forms/ProductEditForm.cs
src/DemoExamTemplate/Forms/ProductEditForm.resx
src/DemoExamTemplate/Models/Category.cs
src/DemoExamTemplate/Models/OrderModels.cs
src/DemoExamTemplate/Models/Product.cs
src/DemoExamTemplate/Models/UserSession.cs
src/DemoExamTemplate/Program.cs
src/DemoExamTemplate/Services/AuthService.cs
src/DemoExamTemplate/Services/OrderService.cs
src/DemoExamTemplate/Services/ProductService.cs
src/DemoExamTemplate/UI/UiHelper.cs
src/DemoExamTemplate/appsettings.json
Assets/logo.png
Assets/app.ico

1. .template.config/template.json

{
  "$schema": "http://json.schemastore.org/template",
  "author": "Aleksaandrooov",
  "classifications": [
    "Windows Forms",
    "Desktop",
    "Demo Exam",
    "SQL Server"
  ],
  "identity": "Aleksaandrooov.DemoExam.WinForms.Template",
  "name": "Demo Exam Windows Forms SQL Server Template",
  "shortName": "demoexam-winforms",
  "sourceName": "DemoExamTemplate",
  "defaultName": "DemoExamProject",
  "preferNameDirectory": true,
  "tags": {
    "language": "C#",
    "type": "project"
  },
  "guids": [
    "8D32B0B9-1F0D-4D5C-8A2D-73A1B5A1B234"
  ],
  "symbols": {
    "dbServer": {
      "type": "parameter",
      "datatype": "text",
      "defaultValue": "localhost,1433",
      "description": "SQL Server address, for example localhost,1433",
      "replaces": "__DB_SERVER__"
    },
    "dbName": {
      "type": "parameter",
      "datatype": "text",
      "defaultValue": "DemoExamShoes",
      "description": "Database name",
      "replaces": "__DB_NAME__"
    },
    "dbUser": {
      "type": "parameter",
      "datatype": "text",
      "defaultValue": "sa",
      "description": "SQL Server login",
      "replaces": "__DB_USER__"
    },
    "dbPassword": {
      "type": "parameter",
      "datatype": "text",
      "defaultValue": "CHANGE_ME",
      "description": "SQL Server password",
      "replaces": "__DB_PASSWORD__"
    }
  },
  "sources": [
    {
      "modifiers": [
        {
          "exclude": [
            ".vs/**",
            "**/bin/**",
            "**/obj/**",
            "**/*.user",
            "**/*.suo"
          ]
        }
      ]
    }
  ],
  "primaryOutputs": [
    {
      "path": "DemoExamTemplate.slnx"
    },
    {
      "path": "src/DemoExamTemplate/DemoExamTemplate.csproj"
    }
  ]
}

2. Database/CreateDatabase.sql

USE master;
GO

IF DB_ID(N'__DB_NAME__') IS NULL
    CREATE DATABASE [__DB_NAME__];
GO

USE [__DB_NAME__];
GO

CREATE TABLE dbo.Roles
(
    Id INT IDENTITY(1,1) PRIMARY KEY,
    Code NVARCHAR(20) NOT NULL UNIQUE,
    Name NVARCHAR(50) NOT NULL
);
GO

CREATE TABLE dbo.Users
(
    Id INT IDENTITY(1,1) PRIMARY KEY,
    Login NVARCHAR(50) NOT NULL UNIQUE,
    Password NVARCHAR(100) NOT NULL,
    FullName NVARCHAR(150) NOT NULL,
    RoleId INT NOT NULL,
    CONSTRAINT FK_Users_Roles FOREIGN KEY (RoleId) REFERENCES dbo.Roles(Id)
);
GO

CREATE TABLE dbo.Categories
(
    Id INT IDENTITY(1,1) PRIMARY KEY,
    Name NVARCHAR(100) NOT NULL UNIQUE
);
GO

CREATE TABLE dbo.Products
(
    Id INT IDENTITY(1,1) PRIMARY KEY,
    Article NVARCHAR(30) NOT NULL UNIQUE,
    Name NVARCHAR(150) NOT NULL,
    CategoryId INT NOT NULL,
    Price DECIMAL(18,2) NOT NULL CHECK (Price > 0),
    Discount INT NOT NULL DEFAULT 0 CHECK (Discount BETWEEN 0 AND 100),
    Quantity INT NOT NULL DEFAULT 0 CHECK (Quantity >= 0),
    CONSTRAINT FK_Products_Categories FOREIGN KEY (CategoryId) REFERENCES dbo.Categories(Id)
);
GO

CREATE TABLE dbo.Orders
(
    Id INT IDENTITY(1,1) PRIMARY KEY,
    Number NVARCHAR(30) NOT NULL UNIQUE,
    OrderDate DATE NOT NULL,
    Customer NVARCHAR(150) NOT NULL,
    Status NVARCHAR(50) NOT NULL
);
GO

CREATE TABLE dbo.OrderItems
(
    Id INT IDENTITY(1,1) PRIMARY KEY,
    OrderId INT NOT NULL,
    ProductId INT NOT NULL,
    Quantity INT NOT NULL CHECK (Quantity > 0),
    Price DECIMAL(18,2) NOT NULL CHECK (Price > 0),
    Discount INT NOT NULL DEFAULT 0 CHECK (Discount BETWEEN 0 AND 100),
    CONSTRAINT FK_OrderItems_Orders FOREIGN KEY (OrderId) REFERENCES dbo.Orders(Id) ON DELETE CASCADE,
    CONSTRAINT FK_OrderItems_Products FOREIGN KEY (ProductId) REFERENCES dbo.Products(Id)
);
GO

INSERT INTO dbo.Roles(Code, Name) VALUES
(N'CLIENT', N'Клиент'),
(N'MANAGER', N'Менеджер'),
(N'ADMIN', N'Администратор');
GO

INSERT INTO dbo.Users(Login, Password, FullName, RoleId)
SELECT N'client', N'client', N'Клиент', Id FROM dbo.Roles WHERE Code=N'CLIENT';
INSERT INTO dbo.Users(Login, Password, FullName, RoleId)
SELECT N'manager', N'manager', N'Менеджер', Id FROM dbo.Roles WHERE Code=N'MANAGER';
INSERT INTO dbo.Users(Login, Password, FullName, RoleId)
SELECT N'admin', N'admin', N'Администратор', Id FROM dbo.Roles WHERE Code=N'ADMIN';
GO

INSERT INTO dbo.Categories(Name) VALUES
(N'Кроссовки'), (N'Ботинки'), (N'Туфли');
GO

INSERT INTO dbo.Products(Article, Name, CategoryId, Price, Discount, Quantity)
SELECT N'SH-001', N'Кроссовки', Id, 4990, 10, 20 FROM dbo.Categories WHERE Name=N'Кроссовки';
INSERT INTO dbo.Products(Article, Name, CategoryId, Price, Discount, Quantity)
SELECT N'SH-002', N'Зимние ботинки', Id, 7990, 20, 10 FROM dbo.Categories WHERE Name=N'Ботинки';
INSERT INTO dbo.Products(Article, Name, CategoryId, Price, Discount, Quantity)
SELECT N'SH-003', N'Классические туфли', Id, 5990, 0, 15 FROM dbo.Categories WHERE Name=N'Туфли';
GO

3. DemoExamTemplate.sln

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.0.0.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DemoExamTemplate", "src\DemoExamTemplate\DemoExamTemplate.csproj", "{8D32B0B9-1F0D-4D5C-8A2D-73A1B5A1B234}"
EndProject
Global
	GlobalSection(SolutionConfigurationPlatforms) = preSolution
		Debug|Any CPU = Debug|Any CPU
		Release|Any CPU = Release|Any CPU
	EndGlobalSection
	GlobalSection(ProjectConfigurationPlatforms) = postSolution
		{8D32B0B9-1F0D-4D5C-8A2D-73A1B5A1B234}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{8D32B0B9-1F0D-4D5C-8A2D-73A1B5A1B234}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{8D32B0B9-1F0D-4D5C-8A2D-73A1B5A1B234}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{8D32B0B9-1F0D-4D5C-8A2D-73A1B5A1B234}.Release|Any CPU.Build.0 = Release|Any CPU
	EndGlobalSection
EndGlobal

4. DemoExamTemplate.slnx

<Solution>
  <Project Path="src/DemoExamTemplate/DemoExamTemplate.csproj" />
</Solution>

5. src/DemoExamTemplate/Data/AppConfig.cs

using System.Text.Json;

namespace DemoExamTemplate.Data;

public static class AppConfig
{
    public static string ConnectionString { get; }

    static AppConfig()
    {
        string path = Path.Combine(AppContext.BaseDirectory, "appsettings.json");
        string json = File.ReadAllText(path);
        using JsonDocument document = JsonDocument.Parse(json);
        ConnectionString = document.RootElement.GetProperty("ConnectionString").GetString()
            ?? throw new InvalidOperationException("В appsettings.json не задана строка подключения.");
    }
}

6. src/DemoExamTemplate/Data/Db.cs

using Microsoft.Data.SqlClient;

namespace DemoExamTemplate.Data;

public static class Db
{
    public static SqlConnection CreateConnection()
    {
        return new SqlConnection(AppConfig.ConnectionString);
    }
}

7. src/DemoExamTemplate/DemoExamTemplate.csproj

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net8.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <UseWindowsForms>true</UseWindowsForms>
    <ImplicitUsings>enable</ImplicitUsings>
    <ForceDesignerDPIUnaware>true</ForceDesignerDPIUnaware>
    <ApplicationIcon Condition="Exists('Assets\app.ico')">Assets\app.ico</ApplicationIcon>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
  </ItemGroup>

  <ItemGroup>
    <None Update="appsettings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Update="Assets\logo.png">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
  </ItemGroup>

  <ItemGroup>
    <Compile Update="Forms\LoginForm.cs"><SubType>Form</SubType></Compile>
    <Compile Update="Forms\LoginForm.Designer.cs"><DependentUpon>LoginForm.cs</DependentUpon></Compile>
    <EmbeddedResource Update="Forms\LoginForm.resx"><DependentUpon>LoginForm.cs</DependentUpon></EmbeddedResource>
    <Compile Update="Forms\MainForm.cs"><SubType>Form</SubType></Compile>
    <Compile Update="Forms\MainForm.Designer.cs"><DependentUpon>MainForm.cs</DependentUpon></Compile>
    <EmbeddedResource Update="Forms\MainForm.resx"><DependentUpon>MainForm.cs</DependentUpon></EmbeddedResource>
    <Compile Update="Forms\ProductEditForm.cs"><SubType>Form</SubType></Compile>
    <Compile Update="Forms\ProductEditForm.Designer.cs"><DependentUpon>ProductEditForm.cs</DependentUpon></Compile>
    <EmbeddedResource Update="Forms\ProductEditForm.resx"><DependentUpon>ProductEditForm.cs</DependentUpon></EmbeddedResource>
    <Compile Update="Forms\OrdersForm.cs"><SubType>Form</SubType></Compile>
    <Compile Update="Forms\OrdersForm.Designer.cs"><DependentUpon>OrdersForm.cs</DependentUpon></Compile>
    <EmbeddedResource Update="Forms\OrdersForm.resx"><DependentUpon>OrdersForm.cs</DependentUpon></EmbeddedResource>
    <Compile Update="Forms\OrderEditForm.cs"><SubType>Form</SubType></Compile>
    <Compile Update="Forms\OrderEditForm.Designer.cs"><DependentUpon>OrderEditForm.cs</DependentUpon></Compile>
    <EmbeddedResource Update="Forms\OrderEditForm.resx"><DependentUpon>OrderEditForm.cs</DependentUpon></EmbeddedResource>
  </ItemGroup>
</Project>

8. src/DemoExamTemplate/Forms/LoginForm.Designer.cs

namespace DemoExamTemplate.Forms;

partial class LoginForm
{
    private System.ComponentModel.IContainer? components = null;
    protected override void Dispose(bool disposing)
    {
        if (disposing && components != null) components.Dispose();
        base.Dispose(disposing);
    }

    #region Windows Form Designer generated code
    private void InitializeComponent()
    {
        lblTitle = new Label();
        lblLogin = new Label();
        txtLogin = new TextBox();
        lblPassword = new Label();
        txtPassword = new TextBox();
        btnLogin = new Button();
        btnGuest = new Button();
        SuspendLayout();
        // lblTitle
        lblTitle.AutoSize = true;
        lblTitle.Font = new Font("Times New Roman", 20F, FontStyle.Bold);
        lblTitle.Location = new Point(92, 38);
        lblTitle.Name = "lblTitle";
        lblTitle.Size = new Size(246, 31);
        lblTitle.Text = "Вход в систему";
        // lblLogin
        lblLogin.AutoSize = true;
        lblLogin.Location = new Point(55, 112);
        lblLogin.Name = "lblLogin";
        lblLogin.Size = new Size(52, 17);
        lblLogin.Text = "Логин";
        // txtLogin
        txtLogin.Location = new Point(55, 135);
        txtLogin.Name = "txtLogin";
        txtLogin.Size = new Size(320, 24);
        // lblPassword
        lblPassword.AutoSize = true;
        lblPassword.Location = new Point(55, 178);
        lblPassword.Name = "lblPassword";
        lblPassword.Size = new Size(58, 17);
        lblPassword.Text = "Пароль";
        // txtPassword
        txtPassword.Location = new Point(55, 201);
        txtPassword.Name = "txtPassword";
        txtPassword.PasswordChar = '*';
        txtPassword.Size = new Size(320, 24);
        // btnLogin
        btnLogin.BackColor = Color.MediumSpringGreen;
        btnLogin.FlatStyle = FlatStyle.Flat;
        btnLogin.Location = new Point(55, 255);
        btnLogin.Name = "btnLogin";
        btnLogin.Size = new Size(150, 42);
        btnLogin.Text = "Войти";
        btnLogin.UseVisualStyleBackColor = false;
        btnLogin.Click += btnLogin_Click;
        // btnGuest
        btnGuest.BackColor = Color.Chartreuse;
        btnGuest.FlatStyle = FlatStyle.Flat;
        btnGuest.Location = new Point(225, 255);
        btnGuest.Name = "btnGuest";
        btnGuest.Size = new Size(150, 42);
        btnGuest.Text = "Войти как гость";
        btnGuest.UseVisualStyleBackColor = false;
        btnGuest.Click += btnGuest_Click;
        // LoginForm
        AcceptButton = btnLogin;
        AutoScaleDimensions = new SizeF(8F, 17F);
        AutoScaleMode = AutoScaleMode.Font;
        BackColor = Color.White;
        ClientSize = new Size(430, 350);
        Controls.Add(btnGuest);
        Controls.Add(btnLogin);
        Controls.Add(txtPassword);
        Controls.Add(lblPassword);
        Controls.Add(txtLogin);
        Controls.Add(lblLogin);
        Controls.Add(lblTitle);
        Font = new Font("Times New Roman", 11F);
        FormBorderStyle = FormBorderStyle.FixedSingle;
        MaximizeBox = false;
        Name = "LoginForm";
        StartPosition = FormStartPosition.CenterScreen;
        Text = "Авторизация";
        ResumeLayout(false);
        PerformLayout();
    }
    #endregion

    private Label lblTitle;
    private Label lblLogin;
    private TextBox txtLogin;
    private Label lblPassword;
    private TextBox txtPassword;
    private Button btnLogin;
    private Button btnGuest;
}

9. src/DemoExamTemplate/Forms/LoginForm.cs

using DemoExamTemplate.Models;
using DemoExamTemplate.Services;
using DemoExamTemplate.UI;

namespace DemoExamTemplate.Forms;

public partial class LoginForm : Form
{
    private readonly AuthService _authService = new AuthService();

    public LoginForm()
    {
        InitializeComponent();
    }

    private async void btnLogin_Click(object? sender, EventArgs e)
    {
        if (string.IsNullOrWhiteSpace(txtLogin.Text) || string.IsNullOrWhiteSpace(txtPassword.Text))
        {
            MessageBox.Show("Введите логин и пароль.", "Авторизация", MessageBoxButtons.OK, MessageBoxIcon.Information);
            return;
        }

        try
        {
            UserSession? session = await _authService.LoginAsync(txtLogin.Text, txtPassword.Text);
            if (session is null)
            {
                MessageBox.Show("Неверный логин или пароль.", "Авторизация", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                return;
            }
            OpenMain(session);
        }
        catch (Exception ex)
        {
            UiHelper.ShowError(ex);
        }
    }

    private void btnGuest_Click(object? sender, EventArgs e)
    {
        OpenMain(UserSession.Guest());
    }

    private void OpenMain(UserSession session)
    {
        Hide();
        using MainForm form = new MainForm(session);
        form.ShowDialog(this);
        Show();
        txtPassword.Clear();
    }
}

10. src/DemoExamTemplate/Forms/LoginForm.resx

<?xml version="1.0" encoding="utf-8"?>
<root>
  <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" /><xsd:element name="comment" type="xsd:string" minOccurs="0" /></xsd:sequence><xsd:attribute name="name" type="xsd:string" use="required" /><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="resheader"><xsd:complexType><xsd:sequence><xsd:element name="value" type="xsd:string" minOccurs="0" /></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>

11. src/DemoExamTemplate/Forms/MainForm.Designer.cs

namespace DemoExamTemplate.Forms;

partial class MainForm
{
    private System.ComponentModel.IContainer? components = null;
    protected override void Dispose(bool disposing)
    {
        if (disposing && components != null) components.Dispose();
        base.Dispose(disposing);
    }

    #region Windows Form Designer generated code
    private void InitializeComponent()
    {
        DataGridViewCellStyle headerStyle = new DataGridViewCellStyle();
        pnlHeader = new Panel();
        lblUser = new Label();
        lblTitle = new Label();
        picLogo = new PictureBox();
        pnlTools = new FlowLayoutPanel();
        lblSearch = new Label();
        txtSearch = new TextBox();
        lblCategory = new Label();
        cmbCategory = new ComboBox();
        lblSort = new Label();
        cmbSort = new ComboBox();
        btnAdd = new Button();
        btnEdit = new Button();
        btnDelete = new Button();
        btnOrders = new Button();
        dgvProducts = new DataGridView();
        colArticle = new DataGridViewTextBoxColumn();
        colName = new DataGridViewTextBoxColumn();
        colCategory = new DataGridViewTextBoxColumn();
        colPrice = new DataGridViewTextBoxColumn();
        colDiscount = new DataGridViewTextBoxColumn();
        colQuantity = new DataGridViewTextBoxColumn();
        pnlHeader.SuspendLayout();
        ((System.ComponentModel.ISupportInitialize)picLogo).BeginInit();
        pnlTools.SuspendLayout();
        ((System.ComponentModel.ISupportInitialize)dgvProducts).BeginInit();
        SuspendLayout();
        // pnlHeader
        pnlHeader.BackColor = Color.Chartreuse;
        pnlHeader.Controls.Add(lblUser);
        pnlHeader.Controls.Add(lblTitle);
        pnlHeader.Controls.Add(picLogo);
        pnlHeader.Dock = DockStyle.Top;
        pnlHeader.Location = new Point(0, 0);
        pnlHeader.Name = "pnlHeader";
        pnlHeader.Size = new Size(1180, 95);
        // picLogo
        picLogo.BackColor = Color.White;
        picLogo.Location = new Point(15, 13);
        picLogo.Name = "picLogo";
        picLogo.Size = new Size(70, 70);
        picLogo.SizeMode = PictureBoxSizeMode.Zoom;
        picLogo.TabStop = false;
        // lblTitle
        lblTitle.AutoSize = true;
        lblTitle.Font = new Font("Times New Roman", 20F, FontStyle.Bold);
        lblTitle.Location = new Point(105, 20);
        lblTitle.Name = "lblTitle";
        lblTitle.Size = new Size(208, 31);
        lblTitle.Text = "Каталог товаров";
        // lblUser
        lblUser.AutoSize = true;
        lblUser.Location = new Point(108, 59);
        lblUser.Name = "lblUser";
        lblUser.Size = new Size(94, 17);
        lblUser.Text = "Пользователь";
        // pnlTools
        pnlTools.AutoSize = true;
        pnlTools.Controls.Add(lblSearch);
        pnlTools.Controls.Add(txtSearch);
        pnlTools.Controls.Add(lblCategory);
        pnlTools.Controls.Add(cmbCategory);
        pnlTools.Controls.Add(lblSort);
        pnlTools.Controls.Add(cmbSort);
        pnlTools.Dock = DockStyle.Top;
        pnlTools.Location = new Point(0, 95);
        pnlTools.Name = "pnlTools";
        pnlTools.Padding = new Padding(10);
        pnlTools.Size = new Size(1180, 50);
        pnlTools.WrapContents = false;
        // lblSearch
        lblSearch.AutoSize = true;
        lblSearch.Margin = new Padding(3, 7, 3, 0);
        lblSearch.Text = "Поиск:";
        // txtSearch
        txtSearch.Margin = new Padding(3, 3, 15, 3);
        txtSearch.Size = new Size(250, 24);
        txtSearch.TextChanged += txtSearch_TextChanged;
        // lblCategory
        lblCategory.AutoSize = true;
        lblCategory.Margin = new Padding(3, 7, 3, 0);
        lblCategory.Text = "Категория:";
        // cmbCategory
        cmbCategory.DropDownStyle = ComboBoxStyle.DropDownList;
        cmbCategory.Margin = new Padding(3, 3, 15, 3);
        cmbCategory.Size = new Size(190, 25);
        cmbCategory.SelectedIndexChanged += cmbTools_SelectedIndexChanged;
        // lblSort
        lblSort.AutoSize = true;
        lblSort.Margin = new Padding(3, 7, 3, 0);
        lblSort.Text = "Сортировка:";
        // cmbSort
        cmbSort.DropDownStyle = ComboBoxStyle.DropDownList;
        cmbSort.Size = new Size(220, 25);
        cmbSort.SelectedIndexChanged += cmbTools_SelectedIndexChanged;
        // buttons
        btnAdd.Anchor = AnchorStyles.Top | AnchorStyles.Right;
        btnAdd.BackColor = Color.MediumSpringGreen;
        btnAdd.FlatStyle = FlatStyle.Flat;
        btnAdd.Location = new Point(604, 153);
        btnAdd.Name = "btnAdd";
        btnAdd.Size = new Size(130, 38);
        btnAdd.Text = "Добавить";
        btnAdd.UseVisualStyleBackColor = false;
        btnAdd.Click += btnAdd_Click;
        btnEdit.Anchor = AnchorStyles.Top | AnchorStyles.Right;
        btnEdit.BackColor = Color.Chartreuse;
        btnEdit.FlatStyle = FlatStyle.Flat;
        btnEdit.Location = new Point(744, 153);
        btnEdit.Name = "btnEdit";
        btnEdit.Size = new Size(130, 38);
        btnEdit.Text = "Редактировать";
        btnEdit.UseVisualStyleBackColor = false;
        btnEdit.Click += btnEdit_Click;
        btnDelete.Anchor = AnchorStyles.Top | AnchorStyles.Right;
        btnDelete.BackColor = Color.Chartreuse;
        btnDelete.FlatStyle = FlatStyle.Flat;
        btnDelete.Location = new Point(884, 153);
        btnDelete.Name = "btnDelete";
        btnDelete.Size = new Size(130, 38);
        btnDelete.Text = "Удалить";
        btnDelete.UseVisualStyleBackColor = false;
        btnDelete.Click += btnDelete_Click;
        btnOrders.Anchor = AnchorStyles.Top | AnchorStyles.Right;
        btnOrders.BackColor = Color.MediumSpringGreen;
        btnOrders.FlatStyle = FlatStyle.Flat;
        btnOrders.Location = new Point(1024, 153);
        btnOrders.Name = "btnOrders";
        btnOrders.Size = new Size(130, 38);
        btnOrders.Text = "Заказы";
        btnOrders.UseVisualStyleBackColor = false;
        btnOrders.Click += btnOrders_Click;
        // dgvProducts
        dgvProducts.AllowUserToAddRows = false;
        dgvProducts.AllowUserToDeleteRows = false;
        dgvProducts.AllowUserToResizeRows = false;
        dgvProducts.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
        dgvProducts.AutoGenerateColumns = false;
        dgvProducts.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
        dgvProducts.BackgroundColor = Color.White;
        headerStyle.BackColor = Color.Chartreuse;
        headerStyle.Font = new Font("Times New Roman", 11F, FontStyle.Bold);
        headerStyle.ForeColor = Color.Black;
        dgvProducts.ColumnHeadersDefaultCellStyle = headerStyle;
        dgvProducts.ColumnHeadersHeight = 42;
        dgvProducts.Columns.AddRange(new DataGridViewColumn[] { colArticle, colName, colCategory, colPrice, colDiscount, colQuantity });
        dgvProducts.EnableHeadersVisualStyles = false;
        dgvProducts.Location = new Point(15, 202);
        dgvProducts.MultiSelect = false;
        dgvProducts.Name = "dgvProducts";
        dgvProducts.ReadOnly = true;
        dgvProducts.RowHeadersVisible = false;
        dgvProducts.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
        dgvProducts.Size = new Size(1140, 505);
        dgvProducts.RowPrePaint += dgvProducts_RowPrePaint;
        colArticle.DataPropertyName = "Article"; colArticle.HeaderText = "Артикул"; colArticle.Name = "colArticle";
        colName.DataPropertyName = "Name"; colName.HeaderText = "Наименование"; colName.Name = "colName";
        colCategory.DataPropertyName = "CategoryName"; colCategory.HeaderText = "Категория"; colCategory.Name = "colCategory";
        colPrice.DataPropertyName = "Price"; colPrice.HeaderText = "Цена"; colPrice.Name = "colPrice"; colPrice.DefaultCellStyle.Format = "N2";
        colDiscount.DataPropertyName = "Discount"; colDiscount.HeaderText = "Скидка, %"; colDiscount.Name = "colDiscount";
        colQuantity.DataPropertyName = "Quantity"; colQuantity.HeaderText = "Количество"; colQuantity.Name = "colQuantity";
        // MainForm
        AutoScaleDimensions = new SizeF(8F, 17F);
        AutoScaleMode = AutoScaleMode.Font;
        BackColor = Color.White;
        ClientSize = new Size(1180, 730);
        Controls.Add(dgvProducts);
        Controls.Add(btnOrders);
        Controls.Add(btnDelete);
        Controls.Add(btnEdit);
        Controls.Add(btnAdd);
        Controls.Add(pnlTools);
        Controls.Add(pnlHeader);
        Font = new Font("Times New Roman", 11F);
        MinimumSize = new Size(1000, 650);
        Name = "MainForm";
        StartPosition = FormStartPosition.CenterScreen;
        Text = "Каталог товаров";
        Shown += MainForm_Shown;
        pnlHeader.ResumeLayout(false);
        pnlHeader.PerformLayout();
        ((System.ComponentModel.ISupportInitialize)picLogo).EndInit();
        pnlTools.ResumeLayout(false);
        pnlTools.PerformLayout();
        ((System.ComponentModel.ISupportInitialize)dgvProducts).EndInit();
        ResumeLayout(false);
        PerformLayout();
    }
    #endregion
    private Panel pnlHeader; private PictureBox picLogo; private Label lblTitle; private Label lblUser;
    private FlowLayoutPanel pnlTools; private Label lblSearch; private TextBox txtSearch; private Label lblCategory; private ComboBox cmbCategory; private Label lblSort; private ComboBox cmbSort;
    private Button btnAdd; private Button btnEdit; private Button btnDelete; private Button btnOrders;
    private DataGridView dgvProducts; private DataGridViewTextBoxColumn colArticle; private DataGridViewTextBoxColumn colName; private DataGridViewTextBoxColumn colCategory; private DataGridViewTextBoxColumn colPrice; private DataGridViewTextBoxColumn colDiscount; private DataGridViewTextBoxColumn colQuantity;
}

12. src/DemoExamTemplate/Forms/MainForm.cs

using DemoExamTemplate.Models;
using DemoExamTemplate.Services;
using DemoExamTemplate.UI;

namespace DemoExamTemplate.Forms;

public partial class MainForm : Form
{
    private sealed class SortOption
    {
        public string Name { get; }
        public string Code { get; }
        public SortOption(string name, string code) { Name = name; Code = code; }
        public override string ToString() => Name;
    }

    private readonly UserSession _session;
    private readonly ProductService _productService = new ProductService();

    public MainForm(UserSession session)
    {
        _session = session;
        InitializeComponent();
        UiHelper.LoadLogo(picLogo);
        lblUser.Text = _session.FullName + " — " + _session.RoleName;
        pnlTools.Visible = _session.CanUseProductTools;
        btnAdd.Visible = _session.CanEditProducts;
        btnEdit.Visible = _session.CanEditProducts;
        btnDelete.Visible = _session.CanEditProducts;
        btnOrders.Visible = _session.CanViewOrders;

        if (!_session.CanUseProductTools)
        {
            dgvProducts.Top = 110;
            dgvProducts.Height += 92;
        }
    }

    private async void MainForm_Shown(object? sender, EventArgs e)
    {
        try
        {
            if (_session.CanUseProductTools)
            {
                List<Category> categories = await _productService.GetCategoriesAsync();
                categories.Insert(0, new Category { Id = 0, Name = "Все категории" });
                cmbCategory.DataSource = categories;
                cmbSort.DataSource = new List<SortOption>
                {
                    new SortOption("Название: А–Я", "NAME_ASC"),
                    new SortOption("Название: Я–А", "NAME_DESC"),
                    new SortOption("Цена: по возрастанию", "PRICE_ASC"),
                    new SortOption("Цена: по убыванию", "PRICE_DESC")
                };
            }
            await LoadProductsAsync();
        }
        catch (Exception ex) { UiHelper.ShowError(ex); }
    }

    private async Task LoadProductsAsync()
    {
        string? search = _session.CanUseProductTools ? txtSearch.Text : null;
        int? categoryId = _session.CanUseProductTools && cmbCategory.SelectedItem is Category category && category.Id > 0 ? category.Id : null;
        string sort = _session.CanUseProductTools && cmbSort.SelectedItem is SortOption option ? option.Code : "NAME_ASC";
        dgvProducts.DataSource = await _productService.GetProductsAsync(search, categoryId, sort);
    }

    private async void txtSearch_TextChanged(object? sender, EventArgs e) => await LoadProductsSafeAsync();
    private async void cmbTools_SelectedIndexChanged(object? sender, EventArgs e) { if (CanReload) await LoadProductsSafeAsync(); }

    private bool CanReload => IsHandleCreated && Visible;

    private async Task LoadProductsSafeAsync()
    {
        try { await LoadProductsAsync(); }
        catch (Exception ex) { UiHelper.ShowError(ex); }
    }

    private Product? SelectedProduct => dgvProducts.CurrentRow?.DataBoundItem as Product;

    private async void btnAdd_Click(object? sender, EventArgs e)
    {
        using ProductEditForm form = new ProductEditForm();
        if (form.ShowDialog(this) == DialogResult.OK) await LoadProductsSafeAsync();
    }

    private async void btnEdit_Click(object? sender, EventArgs e)
    {
        Product? product = SelectedProduct;
        if (product is null) { MessageBox.Show("Выберите товар."); return; }
        using ProductEditForm form = new ProductEditForm(product.Id);
        if (form.ShowDialog(this) == DialogResult.OK) await LoadProductsSafeAsync();
    }

    private async void btnDelete_Click(object? sender, EventArgs e)
    {
        Product? product = SelectedProduct;
        if (product is null) { MessageBox.Show("Выберите товар."); return; }
        if (MessageBox.Show("Удалить выбранный товар?", "Удаление", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes) return;
        try { await _productService.DeleteAsync(product.Id); await LoadProductsAsync(); }
        catch (Exception ex) { UiHelper.ShowError(ex); }
    }

    private void btnOrders_Click(object? sender, EventArgs e)
    {
        using OrdersForm form = new OrdersForm(_session);
        form.ShowDialog(this);
    }

    private void dgvProducts_RowPrePaint(object? sender, DataGridViewRowPrePaintEventArgs e)
    {
        DataGridViewRow row = dgvProducts.Rows[e.RowIndex];
        row.DefaultCellStyle.BackColor = Color.White;
        row.DefaultCellStyle.ForeColor = Color.Black;
        if (row.DataBoundItem is Product product && product.Discount > 15)
        {
            row.DefaultCellStyle.BackColor = Color.SeaGreen;
            row.DefaultCellStyle.ForeColor = Color.White;
        }
    }
}

13. src/DemoExamTemplate/Forms/MainForm.resx

<?xml version="1.0" encoding="utf-8"?>
<root>
  <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" /><xsd:element name="comment" type="xsd:string" minOccurs="0" /></xsd:sequence><xsd:attribute name="name" type="xsd:string" use="required" /><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="resheader"><xsd:complexType><xsd:sequence><xsd:element name="value" type="xsd:string" minOccurs="0" /></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>

14. src/DemoExamTemplate/Forms/OrderEditForm.Designer.cs

namespace DemoExamTemplate.Forms;

partial class OrderEditForm
{
    private System.ComponentModel.IContainer? components = null;
    protected override void Dispose(bool disposing) { if (disposing && components != null) components.Dispose(); base.Dispose(disposing); }
    #region Windows Form Designer generated code
    private void InitializeComponent()
    {
        DataGridViewCellStyle headerStyle = new DataGridViewCellStyle();
        lblTitle = new Label(); tableRoot = new TableLayoutPanel(); tableFields = new TableLayoutPanel();
        lblNumber = new Label(); txtNumber = new TextBox(); lblDate = new Label(); dtpDate = new DateTimePicker(); lblCustomer = new Label(); txtCustomer = new TextBox(); lblStatus = new Label(); cmbStatus = new ComboBox();
        pnlAddItem = new FlowLayoutPanel(); lblProduct = new Label(); cmbProduct = new ComboBox(); lblQuantity = new Label(); nudQuantity = new NumericUpDown(); btnAddItem = new Button(); btnRemoveItem = new Button();
        dgvItems = new DataGridView(); colArticle = new DataGridViewTextBoxColumn(); colProduct = new DataGridViewTextBoxColumn(); colQuantity = new DataGridViewTextBoxColumn(); colPrice = new DataGridViewTextBoxColumn(); colDiscount = new DataGridViewTextBoxColumn(); colTotal = new DataGridViewTextBoxColumn();
        pnlButtons = new FlowLayoutPanel(); lblTotal = new Label(); btnSave = new Button(); btnCancel = new Button();
        tableRoot.SuspendLayout(); tableFields.SuspendLayout(); pnlAddItem.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)nudQuantity).BeginInit(); ((System.ComponentModel.ISupportInitialize)dgvItems).BeginInit(); pnlButtons.SuspendLayout(); SuspendLayout();
        lblTitle.Name = "lblTitle"; lblTitle.AutoSize = true; lblTitle.Font = new Font("Times New Roman", 20F, FontStyle.Bold); lblTitle.Location = new Point(20, 20); lblTitle.Text = "Добавление заказа";
        tableRoot.Name = "tableRoot"; tableRoot.ColumnCount = 1; tableRoot.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); tableRoot.Controls.Add(tableFields, 0, 0); tableRoot.Controls.Add(pnlAddItem, 0, 1); tableRoot.Controls.Add(dgvItems, 0, 2); tableRoot.Controls.Add(pnlButtons, 0, 3); tableRoot.Location = new Point(20, 70); tableRoot.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; tableRoot.Padding = new Padding(0); tableRoot.RowCount = 4; tableRoot.RowStyles.Add(new RowStyle(SizeType.Absolute, 100F)); tableRoot.RowStyles.Add(new RowStyle(SizeType.Absolute, 65F)); tableRoot.RowStyles.Add(new RowStyle(SizeType.Percent, 100F)); tableRoot.RowStyles.Add(new RowStyle(SizeType.Absolute, 55F)); tableRoot.Size = new Size(1040, 590);
        tableFields.Name = "tableFields"; tableFields.ColumnCount = 4; tableFields.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 120F)); tableFields.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F)); tableFields.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 120F)); tableFields.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F)); tableFields.Dock = DockStyle.Fill; tableFields.RowCount = 2; tableFields.RowStyles.Add(new RowStyle(SizeType.Percent, 50F)); tableFields.RowStyles.Add(new RowStyle(SizeType.Percent, 50F));
        tableFields.Controls.Add(lblNumber, 0, 0); tableFields.Controls.Add(txtNumber, 1, 0); tableFields.Controls.Add(lblDate, 2, 0); tableFields.Controls.Add(dtpDate, 3, 0); tableFields.Controls.Add(lblCustomer, 0, 1); tableFields.Controls.Add(txtCustomer, 1, 1); tableFields.Controls.Add(lblStatus, 2, 1); tableFields.Controls.Add(cmbStatus, 3, 1);
        lblNumber.Name = "lblNumber"; lblNumber.Anchor = AnchorStyles.Left; lblNumber.AutoSize = true; lblNumber.Text = "Номер*"; txtNumber.Name = "txtNumber"; txtNumber.Anchor = AnchorStyles.Left | AnchorStyles.Right; txtNumber.Size = new Size(390, 24);
        lblDate.Name = "lblDate"; lblDate.Anchor = AnchorStyles.Left; lblDate.AutoSize = true; lblDate.Text = "Дата*"; dtpDate.Name = "dtpDate"; dtpDate.Anchor = AnchorStyles.Left | AnchorStyles.Right; dtpDate.Format = DateTimePickerFormat.Short;
        lblCustomer.Name = "lblCustomer"; lblCustomer.Anchor = AnchorStyles.Left; lblCustomer.AutoSize = true; lblCustomer.Text = "Клиент*"; txtCustomer.Name = "txtCustomer"; txtCustomer.Anchor = AnchorStyles.Left | AnchorStyles.Right;
        lblStatus.Name = "lblStatus"; lblStatus.Anchor = AnchorStyles.Left; lblStatus.AutoSize = true; lblStatus.Text = "Статус*"; cmbStatus.Name = "cmbStatus"; cmbStatus.Anchor = AnchorStyles.Left | AnchorStyles.Right; cmbStatus.DropDownStyle = ComboBoxStyle.DropDownList; cmbStatus.Items.AddRange(new object[] { "Новый", "В обработке", "Завершён", "Отменён" });
        pnlAddItem.Name = "pnlAddItem"; pnlAddItem.Dock = DockStyle.Fill; pnlAddItem.Padding = new Padding(0, 10, 0, 5); pnlAddItem.WrapContents = false; pnlAddItem.Controls.AddRange(new Control[] { lblProduct, cmbProduct, lblQuantity, nudQuantity, btnAddItem, btnRemoveItem });
        lblProduct.Name = "lblProduct"; lblProduct.AutoSize = true; lblProduct.Margin = new Padding(3, 8, 3, 3); lblProduct.Text = "Товар:"; cmbProduct.Name = "cmbProduct"; cmbProduct.DropDownStyle = ComboBoxStyle.DropDownList; cmbProduct.Margin = new Padding(3, 4, 15, 3); cmbProduct.Size = new Size(330, 25);
        lblQuantity.Name = "lblQuantity"; lblQuantity.AutoSize = true; lblQuantity.Margin = new Padding(3, 8, 3, 3); lblQuantity.Text = "Количество:"; nudQuantity.Name = "nudQuantity"; nudQuantity.Margin = new Padding(3, 4, 15, 3); nudQuantity.Minimum = new decimal(new int[] { 1, 0, 0, 0 }); nudQuantity.Maximum = new decimal(new int[] { 10000, 0, 0, 0 }); nudQuantity.Value = new decimal(new int[] { 1, 0, 0, 0 }); nudQuantity.Size = new Size(90, 24);
        btnAddItem.Name = "btnAddItem"; btnAddItem.BackColor = Color.MediumSpringGreen; btnAddItem.FlatStyle = FlatStyle.Flat; btnAddItem.Margin = new Padding(3, 0, 10, 0); btnAddItem.Size = new Size(130, 38); btnAddItem.Text = "Добавить"; btnAddItem.UseVisualStyleBackColor = false; btnAddItem.Click += btnAddItem_Click;
        btnRemoveItem.Name = "btnRemoveItem"; btnRemoveItem.BackColor = Color.Chartreuse; btnRemoveItem.FlatStyle = FlatStyle.Flat; btnRemoveItem.Margin = new Padding(3, 0, 0, 0); btnRemoveItem.Size = new Size(130, 38); btnRemoveItem.Text = "Удалить"; btnRemoveItem.UseVisualStyleBackColor = false; btnRemoveItem.Click += btnRemoveItem_Click;
        dgvItems.Name = "dgvItems"; dgvItems.AllowUserToAddRows = false; dgvItems.AllowUserToDeleteRows = false; dgvItems.AutoGenerateColumns = false; dgvItems.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill; dgvItems.BackgroundColor = Color.White; headerStyle.BackColor = Color.Chartreuse; headerStyle.Font = new Font("Times New Roman", 11F, FontStyle.Bold); dgvItems.ColumnHeadersDefaultCellStyle = headerStyle; dgvItems.ColumnHeadersHeight = 40; dgvItems.EnableHeadersVisualStyles = false; dgvItems.Dock = DockStyle.Fill; dgvItems.ReadOnly = true; dgvItems.RowHeadersVisible = false; dgvItems.SelectionMode = DataGridViewSelectionMode.FullRowSelect; dgvItems.Columns.AddRange(new DataGridViewColumn[] { colArticle, colProduct, colQuantity, colPrice, colDiscount, colTotal });
        colArticle.Name = "colArticle"; colArticle.DataPropertyName = "Article"; colArticle.HeaderText = "Артикул"; colProduct.Name = "colProduct"; colProduct.DataPropertyName = "ProductName"; colProduct.HeaderText = "Товар"; colQuantity.Name = "colQuantity"; colQuantity.DataPropertyName = "Quantity"; colQuantity.HeaderText = "Количество"; colPrice.Name = "colPrice"; colPrice.DataPropertyName = "Price"; colPrice.HeaderText = "Цена"; colPrice.DefaultCellStyle.Format = "N2"; colDiscount.Name = "colDiscount"; colDiscount.DataPropertyName = "Discount"; colDiscount.HeaderText = "Скидка, %"; colTotal.Name = "colTotal"; colTotal.DataPropertyName = "LineTotal"; colTotal.HeaderText = "Сумма"; colTotal.DefaultCellStyle.Format = "N2";
        pnlButtons.Name = "pnlButtons"; pnlButtons.Dock = DockStyle.Fill; pnlButtons.FlowDirection = FlowDirection.RightToLeft; pnlButtons.Padding = new Padding(0, 7, 0, 0); pnlButtons.WrapContents = false; pnlButtons.Controls.AddRange(new Control[] { btnCancel, btnSave, lblTotal });
        btnCancel.Name = "btnCancel"; btnCancel.BackColor = Color.Chartreuse; btnCancel.FlatStyle = FlatStyle.Flat; btnCancel.Margin = new Padding(5, 0, 0, 0); btnCancel.Size = new Size(130, 38); btnCancel.Text = "Отмена"; btnCancel.UseVisualStyleBackColor = false; btnCancel.Click += btnCancel_Click;
        btnSave.Name = "btnSave"; btnSave.BackColor = Color.MediumSpringGreen; btnSave.FlatStyle = FlatStyle.Flat; btnSave.Margin = new Padding(5, 0, 5, 0); btnSave.Size = new Size(130, 38); btnSave.Text = "Сохранить"; btnSave.UseVisualStyleBackColor = false; btnSave.Click += btnSave_Click;
        lblTotal.Name = "lblTotal"; lblTotal.AutoSize = true; lblTotal.Font = new Font("Times New Roman", 13F, FontStyle.Bold); lblTotal.Margin = new Padding(10, 10, 20, 0); lblTotal.Text = "Итого: 0,00";
        AutoScaleDimensions = new SizeF(8F, 17F); AutoScaleMode = AutoScaleMode.Font; BackColor = Color.White; ClientSize = new Size(1080, 680); Controls.Add(tableRoot); Controls.Add(lblTitle); Font = new Font("Times New Roman", 11F); MinimumSize = new Size(950, 650); Name = "OrderEditForm"; StartPosition = FormStartPosition.CenterParent; Text = "Добавление заказа"; Shown += OrderEditForm_Shown;
        tableRoot.ResumeLayout(false); tableFields.ResumeLayout(false); tableFields.PerformLayout(); pnlAddItem.ResumeLayout(false); pnlAddItem.PerformLayout(); ((System.ComponentModel.ISupportInitialize)nudQuantity).EndInit(); ((System.ComponentModel.ISupportInitialize)dgvItems).EndInit(); pnlButtons.ResumeLayout(false); pnlButtons.PerformLayout(); ResumeLayout(false); PerformLayout();
    }
    #endregion
    private Label lblTitle; private TableLayoutPanel tableRoot; private TableLayoutPanel tableFields; private Label lblNumber; private TextBox txtNumber; private Label lblDate; private DateTimePicker dtpDate; private Label lblCustomer; private TextBox txtCustomer; private Label lblStatus; private ComboBox cmbStatus; private FlowLayoutPanel pnlAddItem; private Label lblProduct; private ComboBox cmbProduct; private Label lblQuantity; private NumericUpDown nudQuantity; private Button btnAddItem; private Button btnRemoveItem; private DataGridView dgvItems; private DataGridViewTextBoxColumn colArticle; private DataGridViewTextBoxColumn colProduct; private DataGridViewTextBoxColumn colQuantity; private DataGridViewTextBoxColumn colPrice; private DataGridViewTextBoxColumn colDiscount; private DataGridViewTextBoxColumn colTotal; private FlowLayoutPanel pnlButtons; private Label lblTotal; private Button btnSave; private Button btnCancel;
}

15. src/DemoExamTemplate/Forms/OrderEditForm.cs

using DemoExamTemplate.Models;
using DemoExamTemplate.Services;
using DemoExamTemplate.UI;

namespace DemoExamTemplate.Forms;

public partial class OrderEditForm : Form
{
    private readonly int _orderId;
    private readonly ProductService _productService = new ProductService();
    private readonly OrderService _orderService = new OrderService();
    private readonly BindingSource _itemsSource = new BindingSource();
    private readonly List<OrderItemData> _items = new List<OrderItemData>();

    public OrderEditForm(int orderId = 0)
    {
        _orderId = orderId;
        InitializeComponent();
        Text = orderId == 0 ? "Добавление заказа" : "Редактирование заказа";
        lblTitle.Text = Text;
        _itemsSource.DataSource = _items;
        dgvItems.DataSource = _itemsSource;
    }

    private async void OrderEditForm_Shown(object? sender, EventArgs e)
    {
        try
        {
            cmbProduct.DataSource = await _productService.GetProductsAsync(null, null, "NAME_ASC");
            cmbProduct.DisplayMember = nameof(Product.Name);
            cmbStatus.SelectedIndex = 0;
            if (_orderId == 0) return;

            OrderData? order = await _orderService.GetByIdAsync(_orderId);
            if (order is null) return;
            txtNumber.Text = order.Number;
            dtpDate.Value = order.OrderDate;
            txtCustomer.Text = order.Customer;
            cmbStatus.SelectedItem = order.Status;
            _items.AddRange(order.Items);
            RefreshItems();
        }
        catch (Exception ex) { UiHelper.ShowError(ex); }
    }

    private void btnAddItem_Click(object? sender, EventArgs e)
    {
        if (cmbProduct.SelectedItem is not Product product) return;
        OrderItemData? existing = _items.FirstOrDefault(x => x.ProductId == product.Id);
        if (existing is null)
        {
            _items.Add(new OrderItemData
            {
                ProductId = product.Id, Article = product.Article, ProductName = product.Name,
                Quantity = (int)nudQuantity.Value, Price = product.Price, Discount = product.Discount
            });
        }
        else existing.Quantity += (int)nudQuantity.Value;
        RefreshItems();
    }

    private void btnRemoveItem_Click(object? sender, EventArgs e)
    {
        if (dgvItems.CurrentRow?.DataBoundItem is OrderItemData item)
        {
            _items.Remove(item);
            RefreshItems();
        }
    }

    private void RefreshItems()
    {
        _itemsSource.ResetBindings(false);
        lblTotal.Text = "Итого: " + _items.Sum(x => x.LineTotal).ToString("N2");
    }

    private async void btnSave_Click(object? sender, EventArgs e)
    {
        if (string.IsNullOrWhiteSpace(txtNumber.Text) || string.IsNullOrWhiteSpace(txtCustomer.Text) || cmbStatus.SelectedItem is null || _items.Count == 0)
        {
            MessageBox.Show("Заполните поля и добавьте хотя бы один товар.");
            return;
        }
        try
        {
            await _orderService.SaveAsync(new OrderData
            {
                Id = _orderId, Number = txtNumber.Text, OrderDate = dtpDate.Value,
                Customer = txtCustomer.Text, Status = cmbStatus.SelectedItem.ToString()!, Items = _items
            });
            DialogResult = DialogResult.OK;
            Close();
        }
        catch (Exception ex) { UiHelper.ShowError(ex); }
    }

    private void btnCancel_Click(object? sender, EventArgs e) => Close();
}

16. src/DemoExamTemplate/Forms/OrderEditForm.resx

<?xml version="1.0" encoding="utf-8"?>
<root>
  <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" /><xsd:element name="comment" type="xsd:string" minOccurs="0" /></xsd:sequence><xsd:attribute name="name" type="xsd:string" use="required" /><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="resheader"><xsd:complexType><xsd:sequence><xsd:element name="value" type="xsd:string" minOccurs="0" /></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>

17. src/DemoExamTemplate/Forms/OrdersForm.Designer.cs

namespace DemoExamTemplate.Forms;

partial class OrdersForm
{
    private System.ComponentModel.IContainer? components = null;
    protected override void Dispose(bool disposing) { if (disposing && components != null) components.Dispose(); base.Dispose(disposing); }
    #region Windows Form Designer generated code
    private void InitializeComponent()
    {
        DataGridViewCellStyle headerStyle = new DataGridViewCellStyle();
        lblTitle = new Label(); btnAdd = new Button(); btnEdit = new Button(); btnDelete = new Button(); dgvOrders = new DataGridView();
        colNumber = new DataGridViewTextBoxColumn(); colDate = new DataGridViewTextBoxColumn(); colCustomer = new DataGridViewTextBoxColumn(); colStatus = new DataGridViewTextBoxColumn(); colTotal = new DataGridViewTextBoxColumn();
        ((System.ComponentModel.ISupportInitialize)dgvOrders).BeginInit(); SuspendLayout();
        lblTitle.Name = "lblTitle"; lblTitle.AutoSize = true; lblTitle.Font = new Font("Times New Roman", 20F, FontStyle.Bold); lblTitle.Location = new Point(20, 22); lblTitle.Text = "Заказы";
        btnAdd.Name = "btnAdd"; btnAdd.Anchor = AnchorStyles.Top | AnchorStyles.Right; btnAdd.BackColor = Color.MediumSpringGreen; btnAdd.FlatStyle = FlatStyle.Flat; btnAdd.Location = new Point(565, 20); btnAdd.Size = new Size(130, 38); btnAdd.Text = "Добавить"; btnAdd.UseVisualStyleBackColor = false; btnAdd.Click += btnAdd_Click;
        btnEdit.Name = "btnEdit"; btnEdit.Anchor = AnchorStyles.Top | AnchorStyles.Right; btnEdit.BackColor = Color.Chartreuse; btnEdit.FlatStyle = FlatStyle.Flat; btnEdit.Location = new Point(705, 20); btnEdit.Size = new Size(130, 38); btnEdit.Text = "Редактировать"; btnEdit.UseVisualStyleBackColor = false; btnEdit.Click += btnEdit_Click;
        btnDelete.Name = "btnDelete"; btnDelete.Anchor = AnchorStyles.Top | AnchorStyles.Right; btnDelete.BackColor = Color.Chartreuse; btnDelete.FlatStyle = FlatStyle.Flat; btnDelete.Location = new Point(845, 20); btnDelete.Size = new Size(130, 38); btnDelete.Text = "Удалить"; btnDelete.UseVisualStyleBackColor = false; btnDelete.Click += btnDelete_Click;
        dgvOrders.Name = "dgvOrders"; dgvOrders.AllowUserToAddRows = false; dgvOrders.AllowUserToDeleteRows = false; dgvOrders.AutoGenerateColumns = false; dgvOrders.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill; dgvOrders.BackgroundColor = Color.White;
        headerStyle.BackColor = Color.Chartreuse; headerStyle.Font = new Font("Times New Roman", 11F, FontStyle.Bold); dgvOrders.ColumnHeadersDefaultCellStyle = headerStyle; dgvOrders.ColumnHeadersHeight = 42; dgvOrders.EnableHeadersVisualStyles = false;
        dgvOrders.Columns.AddRange(new DataGridViewColumn[] { colNumber, colDate, colCustomer, colStatus, colTotal }); dgvOrders.Location = new Point(20, 78); dgvOrders.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; dgvOrders.MultiSelect = false; dgvOrders.ReadOnly = true; dgvOrders.RowHeadersVisible = false; dgvOrders.SelectionMode = DataGridViewSelectionMode.FullRowSelect; dgvOrders.Size = new Size(955, 500);
        colNumber.Name = "colNumber"; colNumber.DataPropertyName = "Number"; colNumber.HeaderText = "Номер"; colDate.Name = "colDate"; colDate.DataPropertyName = "OrderDate"; colDate.HeaderText = "Дата"; colDate.DefaultCellStyle.Format = "d"; colCustomer.Name = "colCustomer"; colCustomer.DataPropertyName = "Customer"; colCustomer.HeaderText = "Клиент"; colStatus.Name = "colStatus"; colStatus.DataPropertyName = "Status"; colStatus.HeaderText = "Статус"; colTotal.Name = "colTotal"; colTotal.DataPropertyName = "Total"; colTotal.HeaderText = "Сумма"; colTotal.DefaultCellStyle.Format = "N2";
        AutoScaleDimensions = new SizeF(8F, 17F); AutoScaleMode = AutoScaleMode.Font; BackColor = Color.White; ClientSize = new Size(995, 600); Controls.Add(dgvOrders); Controls.Add(btnDelete); Controls.Add(btnEdit); Controls.Add(btnAdd); Controls.Add(lblTitle); Font = new Font("Times New Roman", 11F); MinimumSize = new Size(850, 550); Name = "OrdersForm"; StartPosition = FormStartPosition.CenterParent; Text = "Заказы"; Shown += OrdersForm_Shown;
        ((System.ComponentModel.ISupportInitialize)dgvOrders).EndInit(); ResumeLayout(false); PerformLayout();
    }
    #endregion
    private Label lblTitle; private Button btnAdd; private Button btnEdit; private Button btnDelete; private DataGridView dgvOrders; private DataGridViewTextBoxColumn colNumber; private DataGridViewTextBoxColumn colDate; private DataGridViewTextBoxColumn colCustomer; private DataGridViewTextBoxColumn colStatus; private DataGridViewTextBoxColumn colTotal;
}

18. src/DemoExamTemplate/Forms/OrdersForm.cs

using DemoExamTemplate.Models;
using DemoExamTemplate.Services;
using DemoExamTemplate.UI;

namespace DemoExamTemplate.Forms;

public partial class OrdersForm : Form
{
    private readonly UserSession _session;
    private readonly OrderService _service = new OrderService();

    public OrdersForm(UserSession session)
    {
        _session = session;
        InitializeComponent();
        btnAdd.Visible = session.CanEditOrders;
        btnEdit.Visible = session.CanEditOrders;
        btnDelete.Visible = session.CanEditOrders;
    }

    private async void OrdersForm_Shown(object? sender, EventArgs e) => await LoadSafeAsync();

    private async Task LoadSafeAsync()
    {
        try { dgvOrders.DataSource = await _service.GetOrdersAsync(); }
        catch (Exception ex) { UiHelper.ShowError(ex); }
    }

    private OrderListItem? SelectedOrder => dgvOrders.CurrentRow?.DataBoundItem as OrderListItem;

    private async void btnAdd_Click(object? sender, EventArgs e)
    {
        using OrderEditForm form = new OrderEditForm();
        if (form.ShowDialog(this) == DialogResult.OK) await LoadSafeAsync();
    }

    private async void btnEdit_Click(object? sender, EventArgs e)
    {
        OrderListItem? order = SelectedOrder;
        if (order is null) { MessageBox.Show("Выберите заказ."); return; }
        using OrderEditForm form = new OrderEditForm(order.Id);
        if (form.ShowDialog(this) == DialogResult.OK) await LoadSafeAsync();
    }

    private async void btnDelete_Click(object? sender, EventArgs e)
    {
        OrderListItem? order = SelectedOrder;
        if (order is null) { MessageBox.Show("Выберите заказ."); return; }
        if (MessageBox.Show("Удалить выбранный заказ?", "Удаление", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes) return;
        try { await _service.DeleteAsync(order.Id); await LoadSafeAsync(); }
        catch (Exception ex) { UiHelper.ShowError(ex); }
    }
}

19. src/DemoExamTemplate/Forms/OrdersForm.resx

<?xml version="1.0" encoding="utf-8"?>
<root>
  <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" /><xsd:element name="comment" type="xsd:string" minOccurs="0" /></xsd:sequence><xsd:attribute name="name" type="xsd:string" use="required" /><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="resheader"><xsd:complexType><xsd:sequence><xsd:element name="value" type="xsd:string" minOccurs="0" /></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>

20. src/DemoExamTemplate/Forms/ProductEditForm.Designer.cs

namespace DemoExamTemplate.Forms;

partial class ProductEditForm
{
    private System.ComponentModel.IContainer? components = null;
    protected override void Dispose(bool disposing) { if (disposing && components != null) components.Dispose(); base.Dispose(disposing); }
    #region Windows Form Designer generated code
    private void InitializeComponent()
    {
        lblTitle = new Label(); lblArticle = new Label(); txtArticle = new TextBox(); lblName = new Label(); txtName = new TextBox();
        lblCategory = new Label(); cmbCategory = new ComboBox(); lblPrice = new Label(); nudPrice = new NumericUpDown();
        lblDiscount = new Label(); nudDiscount = new NumericUpDown(); lblQuantity = new Label(); nudQuantity = new NumericUpDown();
        btnSave = new Button(); btnCancel = new Button();
        ((System.ComponentModel.ISupportInitialize)nudPrice).BeginInit(); ((System.ComponentModel.ISupportInitialize)nudDiscount).BeginInit(); ((System.ComponentModel.ISupportInitialize)nudQuantity).BeginInit(); SuspendLayout();
        lblTitle.AutoSize = true; lblTitle.Font = new Font("Times New Roman", 18F, FontStyle.Bold); lblTitle.Location = new Point(30, 25); lblTitle.Name = "lblTitle"; lblTitle.Text = "Добавление товара";
        lblArticle.AutoSize = true; lblArticle.Name = "lblArticle"; lblArticle.Location = new Point(30, 82); lblArticle.Text = "Артикул*";
        txtArticle.Name = "txtArticle"; txtArticle.Location = new Point(180, 78); txtArticle.Size = new Size(300, 24);
        lblName.AutoSize = true; lblName.Name = "lblName"; lblName.Location = new Point(30, 125); lblName.Text = "Наименование*";
        txtName.Name = "txtName"; txtName.Location = new Point(180, 121); txtName.Size = new Size(300, 24);
        lblCategory.AutoSize = true; lblCategory.Name = "lblCategory"; lblCategory.Location = new Point(30, 168); lblCategory.Text = "Категория*";
        cmbCategory.Name = "cmbCategory"; cmbCategory.DropDownStyle = ComboBoxStyle.DropDownList; cmbCategory.DisplayMember = "Name"; cmbCategory.ValueMember = "Id"; cmbCategory.Location = new Point(180, 164); cmbCategory.Size = new Size(300, 25);
        lblPrice.AutoSize = true; lblPrice.Name = "lblPrice"; lblPrice.Location = new Point(30, 211); lblPrice.Text = "Цена*";
        nudPrice.Name = "nudPrice"; nudPrice.DecimalPlaces = 2; nudPrice.Maximum = new decimal(new int[] { 10000000, 0, 0, 0 }); nudPrice.Minimum = new decimal(new int[] { 1, 0, 0, 0 }); nudPrice.Location = new Point(180, 207); nudPrice.Size = new Size(300, 24); nudPrice.Value = new decimal(new int[] { 1, 0, 0, 0 });
        lblDiscount.AutoSize = true; lblDiscount.Name = "lblDiscount"; lblDiscount.Location = new Point(30, 254); lblDiscount.Text = "Скидка, %";
        nudDiscount.Name = "nudDiscount"; nudDiscount.Maximum = new decimal(new int[] { 100, 0, 0, 0 }); nudDiscount.Location = new Point(180, 250); nudDiscount.Size = new Size(300, 24);
        lblQuantity.AutoSize = true; lblQuantity.Name = "lblQuantity"; lblQuantity.Location = new Point(30, 297); lblQuantity.Text = "Количество";
        nudQuantity.Name = "nudQuantity"; nudQuantity.Maximum = new decimal(new int[] { 100000, 0, 0, 0 }); nudQuantity.Location = new Point(180, 293); nudQuantity.Size = new Size(300, 24);
        btnSave.Name = "btnSave"; btnSave.BackColor = Color.MediumSpringGreen; btnSave.FlatStyle = FlatStyle.Flat; btnSave.Location = new Point(180, 350); btnSave.Size = new Size(140, 40); btnSave.Text = "Сохранить"; btnSave.UseVisualStyleBackColor = false; btnSave.Click += btnSave_Click;
        btnCancel.Name = "btnCancel"; btnCancel.BackColor = Color.Chartreuse; btnCancel.FlatStyle = FlatStyle.Flat; btnCancel.Location = new Point(340, 350); btnCancel.Size = new Size(140, 40); btnCancel.Text = "Отмена"; btnCancel.UseVisualStyleBackColor = false; btnCancel.Click += btnCancel_Click;
        AutoScaleDimensions = new SizeF(8F, 17F); AutoScaleMode = AutoScaleMode.Font; BackColor = Color.White; ClientSize = new Size(530, 430);
        Controls.AddRange(new Control[] { lblTitle, lblArticle, txtArticle, lblName, txtName, lblCategory, cmbCategory, lblPrice, nudPrice, lblDiscount, nudDiscount, lblQuantity, nudQuantity, btnSave, btnCancel });
        Font = new Font("Times New Roman", 11F); FormBorderStyle = FormBorderStyle.FixedDialog; MaximizeBox = false; MinimizeBox = false; Name = "ProductEditForm"; StartPosition = FormStartPosition.CenterParent; Text = "Добавление товара"; Shown += ProductEditForm_Shown;
        ((System.ComponentModel.ISupportInitialize)nudPrice).EndInit(); ((System.ComponentModel.ISupportInitialize)nudDiscount).EndInit(); ((System.ComponentModel.ISupportInitialize)nudQuantity).EndInit(); ResumeLayout(false); PerformLayout();
    }
    #endregion
    private Label lblTitle; private Label lblArticle; private TextBox txtArticle; private Label lblName; private TextBox txtName; private Label lblCategory; private ComboBox cmbCategory; private Label lblPrice; private NumericUpDown nudPrice; private Label lblDiscount; private NumericUpDown nudDiscount; private Label lblQuantity; private NumericUpDown nudQuantity; private Button btnSave; private Button btnCancel;
}

21. src/DemoExamTemplate/Forms/ProductEditForm.cs

using DemoExamTemplate.Models;
using DemoExamTemplate.Services;
using DemoExamTemplate.UI;

namespace DemoExamTemplate.Forms;

public partial class ProductEditForm : Form
{
    private readonly int _productId;
    private readonly ProductService _service = new ProductService();

    public ProductEditForm(int productId = 0)
    {
        _productId = productId;
        InitializeComponent();
        Text = productId == 0 ? "Добавление товара" : "Редактирование товара";
        lblTitle.Text = Text;
    }

    private async void ProductEditForm_Shown(object? sender, EventArgs e)
    {
        try
        {
            cmbCategory.DataSource = await _service.GetCategoriesAsync();
            if (_productId == 0) return;
            Product? product = await _service.GetByIdAsync(_productId);
            if (product is null) return;
            txtArticle.Text = product.Article;
            txtName.Text = product.Name;
            cmbCategory.SelectedValue = product.CategoryId;
            nudPrice.Value = product.Price;
            nudDiscount.Value = product.Discount;
            nudQuantity.Value = product.Quantity;
        }
        catch (Exception ex) { UiHelper.ShowError(ex); }
    }

    private async void btnSave_Click(object? sender, EventArgs e)
    {
        if (string.IsNullOrWhiteSpace(txtArticle.Text) || string.IsNullOrWhiteSpace(txtName.Text) || cmbCategory.SelectedItem is not Category category)
        {
            MessageBox.Show("Заполните обязательные поля.");
            return;
        }
        try
        {
            await _service.SaveAsync(new Product
            {
                Id = _productId,
                Article = txtArticle.Text,
                Name = txtName.Text,
                CategoryId = category.Id,
                Price = nudPrice.Value,
                Discount = (int)nudDiscount.Value,
                Quantity = (int)nudQuantity.Value
            });
            DialogResult = DialogResult.OK;
            Close();
        }
        catch (Exception ex) { UiHelper.ShowError(ex); }
    }

    private void btnCancel_Click(object? sender, EventArgs e) => Close();
}

22. src/DemoExamTemplate/Forms/ProductEditForm.resx

<?xml version="1.0" encoding="utf-8"?>
<root>
  <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" /><xsd:element name="comment" type="xsd:string" minOccurs="0" /></xsd:sequence><xsd:attribute name="name" type="xsd:string" use="required" /><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="resheader"><xsd:complexType><xsd:sequence><xsd:element name="value" type="xsd:string" minOccurs="0" /></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>

23. src/DemoExamTemplate/Models/Category.cs

namespace DemoExamTemplate.Models;

public sealed class Category
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public override string ToString() => Name;
}

24. src/DemoExamTemplate/Models/OrderModels.cs

namespace DemoExamTemplate.Models;

public sealed class OrderListItem
{
    public int Id { get; set; }
    public string Number { get; set; } = string.Empty;
    public DateTime OrderDate { get; set; }
    public string Customer { get; set; } = string.Empty;
    public string Status { get; set; } = string.Empty;
    public decimal Total { get; set; }
}

public sealed class OrderData
{
    public int Id { get; set; }
    public string Number { get; set; } = string.Empty;
    public DateTime OrderDate { get; set; } = DateTime.Today;
    public string Customer { get; set; } = string.Empty;
    public string Status { get; set; } = "Новый";
    public List<OrderItemData> Items { get; set; } = new List<OrderItemData>();
}

public sealed class OrderItemData
{
    public int ProductId { get; set; }
    public string Article { get; set; } = string.Empty;
    public string ProductName { get; set; } = string.Empty;
    public int Quantity { get; set; }
    public decimal Price { get; set; }
    public int Discount { get; set; }
    public decimal LineTotal => Quantity * Price * (100 - Discount) / 100m;
}

25. src/DemoExamTemplate/Models/Product.cs

namespace DemoExamTemplate.Models;

public sealed class Product
{
    public int Id { get; set; }
    public string Article { get; set; } = string.Empty;
    public string Name { get; set; } = string.Empty;
    public int CategoryId { get; set; }
    public string CategoryName { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public int Discount { get; set; }
    public int Quantity { get; set; }
}

26. src/DemoExamTemplate/Models/UserSession.cs

namespace DemoExamTemplate.Models;

public sealed class UserSession
{
    public string FullName { get; init; } = "Гость";
    public string RoleCode { get; init; } = "GUEST";
    public string RoleName { get; init; } = "Гость";

    public bool CanUseProductTools => RoleCode is "MANAGER" or "ADMIN";
    public bool CanEditProducts => RoleCode == "ADMIN";
    public bool CanViewOrders => RoleCode is "MANAGER" or "ADMIN";
    public bool CanEditOrders => RoleCode == "ADMIN";

    public static UserSession Guest()
    {
        return new UserSession();
    }
}

27. src/DemoExamTemplate/Program.cs

using DemoExamTemplate.Forms;

namespace DemoExamTemplate;

internal static class Program
{
    [STAThread]
    private static void Main()
    {
        ApplicationConfiguration.Initialize();
        Application.Run(new LoginForm());
    }
}

28. src/DemoExamTemplate/Services/AuthService.cs

using DemoExamTemplate.Data;
using DemoExamTemplate.Models;
using Microsoft.Data.SqlClient;

namespace DemoExamTemplate.Services;

public sealed class AuthService
{
    public async Task<UserSession?> LoginAsync(string login, string password)
    {
        const string sql = """
            SELECT u.FullName, r.Code, r.Name
            FROM dbo.Users u
            INNER JOIN dbo.Roles r ON r.Id = u.RoleId
            WHERE u.Login = @Login AND u.Password = @Password;
            """;

        await using SqlConnection connection = Db.CreateConnection();
        await connection.OpenAsync();
        await using SqlCommand command = new SqlCommand(sql, connection);
        command.Parameters.AddWithValue("@Login", login.Trim());
        command.Parameters.AddWithValue("@Password", password);
        await using SqlDataReader reader = await command.ExecuteReaderAsync();

        if (!await reader.ReadAsync()) return null;

        return new UserSession
        {
            FullName = reader.GetString(0),
            RoleCode = reader.GetString(1),
            RoleName = reader.GetString(2)
        };
    }
}

29. src/DemoExamTemplate/Services/OrderService.cs

using DemoExamTemplate.Data;
using DemoExamTemplate.Models;
using Microsoft.Data.SqlClient;

namespace DemoExamTemplate.Services;

public sealed class OrderService
{
    public async Task<List<OrderListItem>> GetOrdersAsync()
    {
        const string sql = """
            SELECT o.Id, o.Number, o.OrderDate, o.Customer, o.Status,
                   COALESCE(SUM(oi.Quantity * oi.Price * (100 - oi.Discount) / 100.0), 0) AS Total
            FROM dbo.Orders o
            LEFT JOIN dbo.OrderItems oi ON oi.OrderId = o.Id
            GROUP BY o.Id, o.Number, o.OrderDate, o.Customer, o.Status
            ORDER BY o.OrderDate DESC, o.Id DESC;
            """;
        List<OrderListItem> orders = new List<OrderListItem>();
        await using SqlConnection connection = Db.CreateConnection();
        await connection.OpenAsync();
        await using SqlCommand command = new SqlCommand(sql, connection);
        await using SqlDataReader reader = await command.ExecuteReaderAsync();
        while (await reader.ReadAsync())
        {
            orders.Add(new OrderListItem
            {
                Id = reader.GetInt32(0),
                Number = reader.GetString(1),
                OrderDate = reader.GetDateTime(2),
                Customer = reader.GetString(3),
                Status = reader.GetString(4),
                Total = reader.GetDecimal(5)
            });
        }
        return orders;
    }

    public async Task<OrderData?> GetByIdAsync(int id)
    {
        const string orderSql = "SELECT Id, Number, OrderDate, Customer, Status FROM dbo.Orders WHERE Id=@Id;";
        const string itemsSql = """
            SELECT oi.ProductId, p.Article, p.Name, oi.Quantity, oi.Price, oi.Discount
            FROM dbo.OrderItems oi
            INNER JOIN dbo.Products p ON p.Id = oi.ProductId
            WHERE oi.OrderId=@Id;
            """;

        await using SqlConnection connection = Db.CreateConnection();
        await connection.OpenAsync();
        OrderData? order;
        await using (SqlCommand command = new SqlCommand(orderSql, connection))
        {
            command.Parameters.AddWithValue("@Id", id);
            await using SqlDataReader reader = await command.ExecuteReaderAsync();
            if (!await reader.ReadAsync()) return null;
            order = new OrderData
            {
                Id = reader.GetInt32(0), Number = reader.GetString(1), OrderDate = reader.GetDateTime(2),
                Customer = reader.GetString(3), Status = reader.GetString(4)
            };
        }

        await using (SqlCommand command = new SqlCommand(itemsSql, connection))
        {
            command.Parameters.AddWithValue("@Id", id);
            await using SqlDataReader reader = await command.ExecuteReaderAsync();
            while (await reader.ReadAsync())
            {
                order.Items.Add(new OrderItemData
                {
                    ProductId = reader.GetInt32(0), Article = reader.GetString(1), ProductName = reader.GetString(2),
                    Quantity = reader.GetInt32(3), Price = reader.GetDecimal(4), Discount = reader.GetInt32(5)
                });
            }
        }
        return order;
    }

    public async Task SaveAsync(OrderData order)
    {
        await using SqlConnection connection = Db.CreateConnection();
        await connection.OpenAsync();
        await using SqlTransaction transaction = (SqlTransaction)await connection.BeginTransactionAsync();
        try
        {
            int orderId = order.Id;
            if (orderId == 0)
            {
                const string insertOrder = """
                    INSERT INTO dbo.Orders(Number, OrderDate, Customer, Status)
                    OUTPUT INSERTED.Id VALUES(@Number, @OrderDate, @Customer, @Status);
                    """;
                await using SqlCommand command = new SqlCommand(insertOrder, connection, transaction);
                AddOrderParameters(command, order);
                orderId = Convert.ToInt32(await command.ExecuteScalarAsync());
            }
            else
            {
                const string updateOrder = """
                    UPDATE dbo.Orders SET Number=@Number, OrderDate=@OrderDate,
                        Customer=@Customer, Status=@Status WHERE Id=@Id;
                    """;
                await using SqlCommand command = new SqlCommand(updateOrder, connection, transaction);
                AddOrderParameters(command, order);
                command.Parameters.AddWithValue("@Id", orderId);
                await command.ExecuteNonQueryAsync();

                await using SqlCommand deleteItems = new SqlCommand("DELETE FROM dbo.OrderItems WHERE OrderId=@Id;", connection, transaction);
                deleteItems.Parameters.AddWithValue("@Id", orderId);
                await deleteItems.ExecuteNonQueryAsync();
            }

            const string insertItem = """
                INSERT INTO dbo.OrderItems(OrderId, ProductId, Quantity, Price, Discount)
                VALUES(@OrderId, @ProductId, @Quantity, @Price, @Discount);
                """;
            foreach (OrderItemData item in order.Items)
            {
                await using SqlCommand command = new SqlCommand(insertItem, connection, transaction);
                command.Parameters.AddWithValue("@OrderId", orderId);
                command.Parameters.AddWithValue("@ProductId", item.ProductId);
                command.Parameters.AddWithValue("@Quantity", item.Quantity);
                command.Parameters.AddWithValue("@Price", item.Price);
                command.Parameters.AddWithValue("@Discount", item.Discount);
                await command.ExecuteNonQueryAsync();
            }
            await transaction.CommitAsync();
        }
        catch
        {
            await transaction.RollbackAsync();
            throw;
        }
    }

    public async Task DeleteAsync(int id)
    {
        await using SqlConnection connection = Db.CreateConnection();
        await connection.OpenAsync();
        await using SqlCommand command = new SqlCommand("DELETE FROM dbo.Orders WHERE Id=@Id;", connection);
        command.Parameters.AddWithValue("@Id", id);
        await command.ExecuteNonQueryAsync();
    }

    private static void AddOrderParameters(SqlCommand command, OrderData order)
    {
        command.Parameters.AddWithValue("@Number", order.Number.Trim());
        command.Parameters.AddWithValue("@OrderDate", order.OrderDate.Date);
        command.Parameters.AddWithValue("@Customer", order.Customer.Trim());
        command.Parameters.AddWithValue("@Status", order.Status);
    }
}

30. src/DemoExamTemplate/Services/ProductService.cs

using System.Text;
using DemoExamTemplate.Data;
using DemoExamTemplate.Models;
using Microsoft.Data.SqlClient;

namespace DemoExamTemplate.Services;

public sealed class ProductService
{
    public async Task<List<Product>> GetProductsAsync(string? search, int? categoryId, string sortCode)
    {
        StringBuilder sql = new StringBuilder("""
            SELECT p.Id, p.Article, p.Name, p.CategoryId, c.Name AS CategoryName,
                   p.Price, p.Discount, p.Quantity
            FROM dbo.Products p
            INNER JOIN dbo.Categories c ON c.Id = p.CategoryId
            WHERE 1 = 1
            """);

        List<SqlParameter> parameters = new List<SqlParameter>();
        if (!string.IsNullOrWhiteSpace(search))
        {
            sql.AppendLine("AND (p.Article LIKE @Search OR p.Name LIKE @Search)");
            parameters.Add(new SqlParameter("@Search", "%" + search.Trim() + "%"));
        }
        if (categoryId.HasValue)
        {
            sql.AppendLine("AND p.CategoryId = @CategoryId");
            parameters.Add(new SqlParameter("@CategoryId", categoryId.Value));
        }

        string orderBy = sortCode switch
        {
            "NAME_DESC" => "p.Name DESC",
            "PRICE_ASC" => "p.Price ASC",
            "PRICE_DESC" => "p.Price DESC",
            _ => "p.Name ASC"
        };
        sql.AppendLine("ORDER BY " + orderBy + ";");

        List<Product> products = new List<Product>();
        await using SqlConnection connection = Db.CreateConnection();
        await connection.OpenAsync();
        await using SqlCommand command = new SqlCommand(sql.ToString(), connection);
        command.Parameters.AddRange(parameters.ToArray());
        await using SqlDataReader reader = await command.ExecuteReaderAsync();
        while (await reader.ReadAsync()) products.Add(Map(reader));
        return products;
    }

    public async Task<Product?> GetByIdAsync(int id)
    {
        const string sql = """
            SELECT p.Id, p.Article, p.Name, p.CategoryId, c.Name AS CategoryName,
                   p.Price, p.Discount, p.Quantity
            FROM dbo.Products p
            INNER JOIN dbo.Categories c ON c.Id = p.CategoryId
            WHERE p.Id = @Id;
            """;
        await using SqlConnection connection = Db.CreateConnection();
        await connection.OpenAsync();
        await using SqlCommand command = new SqlCommand(sql, connection);
        command.Parameters.AddWithValue("@Id", id);
        await using SqlDataReader reader = await command.ExecuteReaderAsync();
        return await reader.ReadAsync() ? Map(reader) : null;
    }

    public async Task<List<Category>> GetCategoriesAsync()
    {
        const string sql = "SELECT Id, Name FROM dbo.Categories ORDER BY Name;";
        List<Category> categories = new List<Category>();
        await using SqlConnection connection = Db.CreateConnection();
        await connection.OpenAsync();
        await using SqlCommand command = new SqlCommand(sql, connection);
        await using SqlDataReader reader = await command.ExecuteReaderAsync();
        while (await reader.ReadAsync())
        {
            categories.Add(new Category { Id = reader.GetInt32(0), Name = reader.GetString(1) });
        }
        return categories;
    }

    public async Task SaveAsync(Product product)
    {
        const string insertSql = """
            INSERT INTO dbo.Products(Article, Name, CategoryId, Price, Discount, Quantity)
            VALUES(@Article, @Name, @CategoryId, @Price, @Discount, @Quantity);
            """;
        const string updateSql = """
            UPDATE dbo.Products SET Article=@Article, Name=@Name, CategoryId=@CategoryId,
                Price=@Price, Discount=@Discount, Quantity=@Quantity
            WHERE Id=@Id;
            """;

        await using SqlConnection connection = Db.CreateConnection();
        await connection.OpenAsync();
        await using SqlCommand command = new SqlCommand(product.Id == 0 ? insertSql : updateSql, connection);
        command.Parameters.AddWithValue("@Article", product.Article.Trim());
        command.Parameters.AddWithValue("@Name", product.Name.Trim());
        command.Parameters.AddWithValue("@CategoryId", product.CategoryId);
        command.Parameters.AddWithValue("@Price", product.Price);
        command.Parameters.AddWithValue("@Discount", product.Discount);
        command.Parameters.AddWithValue("@Quantity", product.Quantity);
        if (product.Id != 0) command.Parameters.AddWithValue("@Id", product.Id);
        await command.ExecuteNonQueryAsync();
    }

    public async Task DeleteAsync(int id)
    {
        await using SqlConnection connection = Db.CreateConnection();
        await connection.OpenAsync();
        await using SqlCommand command = new SqlCommand("DELETE FROM dbo.Products WHERE Id=@Id;", connection);
        command.Parameters.AddWithValue("@Id", id);
        await command.ExecuteNonQueryAsync();
    }

    private static Product Map(SqlDataReader reader)
    {
        return new Product
        {
            Id = reader.GetInt32(reader.GetOrdinal("Id")),
            Article = reader.GetString(reader.GetOrdinal("Article")),
            Name = reader.GetString(reader.GetOrdinal("Name")),
            CategoryId = reader.GetInt32(reader.GetOrdinal("CategoryId")),
            CategoryName = reader.GetString(reader.GetOrdinal("CategoryName")),
            Price = reader.GetDecimal(reader.GetOrdinal("Price")),
            Discount = reader.GetInt32(reader.GetOrdinal("Discount")),
            Quantity = reader.GetInt32(reader.GetOrdinal("Quantity"))
        };
    }
}

31. src/DemoExamTemplate/UI/UiHelper.cs

namespace DemoExamTemplate.UI;

public static class UiHelper
{
    public static void LoadLogo(PictureBox pictureBox)
    {
        string path = Path.Combine(AppContext.BaseDirectory, "Assets", "logo.png");
        if (File.Exists(path))
        {
            using Image image = Image.FromFile(path);
            pictureBox.Image = new Bitmap(image);
        }
    }

    public static void ShowError(Exception exception)
    {
        MessageBox.Show(exception.Message, "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
}

32. src/DemoExamTemplate/appsettings.json

{
  "ConnectionString": "Server=__DB_SERVER__;Database=__DB_NAME__;User Id=__DB_USER__;Password=__DB_PASSWORD__;Encrypt=True;TrustServerCertificate=True;MultipleActiveResultSets=True"
}

This package has no dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.2 276 6/15/2026
1.0.1 241 6/14/2026
1.0.0 240 6/14/2026