Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Правила для GitHub Copilot

- Всегда отвечай, используя русский язык
- Всегда пиши комментарии в коде на русском языке

## Комментарии
- Короткие пояснительные комментарии располагай в конце той же строки, что и код // кратко по делу
- Старайся избегать тривиальных комментариев

## XML‑документация
- Документируй классы, структуры, делегаты, перечисления и их члены только XML‑комментариями
- Одинарное предложение пиши в одной строке внутри тега и без точки в конце
- Каждый тег XML‑комментария располагай на отдельной строке
- Порядок тегов: `<summary>` → `<param>` → `<returns>` → `<exception>` → `<remarks>` → `<example>`
- Для сложных публичных метдов генерируй блок с простым примером использования кода внутри тега `<example>`

Примеры:
- `<summary>Краткое описание сущности</summary>`
- `<param name="Value">Описание параметра</param>`
- `<returns>Описание возвращаемого значения</returns>`

## Синтаксис и минимализм
- При генерации кода используй современные конструкции языка, совместимые с целевыми платформами проекта
- Стремись минимизировать количество фигурных скобок за счёт expression‑bodied членов и switch‑выражений
- Не убирай фигурные скобки в многострочных конструкциях ради читаемости
- Всегда старайся минимизировать размер кода, если не запрошено иное

Разрешённые современные приёмы (когда поддерживается целевой платформой):
- file‑scoped namespace
- expression‑bodied члены
- switch‑выражения и pattern matching
- target‑typed `new`
- collection expressions и инициализаторы коллекций
- `using var` и `await using`
- операторы `??`, `??=`, `is not`, `with`
- упрощение nullable-присвоения `target?.Property = 15;` вместо `if(target is not null) target.Property = 15;`

## Именование
- Локальные переменные: `snake_case`
- Параметры методов: `PascalCase`
- Поля экземпляров: `_PascalCase`
- Статические поля: `__PascalCase`
- Константы: `PascalCase`
- Публичные типы и члены API: `PascalCase`
- Предпочитай английский язык при именовании переменных, методов, классов и прочих сущностей

## Инициализация и объявления
- При инициализации массивов, списков и словарей используй выражения инициализации массивов/коллекций
- При объявлении переменных предпочитай использовать ключевое слово `var` (кроме случаев, когда явный тип заметно повышает понятность)

## Форматирование
- Короткие системные комментарии пиши компактно в одну строку
- Удаляй неиспользуемые `using`, сортируй и группируй директивы `using`
- Разделяй логические блоки пустыми строками по мере необходимости, избегай лишних переносов

## Практики .NET
- Включай `#nullable enable` там, где это поддерживается
- Используй guard‑выражения, например `ArgumentNullException.ThrowIfNull(x)`
- Предпочитай Try‑паттерны для контроля потока вместо исключений
- При генерации метода добавляй в его начале блок проверки входных параметров. Отделяй этот блок пустой строкой от остального тела метода
- При генерации публичных свойств у моделей-представления MVVM (классов, реализующих INotifyPropertyChanged) используй следующий формат (в одну строку):
```csharp
/// <summary>Описание свойства</summary>
public string PropertyName { get; set => Set(ref field, value); }
```
- Для простых лаконичных методов используй expression‑bodied синтаксис, записанный в одну строку.

## Совместимость целей
- В рабочем пространстве используются целевые платформы: `.NET Standard 2.0` и `.NET 10`
- Применяй современные возможности языка и платформы только если они доступны для соответствующей целевой платформы проекта
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x
dotnet-version: 10.0.x

- name: Cache NuGet
uses: actions/cache@v3
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x
dotnet-version: 10.0.x

