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
19 changes: 18 additions & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
- Каждый тег XML‑комментария располагай на отдельной строке
- Порядок тегов: `<summary>` → `<param>` → `<returns>` → `<exception>` → `<remarks>` → `<example>`
- Для сложных публичных метдов генерируй блок с простым примером использования кода внутри тега `<example>`
- Примеры использования кода внутри тега `<example>` должны быть лаконичными и демонстрировать только ключевые моменты использования метода, без избыточных деталей
- Примеры кода нужно заворачивать в тег `<code>` внутри тега `<example>`, чтобы обеспечить правильное форматирование и подсветку синтаксиса, а также размещать в элементе `<![CDATA[ ]]>` что бы не использовать экранирование xml-символов

Примеры:
- `<summary>Краткое описание сущности</summary>`
Expand Down Expand Up @@ -67,4 +69,19 @@ public string PropertyName { get; set => Set(ref field, value); }

## Совместимость целей
- В рабочем пространстве используются целевые платформы: `.NET Standard 2.0` и `.NET 10`
- Применяй современные возможности языка и платформы только если они доступны для соответствующей целевой платформы проекта
- Применяй современные возможности языка и платформы только если они доступны для соответствующей целевой платформы проекта

## Модульные тесты с использованием MSTest
- Используй платформу MSTest для написания модульных тестов
- Классы тестов должны быть помечены атрибутом `[TestClass]`
- Методы тестов должны быть помечены атрибутом `[TestMethod]`
- Для параметризованных тестов используй `[DataTestMethod]` и `[DataRow]`
- Следуй паттерну Arrange-Act-Assert (AAA) при написании тестов
- Каждый тест должен проверять только одно поведение
- Избегай использования статических полей в тестах
- Убедись, что тесты могут выполняться в любом порядке и параллельно
- Для проверки исключений используй `Assert.ThrowsException`
- Используй `Debug.WriteLine` для вывода отладочной информации о процессе выполнения тестов, если в тесте есть промежуточные вычисления
- При написании Assert-методов добавляй сообщения об ошибках на русском языке
- Файлы модульных тестов должны создаваться с учётом структуры каталогов тестируемого кода
- Каждый модульный тест должен быть снабжён XML‑документацией, описывающей его назначение и поведение
35 changes: 22 additions & 13 deletions MathCore.Hosting.WPF/ApplicationHosting.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;

using MathCore.DI;
// ReSharper disable EventNeverSubscribedTo.Global

Expand All @@ -11,8 +12,8 @@ namespace MathCore.Hosting.WPF;
/// </remarks>
/// <example>
/// Пример настройки приложения:
/// 1. Создаём класс App.xaml.cs, наследуя его от ApplicationHosting:
/// <code>
/// 1. Создаём класс App.xaml.cs, наследуя его от ApplicationHosting в разметке XAML:
/// <code><![CDATA[
/// using MathCore.Hosting.WPF;
/// using Microsoft.Extensions.DependencyInjection;
/// using Microsoft.Extensions.Hosting;
Expand All @@ -22,7 +23,7 @@ namespace MathCore.Hosting.WPF;
/// public interface IMyService { void Do(); }
/// public class MyService : IMyService { public void Do() { /* реализация */ } }
///
/// public partial class App : ApplicationHosting
/// public partial class App // : Application - удалить наследование от Application, так как ApplicationHosting уже наследует его в разметке XAML
/// {
/// static App()
/// {
Expand All @@ -36,19 +37,19 @@ namespace MathCore.Hosting.WPF;
/// services.AddSingleton<IMyService, MyService>();
/// }
/// }
/// </code>
/// ]]></code>
/// 2. Изменяем корень файла App.xaml, указывая локальный класс (унаследован от ApplicationHosting):
/// <code>
/// <local:App x:Class="MyApp.App"
/// <code><![CDATA[
/// <ApplicationHosting 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"
/// xmlns:l="clr-namespace:MyApp"
/// StartupUri="MainWindow.xaml">
/// <!-- Ресурсы приложения -->
/// </local:App>
/// </code>
/// </ApplicationHosting>
/// ]]></code>
/// 3. Использование зарегистрированного сервиса в окне:
/// <code>
/// <code><![CDATA[
/// using Microsoft.Extensions.DependencyInjection;
///
/// public partial class MainWindow : Window
Expand All @@ -60,7 +61,15 @@ namespace MathCore.Hosting.WPF;
/// my_service.Do();
/// }
/// }
/// </code>
/// ]]></code>
///
/// Пример компактной настройки приложения:
/// <code><![CDATA[
/// public partial class App // : Application - удалить наследование от Application, так как ApplicationHosting уже наследует его в разметке XAML
/// {
/// static App() => ConfigureServices += (_, services) => services.AddSingleton<IMyService, MyService>();
/// }
/// ]]></code>
/// </example>
public abstract class ApplicationHosting : Application
{
Expand Down Expand Up @@ -198,7 +207,7 @@ protected override async void OnStartup(StartupEventArgs e)
}
catch (Exception error)
{
if(!HandleStartupException(error))
if (!HandleStartupException(error))
// ReSharper disable once AsyncVoidThrowException
throw;
}
Expand All @@ -225,7 +234,7 @@ protected override async void OnExit(ExitEventArgs e)
}
catch (Exception error)
{
if(!HandleExitException(error))
if (!HandleExitException(error))
// ReSharper disable once AsyncVoidThrowException
throw;
}
Expand Down
6 changes: 6 additions & 0 deletions MathCore.Hosting.WPF/Extensions/CommandEx.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
namespace MathCore.Hosting.WPF.Extensions;

/// <summary>Методы расширения для конфигурации команд</summary>
/// <example>
/// <code><![CDATA[
/// var command = new RelayCommand(_ => DoWork());
/// command.WithLogging(logger);
/// ]]></code>
/// </example>
public static class CommandEx
{
/// <summary>Добавляет логирование событий выполнения команды</summary>
Expand Down
25 changes: 25 additions & 0 deletions MathCore.Hosting.WPF/HostedServiceLocator.cs
Original file line number Diff line number Diff line change
@@ -1,25 +1,50 @@
namespace MathCore.Hosting.WPF;

/// <summary>Базовый класс для реализации локатора сервисов приложения</summary>
/// <example>
/// <code><![CDATA[
/// var locator = new ServiceLocatorHosted();
/// var service = locator.GetRequiredService<IMyService>();
/// ]]></code>
/// </example>
public class ServiceLocatorHosted : ServiceLocator
{
static ServiceLocatorHosted() => ApplicationHosting.HostBuilderConfiguratorAdd(ConfigureAppServices);

private static void ConfigureAppServices(IHostBuilder HostBuilder) => HostBuilder.ConfigureServices(ConfigureServices);

/// <summary>Контейнер сервисов приложения</summary>
protected override IServiceProvider Services => ApplicationHosting.Services;

/// <summary>Получить сервис по типу</summary>
/// <param name="ServiceType">Тип сервиса</param>
/// <returns>Экземпляр сервиса или <see langword="null"/></returns>
public object? this[Type ServiceType] => Services.GetService(ServiceType);

/// <summary>Получить сервис по имени типа</summary>
/// <param name="ServiceTypeName">Имя типа сервиса</param>
/// <returns>Экземпляр сервиса или <see langword="null"/></returns>
public object? this[string ServiceTypeName] => Type.GetType(ServiceTypeName) is { } type ? this[type] : null;

/// <summary>Получить сервис по типу</summary>
/// <param name="ServiceType">Тип сервиса</param>
/// <returns>Экземпляр сервиса или <see langword="null"/></returns>
public virtual object? GetService(Type ServiceType) => Services.GetService(ServiceType);

/// <summary>Получить сервис по типу</summary>
/// <typeparam name="T">Тип сервиса</typeparam>
/// <returns>Экземпляр сервиса или <see langword="null"/></returns>
public virtual T? GetService<T>() => (T?)GetService(typeof(T));

/// <summary>Получить обязательный сервис по типу</summary>
/// <param name="ServiceType">Тип сервиса</param>
/// <returns>Экземпляр сервиса</returns>
public virtual object GetRequiredService(Type ServiceType) => Services.GetRequiredService(ServiceType);

#pragma warning disable CS8714 // Тип не может быть использован как параметр типа в универсальном типе или методе. Допустимость значения NULL для аргумента типа не соответствует ограничению "notnull".
/// <summary>Получить обязательный сервис по типу</summary>
/// <typeparam name="T">Тип сервиса</typeparam>
/// <returns>Экземпляр сервиса</returns>
public virtual T GetRequiredService<T>() => Services.GetRequiredService<T>();
#pragma warning restore CS8714 // Тип не может быть использован как параметр типа в универсальном типе или методе. Допустимость значения NULL для аргумента типа не соответствует ограничению "notnull".
}
32 changes: 32 additions & 0 deletions MathCore.Hosting.WPF/Infrastructure/RequiredMemberAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,55 @@
// ReSharper disable once CheckNamespace
namespace System.Runtime.CompilerServices;

/// <summary>Атрибут обязательного члена</summary>
/// <example>
/// <code><![CDATA[
/// public sealed class Person
/// {
/// [RequiredMember]
/// public string Name { get; init; }
/// }
/// ]]></code>
/// </example>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, Inherited = false)]
internal sealed class RequiredMemberAttribute : Attribute { }

/// <summary>Атрибут требования возможностей компилятора</summary>
/// <example>
/// <code><![CDATA[
/// [CompilerFeatureRequired(CompilerFeatureRequiredAttribute.RequiredMembers)]
/// public sealed class FeatureDependentType { }
/// ]]></code>
/// </example>
[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)]
internal sealed class CompilerFeatureRequiredAttribute : Attribute
{
/// <summary>Имя возможности обязательных членов</summary>
public const string RequiredMembers = nameof(RequiredMembers);

/// <summary>Имя возможности ссылочных структур</summary>
public const string RefStructs = nameof(RefStructs);

/// <summary>Имя требуемой возможности</summary>
public string FeatureName { get; }

/// <summary>Признак необязательной возможности</summary>
public bool IsOptional { get; init; }

/// <summary>Создать атрибут требования возможностей компилятора</summary>
/// <param name="FeatureName">Имя требуемой возможности</param>
public CompilerFeatureRequiredAttribute(string FeatureName) => this.FeatureName = FeatureName;
}