- name: Cache NuGet
- name: Cache NuGet
uses: actions/cache@v3
with:
path: ~/.nuget/packages
Expand Down
11 changes: 9 additions & 2 deletions MathCore.Hosting.WPF.sln
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.1.32421.90
# Visual Studio Version 18
VisualStudioVersion = 18.0.11205.157 d18.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MathCore.Hosting.WPF", "MathCore.Hosting.WPF\MathCore.Hosting.WPF.csproj", "{3D43B887-FC43-4074-B027-30E3F54EAC9F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{A61C5345-67BE-46B8-A52D-338801906E34}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MathCore.Hosting.WPF.TestWPF", "Tests\MathCore.Hosting.WPF.TestWPF\MathCore.Hosting.WPF.TestWPF.csproj", "{0F4EEB55-74E5-4075-BBE0-FA8DBBE397E5}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".Service", ".Service", "{28372A79-1495-45D6-A555-D9D5F75FE7F8}"
ProjectSection(SolutionItems) = preProject
.github\copilot-instructions.md = .github\copilot-instructions.md
.github\workflows\publish.yml = .github\workflows\publish.yml
.github\workflows\testing.yml = .github\workflows\testing.yml
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down
9 changes: 9 additions & 0 deletions MathCore.Hosting.WPF.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=_041B_043E_0433_0433_0435_0440/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=_0425_043E_0441_0442/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=_043B_043E_0433_0438_0440_043E_0432_0430_043D_0438_0435/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=_043B_043E_0433_0438_0440_043E_0432_0430_043D_0438_044F/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=_043B_043E_0433_0438_0440_0443_0435_043C/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=_043F_043E_0441_0442_0440_043E_0438_0442_0435_043B_044C/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=_043F_043E_0441_0442_0440_043E_0438_0442_0435_043B_044F/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=_0445_043E_0441_0442_0430/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
130 changes: 114 additions & 16 deletions MathCore.Hosting.WPF/ApplicationHosting.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,67 @@
using MathCore.DI;
using System.Diagnostics.CodeAnalysis;
using MathCore.DI;
// ReSharper disable EventNeverSubscribedTo.Global

namespace MathCore.Hosting.WPF;

/// <summary>Приложение WPF с поддержкой механизмов хоста и контейнера сервисов</summary>
/// <remarks>
/// Для использования необходимо унаследовать ваш класс приложения от данного абстрактного класса и корректно указать его в корневой разметке App.xaml
/// Регистрацию пользовательских сервисов можно выполнить через статическое событие ConfigureServices или методы ServicesAdd/ServicesRemove
/// </remarks>
/// <example>
/// Пример настройки приложения:
/// 1. Создаём класс App.xaml.cs, наследуя его от ApplicationHosting:
/// <code>
/// using MathCore.Hosting.WPF;
/// using Microsoft.Extensions.DependencyInjection;
/// using Microsoft.Extensions.Hosting;
///
/// namespace MyApp;
///
/// public interface IMyService { void Do(); }
/// public class MyService : IMyService { public void Do() { /* реализация */ } }
///
/// public partial class App : ApplicationHosting
/// {
/// static App()
/// {
/// // Подписка на событие конфигурации сервисов один раз при загрузке типа
/// ConfigureServices += OnConfigureServices;
/// }
///
/// private static void OnConfigureServices(HostBuilderContext context, IServiceCollection services)
/// {
/// // Регистрация собственных сервисов приложения
/// services.AddSingleton<IMyService, MyService>();
/// }
/// }
/// </code>
/// 2. Изменяем корень файла App.xaml, указывая локальный класс (унаследован от ApplicationHosting):
/// <code>
/// <local:App x:Class="MyApp.App"
/// xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
/// xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
/// xmlns:local="clr-namespace:MyApp"
/// StartupUri="MainWindow.xaml">
/// <!-- Ресурсы приложения -->
/// </local:App>
/// </code>
/// 3. Использование зарегистрированного сервиса в окне:
/// <code>
/// using Microsoft.Extensions.DependencyInjection;
///
/// public partial class MainWindow : Window
/// {
/// public MainWindow()
/// {
/// InitializeComponent();
/// var my_service = ApplicationHosting.Services.GetRequiredService<IMyService>();
/// my_service.Do();
/// }
/// }
/// </code>
/// </example>
public abstract class ApplicationHosting : Application
{
/// <summary>Событие возникает в момент первичной конфигурации хоста</summary>
Expand Down Expand Up @@ -31,7 +89,8 @@ public abstract class ApplicationHosting : Application
LoadingServiceFromExecutingAssembly,
];

public static IReadOnlyList<Assembly> ErrorLoadingServicesAssemblies { get; private set; } = Array.Empty<Assembly>();
/// <summary>Список сборок, в которых произошли ошибки при загрузке сервисов</summary>
public static IReadOnlyList<Assembly> ErrorLoadingServicesAssemblies { get; private set; } = [];

private static void LoadingServiceFromExecutingAssembly(HostBuilderContext Host, IServiceCollection services)
{
Expand Down Expand Up @@ -85,10 +144,9 @@ private static void LoadingServiceFromExecutingAssembly(HostBuilderContext Host,
/// <summary>Текущее окно</summary>
public static Window? CurrentWindow => FocusedWindow ?? ActiveWindow ?? Current.MainWindow;

private static IHost? __Hosting;

/// <summary>Хост приложения</summary>
public static IHost Hosting => __Hosting ??= CreateHostBuilder(Environment.GetCommandLineArgs())
[field: MaybeNull, AllowNull]
public static IHost Hosting => field ??= CreateHostBuilder(Environment.GetCommandLineArgs())
.AddServiceLocator()
.Build();

Expand Down Expand Up @@ -124,22 +182,62 @@ public static IHostBuilder CreateHostBuilder(string[] Args)
return builder;
}

/// <summary>Переопределяет логику старта приложения для инициализации хоста и контейнера сервисов</summary>
protected override async void OnStartup(StartupEventArgs e)
{
var host = Hosting;
Resources["ServiceLocator"] = new ServiceLocatorHosted();
base.OnStartup(e);
// ReSharper disable once AsyncApostle.AsyncAwaitMayBeElidedHighlighting
await host.StartAsync().ConfigureAwait(false);

__HostBuilderConfigurations.Clear();
__ServicesConfigurators.Clear();
try
{
var host = Hosting;
Resources["ServiceLocator"] = new ServiceLocatorHosted();
base.OnStartup(e);
// ReSharper disable once AsyncApostle.AsyncAwaitMayBeElidedHighlighting
await host.StartAsync().ConfigureAwait(false);

__HostBuilderConfigurations.Clear();
__ServicesConfigurators.Clear();
}
catch (Exception error)
{
if(!HandleStartupException(error))
// ReSharper disable once AsyncVoidThrowException
throw;
}
}

/// <summary>
/// Переопределяет логику обработки исключений, возникающих при старте приложения
/// </summary>
/// <param name="error">Возникшее в процессе выполнения метода <see cref="OnStartup"/> исключение</param>
/// <returns>
/// Истина, если исключение обработано и его повторная генерация не требуется - приложение продолжит работать;
/// Ложь, если исключение не обработано и его необходимо повторно сгенерировать - приложение завершит работу.
/// </returns>
protected virtual bool HandleStartupException(Exception error) => false;

/// <summary>Переопределяет логику завершения приложения для корректной остановки хоста</summary>
protected override async void OnExit(ExitEventArgs e)
{
using var host = Hosting;
base.OnExit(e);
await host.StopAsync().ConfigureAwait(false);
try
{
using var host = Hosting;
base.OnExit(e);
await host.StopAsync().ConfigureAwait(false);
}
catch (Exception error)
{
if(!HandleExitException(error))
// ReSharper disable once AsyncVoidThrowException
throw;
}
}

/// <summary>
/// Переопределяет логику обработки исключений, возникающих при завершении приложения
/// </summary>
/// <param name="error">Возникшее в процессе выполнения метода <see cref="OnExit"/> исключение</param>
/// <returns>
/// Истина, если исключение обработано и его повторная генерация не требуется - приложение продолжит завершение работы;
/// Ложь, если исключение не обработано и его необходимо повторно сгенерировать - приложение завершит работу с ошибкой.
/// </returns>
protected virtual bool HandleExitException(Exception error) => false;
}
23 changes: 10 additions & 13 deletions MathCore.Hosting.WPF/Extensions/CommandEx.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
using MathCore.WPF.Commands;
#nullable enable
using MathCore.WPF.Commands;
using Microsoft.Extensions.Logging;

namespace MathCore.Hosting.WPF.Extensions;

/// <summary>Методы расширения для конфигурации команд</summary>
public static class CommandEx
{
/// <summary>Добавляет логирование событий выполнения команды</summary>
/// <param name="command">Команда к которой добавляется логирование</param>
/// <param name="logger">Логгер используемый для записи событий команды</param>
/// <returns>Исходная команда с подключёнными обработчиками логирования</returns>
public static TCommand WithLogging<TCommand>(this TCommand command, ILogger logger) where TCommand : Command
{
command.BeforeExecuted += BeforeCommandExecuting;
Expand All @@ -13,19 +19,10 @@ public static TCommand WithLogging<TCommand>(this TCommand command, ILogger logg

return command;

void BeforeCommandExecuting(object? Sender, EventArgs<object?> E)
{
logger.LogInformation("Command {0} start executing with parameter {1}", command, E.Argument);
}
void BeforeCommandExecuting(object? Sender, EventArgs<object?> E) => logger.LogInformation("Command {command} start executing with parameter {parameter}", command, E.Argument); // логируем старт выполнения

void OnCommandExecuted(object? Sender, EventArgs<object?> E)
{
logger.LogInformation("Command {0} executed successful with parameter {1}", command, E.Argument);
}
void OnCommandExecuted(object? Sender, EventArgs<object?> E) => logger.LogInformation("Command {command} executed successful with parameter {parameter}", command, E.Argument); // логируем успешное выполнение

void OnCommandExecutingError(object Sender, ExceptionEventHandlerArgs<Exception> Args)
{
logger.LogError("Command {0} thrown error {1}", command, Args.Argument);
}
void OnCommandExecutingError(object Sender, ExceptionEventHandlerArgs<Exception> Args) => logger.LogError("Command {command} thrown error {exception}", command, Args.Argument); // логируем ошибку выполнения
}
}
Loading