/// <summary>Маркер инициализации внешнего члена</summary>
/// <example>
/// <code><![CDATA[
/// public sealed class Person
/// {
/// public string Name { get; init; }
/// }
/// ]]></code>
/// </example>
internal sealed class IsExternalInit { }

#endif
5 changes: 2 additions & 3 deletions MathCore.Hosting.WPF/MathCore.Hosting.WPF.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
net10.0-windows;
net8.0-windows;
net4.8-windows;
net4.7-windows;
net4.6.1-windows;
</TargetFrameworks>
<UseWPF>true</UseWPF>
Expand All @@ -17,7 +16,7 @@
</PropertyGroup>

<PropertyGroup>
<Version>1.0.1</Version>
<Version>1.0.2</Version>
<PackageReleaseNotes>
Обновление пакетов
</PackageReleaseNotes>
Expand Down Expand Up @@ -50,7 +49,7 @@

<ItemGroup>
<PackageReference Include="MathCore.Hosting" Version="0.0.6" />
<PackageReference Include="MathCore.WPF" Version="0.0.49" />
<PackageReference Include="MathCore.WPF" Version="0.0.48.3" />
</ItemGroup>

<PropertyGroup Condition="'$(TargetFramework)'=='net7.0-windows'">
Expand Down
15 changes: 15 additions & 0 deletions MathCore.Hosting.WPF/WindowViewModelAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,17 @@
namespace MathCore.Hosting.WPF;

/// <summary>Модель-представления окна</summary>
/// <param name="WindowType">Тип окна для модели-представления</param>
/// <example>
/// <code><![CDATA[
/// [WindowViewModel(typeof(MainWindow))]
/// public sealed class MainWindowViewModel : ViewModel { }
/// ]]></code>
/// </example>
[AttributeUsage(AttributeTargets.Class)]
public class WindowViewModelAttribute(Type WindowType) : Attribute
{
/// <summary>Создать атрибут без указания типа окна</summary>
public WindowViewModelAttribute() : this(null!) { }

/// <summary>Тип окна для модели-представления</summary>
Expand All @@ -19,6 +27,13 @@ public WindowViewModelAttribute() : this(null!) { }
#if NET7_0_OR_GREATER

/// <summary>Модель-представления окна</summary>
/// <typeparam name="TWindow">Тип окна</typeparam>
/// <example>
/// <code><![CDATA[
/// [WindowViewModel<MainWindow>]
/// public sealed class MainWindowViewModel : ViewModel { }
/// ]]></code>
/// </example>
[AttributeUsage(AttributeTargets.Class)]
public sealed class WindowViewModelAttribute<TWindow>() : Attribute where TWindow : Window
{
Expand Down