From 90618e2a2bfefd28054036355fc5c360587ce43e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Feb 2025 02:23:23 +0000 Subject: [PATCH 01/56] Bump actions/upload-artifact from 4.4.3 to 4.6.1 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.4.3 to 4.6.1. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4.4.3...v4.6.1) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d29cd54..4bd4797 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -56,7 +56,7 @@ jobs: run: dotnet pack MathCore.WPF/MathCore.WPF.csproj --no-build -c Release -v q -o ${{ github.workspace }}/ReleasePack --include-symbols - name: Upload build artifacts - uses: actions/upload-artifact@v4.4.3 + uses: actions/upload-artifact@v4.6.1 with: name: Release path: ${{ github.workspace }}/ReleasePack From d1f10754ecb829ee025aaad228cf4d9cc15be1be Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Feb 2025 02:40:02 +0000 Subject: [PATCH 02/56] Bump actions/download-artifact from 4.1.8 to 4.1.9 Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4.1.8 to 4.1.9. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v4.1.8...v4.1.9) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d29cd54..2abaf34 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -69,7 +69,7 @@ jobs: steps: - name: Get artifact - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v4.1.9 id: download with: name: Release @@ -85,7 +85,7 @@ jobs: steps: - name: Get artifact - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v4.1.9 id: download with: name: Release From 0271cac20523583c108a81b9fb92304c73e24469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D0=B0=D0=B2=D0=B5=D0=BB?= Date: Tue, 18 Nov 2025 12:24:00 +0300 Subject: [PATCH 03/56] =?UTF-8?q?=D0=9F=D0=BE=D0=B4=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D1=80=D0=B5=D0=B4=D1=83=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=D0=B4=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F=20CS1591=20?= =?UTF-8?q?=D0=BE=D0=B1=20=D0=BE=D1=82=D1=81=D1=83=D1=82=D1=81=D1=82=D0=B2?= =?UTF-8?q?=D0=B8=D0=B8=20xml-=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=B0=D1=80=D0=B8=D1=8F=20=D1=83=20=D1=87=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=BF=D1=80=D0=B8=20=D1=84=D0=BE=D1=80=D0=BC=D0=B8?= =?UTF-8?q?=D1=80=D0=B2=D0=BE=D0=B0=D0=BD=D0=B8=D0=B8=20=D1=80=D0=B5=D0=BB?= =?UTF-8?q?=D0=B8=D0=B7=D0=B0=20=D0=B2=20ci-cd-=D1=81=D0=BA=D1=80=D0=B8?= =?UTF-8?q?=D0=BF=D1=82=D0=B5=20publish.yml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6912cf4..e19496b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -46,7 +46,7 @@ jobs: - name: Building run: | - dotnet build MathCore.WPF -c Release --no-restore --nowarn:CS1998,CS8625,CS8600,CS8603,CS8620,CS8618,CS8604,CS8602,CS8622,CS8619,CS8632,CS0108,NU1701,NU1702,MSB3277,NU1701 + dotnet build MathCore.WPF -c Release --no-restore --nowarn:CS1998,CS8625,CS8600,CS8603,CS8620,CS8618,CS8604,CS8602,CS8622,CS8619,CS8632,CS0108,NU1701,NU1702,MSB3277,NU1701,CS1591 dotnet build Tests/MathCore.WPF.Tests -c Release --no-restore - name: Test From ad464c540e7470eb20e7cd4dad5fa0dbc4c1cbce Mon Sep 17 00:00:00 2001 From: Infarh Date: Sat, 13 Dec 2025 19:06:26 +0300 Subject: [PATCH 04/56] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D1=81=D0=BA=D1=80=D0=B8=D0=BF=D1=82=D1=8B?= =?UTF-8?q?=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=B0=D0=BA=D0=B5=D1=82=D0=B0=20=D0=B8=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B5=D0=BA=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавлены два C#-скрипта: nuget-get-last-version.cs для получения последней версии NuGet-пакета (по .csproj или ID) и version.cs для извлечения из .csproj. Оба скрипта поддерживают запуск через dotnet-script, снабжены обработкой ошибок и сообщениями на русском языке. --- .tools/nuget-get-last-version.cs | 120 +++++++++++++++++++++++++++++++ .tools/version.cs | 50 +++++++++++++ MathCore.WPF.sln | 12 +++- 3 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 .tools/nuget-get-last-version.cs create mode 100644 .tools/version.cs diff --git a/.tools/nuget-get-last-version.cs b/.tools/nuget-get-last-version.cs new file mode 100644 index 0000000..436a95c --- /dev/null +++ b/.tools/nuget-get-last-version.cs @@ -0,0 +1,120 @@ +#!/usr/bin/env dotnet +#:package NuGet.Protocol@6.10.1 +#:package NuGet.Configuration@6.10.1 +#:package NuGet.Versioning@6.10.1 +#:package NuGet.Common@6.10.1 + +/* +Использование: + dotnet run nuget-get-last-version.cs [<путь_к_.csproj> | <идентификатор_пакета>] + + +Коды возврата: + 0 — успешное завершение + -1 — запрос помощи (--help, -h) + -2 — ошибка использования (отсутствует аргумент, пустой аргумент) + 1 — непредвиденная ошибка выполнения (catch) + 2 — ошибка чтения/парсинга .csproj + 3 — отсутствуют элементы в .csproj + 5 — версия пакета не найдена +*/ + +using System; +using System.IO; +using System.Linq; +using System.Xml.Linq; +using System.Threading; +using System.Threading.Tasks; +using NuGet.Configuration; +using NuGet.Protocol; +using NuGet.Protocol.Core.Types; +using NuGet.Versioning; +using NuGet.Common; // для NullLogger + +if (args.Length < 1) +{ + Console.Error.WriteLine("Не указан путь к .csproj"); // ошибка использования + Console.Out.WriteLine("Использование: dotnet run nuget-get-last-version.cs [<путь_к_.csproj> | <идентификатор_пакета>]"); + Environment.Exit(-2); +} + +var input = args[0].Trim(); +if (input.Length == 0) +{ + Console.Error.WriteLine("Аргумент пуст"); // ошибка использования + Console.Out.WriteLine("Использование: dotnet run nuget-get-last-version.cs [<путь_к_.csproj> | <идентификатор_пакета>]"); + Environment.Exit(-2); +} + +if(input == "--help" || input == "-h") +{ + Console.Out.WriteLine("Использование: dotnet run nuget-get-last-version.cs [<путь_к_.csproj> | <идентификатор_пакета>]"); + Environment.Exit(-1); +} + +// Если передан путь к .csproj, извлечь идентификатор пакета из PackageReference +string package_id; +if (File.Exists(input) && string.Equals(Path.GetExtension(input), ".csproj", StringComparison.OrdinalIgnoreCase)) + try + { + var xml = XDocument.Load(input); // загрузка проекта + // Пытаемся найти ссылку на пакет MathCore, иначе берём первый PackageReference + var pkg_mathcore = xml + .Root? + .Descendants("PackageReference") + .FirstOrDefault(e => string.Equals((string?)e.Attribute("Include"), "MathCore", StringComparison.OrdinalIgnoreCase)); + + var pkg = pkg_mathcore ?? xml.Root?.Descendants("PackageReference").FirstOrDefault(); + package_id = (string?)pkg?.Attribute("Include") ?? string.Empty; + + if (string.IsNullOrWhiteSpace(package_id)) + { + Console.Error.WriteLine("В .csproj не найдены элементы "); // отсутствуют требуемые данные + Environment.Exit(3); + } + } + catch (Exception e) + { + Console.Error.WriteLine($"Ошибка чтения .csproj: {e.Message}"); // ошибка парсинга + Environment.Exit(2); + return; + } +else + package_id = input; // непосредственно ID пакета + +try +{ + if (await GetLatestStableVersionAsync(package_id, CancellationToken.None) is not { } version) + { + Console.Error.WriteLine($"Не удалось определить последнюю версию пакета {package_id}"); // ресурс/версия не найдены + Environment.Exit(5); + } + + Console.Out.Write(version.ToString()); // вывод версии +} +catch (Exception e) +{ + Console.Error.WriteLine($"Ошибка: {e.Message}"); // общая непредвиденная ошибка выполнения + Environment.Exit(1); +} + +static async Task GetLatestStableVersionAsync(string PackageId, CancellationToken Cancel) +{ + var providers = Repository.Provider.GetCoreV3(); // провайдеры ресурсов + var source = new PackageSource("https://api.nuget.org/v3/index.json"); // индекс nuget.org + var repo = new SourceRepository(source, providers); + + var metadata = await repo.GetResourceAsync(Cancel); // ресурс метаданных + var search = await metadata.GetMetadataAsync( + PackageId, + includePrerelease: true, + includeUnlisted: false, + new SourceCacheContext(), + NullLogger.Instance, + Cancel); + + var versions = search.Select(m => m.Identity.Version).OrderBy(v => v); // сортировка по возрастанию + var latest_stable = versions.LastOrDefault(v => !v.IsPrerelease); // последняя стабильная + + return latest_stable ?? versions.LastOrDefault(); // если стабильной нет, взять последнюю вообще +} \ No newline at end of file diff --git a/.tools/version.cs b/.tools/version.cs new file mode 100644 index 0000000..a573803 --- /dev/null +++ b/.tools/version.cs @@ -0,0 +1,50 @@ +#!/usr/bin/env dotnet + +using System; +using System.IO; +using System.Linq; +using System.Xml.Linq; + +if (args.Length < 1) +{ + Console.Error.WriteLine("Укажите путь к .csproj файлу"); // проверка аргументов + Environment.Exit(1); +} + +var project_path = args[0]; // путь к проекту + +if (!File.Exists(project_path)) +{ + Console.Error.WriteLine($"Файл не найден: {project_path}"); // проверка наличия файла + Environment.Exit(2); +} + +XDocument doc; +try +{ + doc = XDocument.Load(project_path); // загрузка XML +} +catch (Exception e) +{ + Console.Error.WriteLine($"Ошибка загрузки XML: {e.Message}"); // ошибка парсинга + Environment.Exit(3); + return; +} + +// поиск версии в PropertyGroup/Version +var version = doc + .Root? + .Descendants("PropertyGroup") + .Elements("Version") + .Select(v => (string?)v.Value) + .FirstOrDefault(v => !string.IsNullOrWhiteSpace(v)); + +if (string.IsNullOrWhiteSpace(version)) +{ + Console.Error.WriteLine("Свойство не найдено"); // версия не обнаружена + Environment.Exit(4); +} +else +{ + Console.Out.Write(version.Trim()); // вывод версии в stdout +} \ No newline at end of file diff --git a/MathCore.WPF.sln b/MathCore.WPF.sln index b123683..654a76c 100644 --- a/MathCore.WPF.sln +++ b/MathCore.WPF.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.1.32421.90 +# Visual Studio Version 18 +VisualStudioVersion = 18.1.11304.174 d18.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MathCore.WPF", "MathCore.WPF\MathCore.WPF.csproj", "{32A25B93-BDF4-4451-BECC-2676D65BD2AB}" EndProject @@ -16,6 +16,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".Service", ".Service", "{F8D2B6B1-796B-4514-AD6D-78BA8F120698}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + .tools\version.cs = .tools\version.cs EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{40F619FF-49AE-493A-846A-FE4D455B9BCB}" @@ -29,6 +30,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ .github\workflows\testing.yml = .github\workflows\testing.yml EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{2EDACEB4-3693-49E4-8307-0915042BBDA5}" + ProjectSection(SolutionItems) = preProject + .tools\nuget-get-last-version.cs = .tools\nuget-get-last-version.cs + .tools\version.cs = .tools\version.cs + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -61,6 +68,7 @@ Global {9A075286-7BDD-48D0-9C51-4157E5583842} = {7B0FF24E-606F-4FE1-B2EB-59CE8B955D18} {40F619FF-49AE-493A-846A-FE4D455B9BCB} = {F8D2B6B1-796B-4514-AD6D-78BA8F120698} {C83B7C5C-5536-4BDA-9447-7B9A6393F30E} = {40F619FF-49AE-493A-846A-FE4D455B9BCB} + {2EDACEB4-3693-49E4-8307-0915042BBDA5} = {F8D2B6B1-796B-4514-AD6D-78BA8F120698} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {9D12AAC1-92D6-412C-9508-5B021ABB30B6} From 7b5dc628891d1c10840f68e9b9e5b2201ebbd6f3 Mon Sep 17 00:00:00 2001 From: Infarh Date: Sat, 13 Dec 2025 19:06:56 +0300 Subject: [PATCH 05/56] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B9?= =?UTF-8?q?=20=D0=B8=20=D1=8D=D0=BA=D1=81=D0=BF=D0=B5=D1=80=D0=B8=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D1=8B=20=D1=81=20=D0=BC=D0=B5=D1=82=D0=BE?= =?UTF-8?q?=D0=B4=D0=B0=D0=BC=D0=B8-=D1=80=D0=B0=D1=81=D1=88=D0=B8=D1=80?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Tests/MathCore.WPF.ConsoleTest/Program.cs | 18 ++++++++++++++++++ .../MathCore.WPF.WindowTest.csproj | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Tests/MathCore.WPF.ConsoleTest/Program.cs b/Tests/MathCore.WPF.ConsoleTest/Program.cs index 1e02f8f..e0a2745 100644 --- a/Tests/MathCore.WPF.ConsoleTest/Program.cs +++ b/Tests/MathCore.WPF.ConsoleTest/Program.cs @@ -1,5 +1,7 @@ using System.Threading.Channels; +_ = Console.Out << "Bounded Channel Test" << Environment.NewLine; + var sh = Channel.CreateBounded(new BoundedChannelOptions(10) { AllowSynchronousContinuations = true, @@ -36,3 +38,19 @@ Console.WriteLine("Completed"); Console.ReadLine(); + +public static class TextWriterOperators +{ + extension(TextWriter writer) // блок расширения для TextWriter + { + // Запись строки и возврат того же TextWriter для цепочек + public static TextWriter operator <<(TextWriter Writer, string Value) + { + Writer.Write(Value); // записать значение + return Writer; // вернуть для цепочки + } + + // Универсальный вариант для любых объектов + public static TextWriter operator <<(TextWriter Writer, object? Value) => Writer << (Value?.ToString() ?? string.Empty); + } +} \ No newline at end of file diff --git a/Tests/MathCore.WPF.WindowTest/MathCore.WPF.WindowTest.csproj b/Tests/MathCore.WPF.WindowTest/MathCore.WPF.WindowTest.csproj index e9774e8..439c910 100644 --- a/Tests/MathCore.WPF.WindowTest/MathCore.WPF.WindowTest.csproj +++ b/Tests/MathCore.WPF.WindowTest/MathCore.WPF.WindowTest.csproj @@ -27,7 +27,7 @@ - + From 11e07b8cf1530551b1d5ac812382ec76e503cb61 Mon Sep 17 00:00:00 2001 From: Infarh Date: Sat, 13 Dec 2025 19:11:58 +0300 Subject: [PATCH 06/56] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BF=D0=BE=D0=B4=D1=80=D0=BE=D0=B1=D0=BD?= =?UTF-8?q?=D0=B0=D1=8F=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BA=20=D0=BA=D0=BB=D0=B0=D1=81?= =?UTF-8?q?=D1=81=D1=83=20Arc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавлены XML-комментарии к классу Arc, его основным свойствам и методам для улучшения понимания и использования. Внутри метода GetGeometry добавлены поясняющие комментарии к этапам построения дуги. Форматирование кода улучшено для читаемости. Функциональность не изменена. --- MathCore.WPF/Shapes/Arc.cs | 80 +++++++++++++++++++++++++++++++------- 1 file changed, 66 insertions(+), 14 deletions(-) diff --git a/MathCore.WPF/Shapes/Arc.cs b/MathCore.WPF/Shapes/Arc.cs index 5f37990..cc2b5b8 100644 --- a/MathCore.WPF/Shapes/Arc.cs +++ b/MathCore.WPF/Shapes/Arc.cs @@ -6,6 +6,29 @@ namespace MathCore.WPF.Shapes; +/// Фигура WPF для рисования дуги окружности +/// +/// Используется в XAML как обычная фигура, например внутри элемента +/// Свойства , и управляют положением и размером дуги +/// +/// +/// +/// <Window +/// x:Class="DemoApp.MainWindow" +/// xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" +/// xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" +/// xmlns:shapes="clr-namespace:MathCore.WPF.Shapes;assembly=MathCore.WPF"> +/// <Grid> +/// <shapes:Arc +/// Stroke="Red" +/// StrokeThickness="2" +/// R="1" +/// StartAngle="0" +/// StopAngle="180" /> +/// </Grid> +/// </Window> +/// +/// public class Arc : ShapeBase { static Arc() @@ -13,8 +36,14 @@ static Arc() //StretchProperty.OverrideMetadata(typeof(Arc), new FrameworkPropertyMetadata(Stretch.None)); } + /// Радиус дуги в относительных единицах от 0 до 1 + /// + /// Значение интерпретируется относительно размеров контейнера + /// При значении 1 дуга строится по максимальному доступному радиусу в пределах прямоугольника + /// public double R { get => (double)GetValue(RProperty); set => SetValue(RProperty, value); } + /// Радиус дуги public static readonly DependencyProperty RProperty = DependencyProperty.Register(nameof(R), typeof(double), @@ -22,8 +51,13 @@ static Arc() new FrameworkPropertyMetadata(1D, FrameworkPropertyMetadataOptions.AffectsRender)); + /// Начальный угол дуги в градусах + /// + /// Отсчёт ведётся по часовой стрелке, 0 градусов направлен вправо, 90 градусов вниз + /// public double StartAngle { get => (double)GetValue(StartAngleProperty); set => SetValue(StartAngleProperty, value); } + /// Свойство зависимости начального угла дуги public static readonly DependencyProperty StartAngleProperty = DependencyProperty.Register(nameof(StartAngle), typeof(double), @@ -31,8 +65,14 @@ static Arc() new FrameworkPropertyMetadata(0D, FrameworkPropertyMetadataOptions.AffectsRender)); + /// Конечный угол дуги в градусах + /// + /// Если разница между и больше либо равна 360, + /// будет отрисована полная окружность вместо дуги + /// public double StopAngle { get => (double)GetValue(StopAngleProperty); set => SetValue(StopAngleProperty, value); } + /// Свойство зависимости конечного угла дуги public static readonly DependencyProperty StopAngleProperty = DependencyProperty.Register(nameof(StopAngle), typeof(double), @@ -40,11 +80,17 @@ static Arc() new FrameworkPropertyMetadata(360d, FrameworkPropertyMetadataOptions.AffectsRender)); + /// Геометрия, определяющая отображаемую дугу protected override Geometry DefiningGeometry => _VisibleRect is { IsEmpty: false, Width: > 0, Height: > 0 } ? GetGeometry(_VisibleRect, StartAngle, StopAngle, R) : Geometry.Empty; + /// Вычисляет координаты точки дуги по углу и радиусу в пределах прямоугольника + /// Угол в градусах + /// Радиус в относительных единицах + /// Прямоугольник отрисовки + /// Координаты точки дуги private static Point GetPoint(double a, double r, Rect rect) { const double to_rad = Math.PI / 180; @@ -56,30 +102,36 @@ private static Point GetPoint(double a, double r, Rect rect) return new(x, y); } + /// Создает геометрию дуги по заданным параметрам + /// Прямоугольник ограничивающей области + /// Начальный угол дуги в градусах + /// Конечный угол дуги в градусах + /// Радиус дуги в относительных единицах + /// Геометрия дуги или пустая геометрия private static Geometry GetGeometry(Rect rect, double Start, double End, double Radius) { var w = rect.Width; var h = rect.Height; - if(w == 0 || h == 0) return Geometry.Empty; + if (w == 0 || h == 0) return Geometry.Empty; // Если хотя бы одна из сторон прямоугольника равна нулю, возвращаем пустую геометрию var d = Math.Abs(End - Start); - if(d >= 360) return new EllipseGeometry(rect); + if (d >= 360) return new EllipseGeometry(rect); // Если угол больше либо равен полному обороту, рисуем окружность целиком - var p1 = GetPoint(Math.Min(Start, End), Radius, rect); - var p2 = GetPoint(Math.Max(Start, End), Radius, rect); + var p1 = GetPoint(Math.Min(Start, End), Radius, rect); // Вычисляем координаты начальной точки дуги + var p2 = GetPoint(Math.Max(Start, End), Radius, rect); // Вычисляем координаты конечной точки дуги - /* To draw the arc in perfect way instead of seeing it as Big arc */ - var y = w / 2 * Radius; - var y1 = h / 2 * Radius; - var arc = new Size(Math.Max(0, y), Math.Max(0, y1)); + /* Рисуем дугу корректным образом, чтобы она не считалась большой дугой */ + var y = w / 2 * Radius; // Половина ширины прямоугольника, масштабированная радиусом, как горизонтальная полуось + var y1 = h / 2 * Radius; // Половина высоты прямоугольника, масштабированная радиусом, как.vertical C# 7.3+ + var arc = new Size(Math.Max(0, y), Math.Max(0, y1)); // Размеры дуги (радиусы эллипса), гарантируем неотрицательность - var is_large = d > 180; + var is_large = d > 180; // Определяем, является ли дуга большой (более 180 градусов) - var geometry = new StreamGeometry(); - using var context = geometry.Open(); - context.BeginFigure(p1, isFilled: false, isClosed: false); - context.ArcTo(p2, arc, 0, is_large, SweepDirection.Clockwise, isStroked: true, isSmoothJoin: false); + var geometry = new StreamGeometry(); // Создаём потоковую геометрию для описания дуги + using var context = geometry.Open(); // Открываем контекст для построения фигуры + context.BeginFigure(p1, isFilled: false, isClosed: false); // Начинаем фигуру с первой точки, без заливки и без замыкания контура + context.ArcTo(p2, arc, 0, is_large, SweepDirection.Clockwise, isStroked: true, isSmoothJoin: false); // Строим дугу от первой до второй точки по часовой стрелке - return geometry; + return geometry; // Возвращаем построенную геометрию дуги } } \ No newline at end of file From 06ba5bfda7fd4506fa0f9d856af8f04e79b489d4 Mon Sep 17 00:00:00 2001 From: Infarh Date: Sat, 13 Dec 2025 19:53:04 +0300 Subject: [PATCH 07/56] =?UTF-8?q?=D0=A3=D0=BB=D1=83=D1=87=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20=D0=B4?= =?UTF-8?q?=D1=83=D0=B3=D0=B8=20Arc=20=D0=B8=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=8B=D0=B5=20=D1=82=D0=B5=D1=81=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавлены ограничения радиуса дуги (диапазон [0;1]), нормализация углов, учёт направления и формы эллипса при построении геометрии. Исправлена обработка крайних случаев (полная окружность, нулевая дуга). Добавлен набор модульных тестов для проверки корректности работы и граничных условий. --- MathCore.WPF/Shapes/Arc.cs | 82 +++++++++++++++----- Tests/MathCore.WPF.Tests/Shapes/ArcTests.cs | 83 +++++++++++++++++++++ 2 files changed, 148 insertions(+), 17 deletions(-) create mode 100644 Tests/MathCore.WPF.Tests/Shapes/ArcTests.cs diff --git a/MathCore.WPF/Shapes/Arc.cs b/MathCore.WPF/Shapes/Arc.cs index cc2b5b8..f70914f 100644 --- a/MathCore.WPF/Shapes/Arc.cs +++ b/MathCore.WPF/Shapes/Arc.cs @@ -31,6 +31,9 @@ namespace MathCore.WPF.Shapes; /// public class Arc : ShapeBase { + private const double FullCircleDegrees = 360d; + private const double MinArcDegrees = 1e-6; // минимальная длина дуги в градусах, ниже считаем дугу нулевой + static Arc() { //StretchProperty.OverrideMetadata(typeof(Arc), new FrameworkPropertyMetadata(Stretch.None)); @@ -40,6 +43,7 @@ static Arc() /// /// Значение интерпретируется относительно размеров контейнера /// При значении 1 дуга строится по максимальному доступному радиусу в пределах прямоугольника + /// Значения вне диапазона [0;1] автоматически ограничиваются /// public double R { get => (double)GetValue(RProperty); set => SetValue(RProperty, value); } @@ -49,11 +53,14 @@ static Arc() typeof(double), typeof(Arc), new FrameworkPropertyMetadata(1D, - FrameworkPropertyMetadataOptions.AffectsRender)); + FrameworkPropertyMetadataOptions.AffectsRender, + null, + CoerceR)); /// Начальный угол дуги в градусах /// /// Отсчёт ведётся по часовой стрелке, 0 градусов направлен вправо, 90 градусов вниз + /// Дуга рисуется от к , знак разности задаёт направление обхода /// public double StartAngle { get => (double)GetValue(StartAngleProperty); set => SetValue(StartAngleProperty, value); } @@ -67,7 +74,7 @@ static Arc() /// Конечный угол дуги в градусах /// - /// Если разница между и больше либо равна 360, + /// Если разница между и по модулю близка к 360 градусам, /// будет отрисована полная окружность вместо дуги /// public double StopAngle { get => (double)GetValue(StopAngleProperty); set => SetValue(StopAngleProperty, value); } @@ -86,6 +93,21 @@ static Arc() ? GetGeometry(_VisibleRect, StartAngle, StopAngle, R) : Geometry.Empty; + private static object CoerceR(DependencyObject d, object base_value) + { + var r = (double)base_value; + if (r < 0) return 0d; + if (r > 1) return 1d; + return r; + } + + /// Нормализует угол к диапазону [0;360) + private static double NormalizeAngle(double angle) + { + angle %= FullCircleDegrees; + return angle < 0 ? angle + FullCircleDegrees : angle; + } + /// Вычисляет координаты точки дуги по углу и радиусу в пределах прямоугольника /// Угол в градусах /// Радиус в относительных единицах @@ -94,11 +116,21 @@ static Arc() private static Point GetPoint(double a, double r, Rect rect) { const double to_rad = Math.PI / 180; - a -= 90; - a *= to_rad; - r /= 2; - var x = (0.5 + r * Math.Cos(a)) * rect.Width + rect.Left; - var y = (0.5 + r * Math.Sin(a)) * rect.Height + rect.Top; + + a = (a - 90) * to_rad; + + var half_width = rect.Width / 2; + var half_height = rect.Height / 2; + + var center_x = rect.Left + half_width; + var center_y = rect.Top + half_height; + + var radius_x = half_width * r; + var radius_y = half_height * r; + + var x = center_x + radius_x * Math.Cos(a); + var y = center_y + radius_y * Math.Sin(a); + return new(x, y); } @@ -114,23 +146,39 @@ private static Geometry GetGeometry(Rect rect, double Start, double End, double var h = rect.Height; if (w == 0 || h == 0) return Geometry.Empty; // Если хотя бы одна из сторон прямоугольника равна нулю, возвращаем пустую геометрию - var d = Math.Abs(End - Start); - if (d >= 360) return new EllipseGeometry(rect); // Если угол больше либо равен полному обороту, рисуем окружность целиком + var start_angle = NormalizeAngle(Start); + var end_angle = NormalizeAngle(End); + + var d_raw = end_angle - start_angle; + var d_abs = Math.Abs(d_raw); - var p1 = GetPoint(Math.Min(Start, End), Radius, rect); // Вычисляем координаты начальной точки дуги - var p2 = GetPoint(Math.Max(Start, End), Radius, rect); // Вычисляем координаты конечной точки дуги + // Если длина дуги по модулю близка к полному кругу, рисуем полную окружность + if (d_abs >= FullCircleDegrees - MinArcDegrees) + return new EllipseGeometry(rect); - /* Рисуем дугу корректным образом, чтобы она не считалась большой дугой */ - var y = w / 2 * Radius; // Половина ширины прямоугольника, масштабированная радиусом, как горизонтальная полуось - var y1 = h / 2 * Radius; // Половина высоты прямоугольника, масштабированная радиусом, как.vertical C# 7.3+ - var arc = new Size(Math.Max(0, y), Math.Max(0, y1)); // Размеры дуги (радиусы эллипса), гарантируем неотрицательность + // Слишком маленькая дуга считается нулевой + if (d_abs < MinArcDegrees) + return Geometry.Empty; - var is_large = d > 180; // Определяем, является ли дуга большой (более 180 градусов) + var p1 = GetPoint(start_angle, Radius, rect); // Вычисляем координаты начальной точки дуги + var p2 = GetPoint(end_angle, Radius, rect); // Вычисляем координаты конечной точки дуги + + var half_width = w / 2; + var half_height = h / 2; + + var radius_x = Math.Max(0, half_width * Radius); + var radius_y = Math.Max(0, half_height * Radius); + var arc = new Size(radius_x, radius_y); // Размеры дуги (радиусы эллипса), гарантируем неотрицательность + + var is_large = d_abs > 180; // Определяем, является ли дуга большой (более 180 градусов) + var sweep_direction = d_raw >= 0 ? SweepDirection.Clockwise : SweepDirection.Counterclockwise; // Учитываем направление дуги var geometry = new StreamGeometry(); // Создаём потоковую геометрию для описания дуги using var context = geometry.Open(); // Открываем контекст для построения фигуры context.BeginFigure(p1, isFilled: false, isClosed: false); // Начинаем фигуру с первой точки, без заливки и без замыкания контура - context.ArcTo(p2, arc, 0, is_large, SweepDirection.Clockwise, isStroked: true, isSmoothJoin: false); // Строим дугу от первой до второй точки по часовой стрелке + context.ArcTo(p2, arc, 0, is_large, sweep_direction, isStroked: true, isSmoothJoin: false); // Строим дугу от первой до второй точки + + geometry.Freeze(); // Оптимизация: делаем геометрию неизменяемой return geometry; // Возвращаем построенную геометрию дуги } diff --git a/Tests/MathCore.WPF.Tests/Shapes/ArcTests.cs b/Tests/MathCore.WPF.Tests/Shapes/ArcTests.cs new file mode 100644 index 0000000..a1a6050 --- /dev/null +++ b/Tests/MathCore.WPF.Tests/Shapes/ArcTests.cs @@ -0,0 +1,83 @@ +using MathCore.WPF.Shapes; + +namespace MathCore.WPF.Tests.Shapes; + +/// Набор модульных тестов для проверки поведения дуги Arc +[TestClass] +public sealed class ArcTests +{ + /// Проверка что отрицательное значение радиуса принудительно приводится к нулю + [STATestMethod] + public void ZeroRadius_IsCoercedToZero() + { + var arc = new Arc { R = -1 }; + + Assert.AreEqual(0d, arc.R, "Отрицательный радиус должен быть приведён к нулю"); + } + + /// Проверка что значение радиуса больше единицы принудительно ограничивается единицей + [STATestMethod] + public void RadiusGreaterThanOne_IsCoercedToOne() + { + var arc = new Arc { R = 2 }; + + Assert.AreEqual(1d, arc.R, "Радиус больше единицы должен быть приведён к единице"); + } + + /// Проверка что значение радиуса в допустимом диапазоне [0;1] не изменяется при установке + [STATestMethod] + public void RadiusWithinRange_IsNotChanged() + { + var arc = new Arc { R = 0.5 }; + + Assert.AreEqual(0.5d, arc.R, "Радиус в диапазоне [0;1] не должен изменяться при установке"); + } + + /// Проверка что при нулевой ширине фигуры формируется пустая геометрия дуги + [STATestMethod] + public void ZeroWidth_ReturnsEmptyGeometry() + { + var arc = new Arc + { + Width = 0, + Height = 100, + R = 1, + StartAngle = 0, + StopAngle = 180 + }; + + Assert.IsTrue(arc.RenderedGeometry.IsEmpty(), "При нулевой ширине фигуры геометрия дуги должна быть пустой"); + } + + /// Проверка что при нулевой высоте фигуры формируется пустая геометрия дуги + [STATestMethod] + public void ZeroHeight_ReturnsEmptyGeometry() + { + var arc = new Arc + { + Width = 100, + Height = 0, + R = 1, + StartAngle = 0, + StopAngle = 180 + }; + + Assert.IsTrue(arc.RenderedGeometry.IsEmpty(), "При нулевой высоте фигуры геометрия дуги должна быть пустой"); + } + + /// Проверка что при равных начальном и конечном углах строится нулевая дуга и геометрия пуста + [STATestMethod] + public void ZeroArc_WhenAnglesAreEqual_ReturnsEmptyGeometry() + { + var arc = new Arc + { + Width = 100, + Height = 100, + R = 1, + StartAngle = 10, + StopAngle = 10 + }; + + Assert.IsTrue(arc.RenderedGeometry.IsEmpty(), "При равных начальном и конечном углах геометрия дуги должна быть пустой"); + } +} From 42c196de56fc032c4c87f569f06289fcb193ef06 Mon Sep 17 00:00:00 2001 From: Infarh Date: Sat, 13 Dec 2025 19:57:12 +0300 Subject: [PATCH 08/56] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=B8=D0=BD=D1=81=D1=82=D1=80=D1=83=D0=BA?= =?UTF-8?q?=D1=86=D0=B8=D0=B8=20=D0=BF=D0=BE=20=D0=BD=D0=B0=D0=BF=D0=B8?= =?UTF-8?q?=D1=81=D0=B0=D0=BD=D0=B8=D1=8E=20=D1=82=D0=B5=D1=81=D1=82=D0=BE?= =?UTF-8?q?=D0=B2=20=D0=BD=D0=B0=20MSTest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit В copilot-instructions.md добавлен раздел с требованиями к модульным тестам на MSTest: оформление классов и методов, использование AAA-паттерна, изоляция тестов, обработка исключений, вывод отладочной информации и рекомендации по сообщениям об ошибках. --- .github/copilot-instructions.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2adf293..031deac 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -67,4 +67,17 @@ public string PropertyName { get; set => Set(ref field, value); } ## Совместимость целей - В рабочем пространстве используются целевые платформы: `.NET Standard 2.0` и `.NET 10` -- Применяй современные возможности языка и платформы только если они доступны для соответствующей целевой платформы проекта \ No newline at end of file +- Применяй современные возможности языка и платформы только если они доступны для соответствующей целевой платформы проекта + +## Модульные тесты с использованием MSTest +- Используй платформу MSTest для написания модульных тестов +- Классы тестов должны быть помечены атрибутом `[TestClass]` +- Методы тестов должны быть помечены атрибутом `[TestMethod]` +- Для параметризованных тестов используй `[DataTestMethod]` и `[DataRow]` +- Следуй паттерну Arrange-Act-Assert (AAA) при написании тестов +- Каждый тест должен проверять только одно поведение +- Избегай использования статических полей в тестах +- Убедись, что тесты могут выполняться в любом порядке и параллельно +- Для проверки исключений используй `Assert.ThrowsException` +- Используй `Debug.WriteLine` для вывода отладочной информации о процессе выполнения тестов, если в тесте есть промежуточные вычисления +- При написании Assert-методов добавляй сообщения об ошибках на русском языке \ No newline at end of file From 88f83588d60bd6b83fd64c3262b144708eea5a6f Mon Sep 17 00:00:00 2001 From: Infarh Date: Sat, 13 Dec 2025 20:06:47 +0300 Subject: [PATCH 09/56] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20IsAligned=20=D0=B8=20=D1=82=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D1=8B,=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20Pie?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit В класс Pie добавлено свойство IsAligned для выравнивания сектора по меньшему размеру. Для всех зависимых свойств и методов добавлены XML-документации. Улучшена валидация радиусов с помощью pattern matching. В проект тестов добавлен PieTests.cs с полным набором модульных тестов для проверки поведения фигуры Pie, включая граничные случаи, значения по умолчанию и синхронизацию свойств. Проведена унификация и улучшено форматирование кода. --- MathCore.WPF/Shapes/Pie.cs | 118 +++++++---- Tests/MathCore.WPF.Tests/Shapes/PieTests.cs | 206 ++++++++++++++++++++ 2 files changed, 288 insertions(+), 36 deletions(-) create mode 100644 Tests/MathCore.WPF.Tests/Shapes/PieTests.cs diff --git a/MathCore.WPF/Shapes/Pie.cs b/MathCore.WPF/Shapes/Pie.cs index 5fa9f5c..3d95cbd 100644 --- a/MathCore.WPF/Shapes/Pie.cs +++ b/MathCore.WPF/Shapes/Pie.cs @@ -11,6 +11,7 @@ namespace MathCore.WPF.Shapes; +/// Определяет форму сектора круга или кольца public class Pie : Shape { private const FrameworkPropertyMetadataOptions __DependendPropertyMetadataOptions = @@ -24,18 +25,21 @@ static Pie() FillProperty.OverrideMetadata(typeof(Pie), new FrameworkPropertyMetadata(Brushes.LightGray)); } + /// Определяет зависимое свойство для выравнивания сектора по меньшему размеру public static readonly DependencyProperty IsAlignedProperty = DependencyProperty.Register(nameof(IsAligned), typeof(bool), typeof(Pie), new FrameworkPropertyMetadata(false, __DependendPropertyMetadataOptions, - propertyChangedCallback: null, + propertyChangedCallback: null, coerceValueCallback: null, isAnimationProhibited: false, defaultUpdateSourceTrigger: UpdateSourceTrigger.PropertyChanged)); + /// Получает или устанавливает значение, указывающее, выравнивается ли сектор по меньшему размеру public bool IsAligned { get => (bool)GetValue(IsAlignedProperty); set => SetValue(IsAlignedProperty, value); } + /// Определяет зависимое свойство для внешнего радиуса сектора public static readonly DependencyProperty OuterRadiusProperty = DependencyProperty.Register(nameof(OuterRadius), typeof(double), @@ -47,8 +51,10 @@ static Pie() defaultUpdateSourceTrigger: UpdateSourceTrigger.PropertyChanged), ValidateRadius); + /// Получает или устанавливает внешний радиус сектора (от 0 до 1) public double OuterRadius { get => (double)GetValue(OuterRadiusProperty); set => SetValue(OuterRadiusProperty, value); } + /// Определяет зависимое свойство для внутреннего радиуса сектора public static readonly DependencyProperty InnerRadiusProperty = DependencyProperty.Register(nameof(InnerRadius), typeof(double), @@ -60,33 +66,41 @@ static Pie() defaultUpdateSourceTrigger: UpdateSourceTrigger.PropertyChanged), ValidateRadius); - private static bool ValidateRadius(object r) => (double)r >= 0 && (double)r <= 1; + /// Проверяет, что радиус находится в диапазоне [0, 1] + private static bool ValidateRadius(object r) => (double)r is >= 0 and <= 1; + /// Получает или устанавливает внутренний радиус сектора (от 0 до 1) public double InnerRadius { get => (double)GetValue(InnerRadiusProperty); set => SetValue(InnerRadiusProperty, value); } + /// Определяет зависимое свойство для начального угла сектора public static readonly DependencyProperty StartAngleProperty = DependencyProperty.Register(nameof(StartAngle), typeof(double), typeof(Pie), - new FrameworkPropertyMetadata(0d, __DependendPropertyMetadataOptions, - propertyChangedCallback: null, + new FrameworkPropertyMetadata(0d, __DependendPropertyMetadataOptions, + propertyChangedCallback: null, coerceValueCallback: null)); + /// Получает или устанавливает начальный угол сектора в градусах public double StartAngle { get => (double)GetValue(StartAngleProperty); set => SetValue(StartAngleProperty, value); } + /// Определяет зависимое свойство для конечного угла сектора public static readonly DependencyProperty StopAngleProperty = DependencyProperty.Register(nameof(StopAngle), typeof(double), typeof(Pie), - new FrameworkPropertyMetadata(360d, __DependendPropertyMetadataOptions, - propertyChangedCallback: OnStopAngleChanged, + new FrameworkPropertyMetadata(360d, __DependendPropertyMetadataOptions, + propertyChangedCallback: OnStopAngleChanged, coerceValueCallback: null)); + /// Обработчик изменения конечного угла private static void OnStopAngleChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) => o.SetValue(AngleProperty, (double)e.NewValue - ((Pie)o).StartAngle); + /// Получает или устанавливает конечный угол сектора в градусах public double StopAngle { get => (double)GetValue(StopAngleProperty); set => SetValue(StopAngleProperty, value); } + /// Определяет зависимое свойство для угла раствора сектора public static readonly DependencyProperty AngleProperty = DependencyProperty.Register(nameof(Angle), typeof(double), @@ -97,9 +111,11 @@ private static void OnStopAngleChanged(DependencyObject o, DependencyPropertyCha isAnimationProhibited: false, defaultUpdateSourceTrigger: UpdateSourceTrigger.PropertyChanged)); + /// Обработчик изменения угла раствора private static void OnAngleChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) => o.SetValue(StopAngleProperty, (double)e.NewValue + ((Pie)o).StartAngle); + /// Получает или устанавливает угол раствора сектора в градусах public double Angle { get => (double)GetValue(AngleProperty); set => SetValue(AngleProperty, value); } private readonly EllipseGeometry _OuterEllipse = new(); @@ -108,33 +124,37 @@ private static void OnAngleChanged(DependencyObject o, DependencyPropertyChanged private readonly CombinedGeometry _Pie; private Rect _Rect; + /// Получает геометрию, определяющую форму сектора protected override Geometry DefiningGeometry => _Rect is { IsEmpty: false, Width: > 0, Height: > 0 } ? GetGeometry(_Rect, StartAngle, StopAngle, OuterRadius, InnerRadius, IsAligned) : Geometry.Empty; + /// Инициализирует новый экземпляр класса Pie public Pie() { _Cycle = new(GeometryCombineMode.Exclude, _OuterEllipse, _InnerEllipse); - _Pie = new(GeometryCombineMode.Exclude, _OuterEllipse, _InnerEllipse); + _Pie = new(GeometryCombineMode.Exclude, _OuterEllipse, _InnerEllipse); } + /// Измеряет размер сектора protected override Size MeasureOverride(Size ConstraintSize) { _Rect = Rect.Empty; return base.MeasureOverride(ConstraintSize); } + /// Располагает сектор в пределах отведённого пространства protected override Size ArrangeOverride(Size FinalSize) { var size = base.ArrangeOverride(FinalSize); - var t = StrokeThickness; - var m = t / 2; + var t = StrokeThickness; + var m = t / 2; _Rect = size.IsEmpty || size.Width.Equals(0d) || size.Height.Equals(0d) ? Rect.Empty : new(m, m, Math.Max(0, size.Width - t), Math.Max(0, size.Height - t)); - switch(Stretch) + switch (Stretch) { case Stretch.None: //_Rect.Width = _Rect.Height = 0; @@ -142,13 +162,13 @@ protected override Size ArrangeOverride(Size FinalSize) case Stretch.Fill: break; case Stretch.Uniform: - if(_Rect.Width > _Rect.Height) + if (_Rect.Width > _Rect.Height) _Rect.Width = _Rect.Height; else _Rect.Height = _Rect.Width; break; case Stretch.UniformToFill: - if(_Rect.Width < _Rect.Height) + if (_Rect.Width < _Rect.Height) _Rect.Width = _Rect.Height; else _Rect.Height = _Rect.Width; @@ -157,37 +177,50 @@ protected override Size ArrangeOverride(Size FinalSize) return size; } + /// Вычисляетсектора на основе заданных параметров + /// Прямоугольник ограничивающей области + /// Начальный угол в градусах + /// Конечный угол в градусах + /// Внешний радиус (от 0 до 1) + /// Внутренний радиус (от 0 до 1) + /// Флаг выравнивания по меньшему размеру + /// Геометрия сектора private Geometry GetGeometry(Rect rect, double start, double stop, double R, double r, bool aligned) { var a = Math.Abs(stop - start); - if(a is 0d) + if (a is 0d) return Geometry.Empty; ChangeGeometry(rect, R, r, aligned); - if(a is 0d || Math.Abs(a) >= 360) - return r is 0d - ? _OuterEllipse + if (a is 0d || Math.Abs(a) >= 360) + return r is 0d + ? _OuterEllipse : _Cycle; var geometry = new StreamGeometry(); - using(var geometry_context = geometry.Open()) + using (var geometry_context = geometry.Open()) DrawGeometry(geometry_context, rect, R, r, start, stop, aligned); geometry.Freeze(); - if(r is 0d) return geometry; + if (r is 0d) return geometry; _Pie.Geometry1 = geometry; return _Pie; } + /// Обновляетэллипсов внешнего и внутреннего радиусов + /// Прямоугольник ограничивающей области + /// Внешний радиус + /// Внутренний радиус + /// Флаг выравнивания по меньшему размеру private void ChangeGeometry(Rect rect, double R, double r, bool aligned) { - var center = new Point(rect.Width / 2 + rect.Left, rect.Height / 2 + rect.Left); - var w = rect.Width; - var h = rect.Height; - if(aligned) w = h = Math.Min(w, h); + var center = new Point(rect.Width / 2 + rect.Left, rect.Height / 2 + rect.Left); + var w = rect.Width; + var h = rect.Height; + if (aligned) w = h = Math.Min(w, h); w /= 2; h /= 2; - _OuterEllipse.Center = _InnerEllipse.Center = center; + _OuterEllipse.Center = _InnerEllipse.Center = center; _OuterEllipse.RadiusX = w * R; _OuterEllipse.RadiusY = h * R; _InnerEllipse.RadiusX = w * r; @@ -202,6 +235,11 @@ private void ChangeGeometry(Rect rect, double R, double r, bool aligned) // return new Point(p0.X + r * Math.Cos(a) * w / 2, p0.Y + r * Math.Sin(a) * h / 2); //} + /// Вычисляетна эллипсе по углу и радиусу + /// Прямоугольник ограничивающей области + /// Угол в градусах + /// Радиус (от 0 до 1) + /// Координата точки на эллипсе private static Point GetPoint(Rect rect, double a, double r) { const double to_rad = Math.PI / 180; @@ -213,37 +251,45 @@ private static Point GetPoint(Rect rect, double a, double r) return new(x, y); } + /// Рисует гев контекст потока + /// Контекст потока геометрии + /// Прямоугольник ограничивающей области + /// Внешний радиус + /// Внутренний радиус + /// Начальный угол + /// Конечный угол + /// Флаг выравнивания private static void DrawGeometry(StreamGeometryContext g, Rect rect, double R, double r, double start, double stop, bool aligned) { var w = rect.Width; var h = rect.Height; - if(w <= 0 || h <= 0) return; + if (w <= 0 || h <= 0) return; var p0 = new Point(0.5 * rect.Width + rect.Left, 0.5 * rect.Height + rect.Top); Func min = Math.Min; - var a = min(start, stop); - var b = Math.Max(start, stop); - var d = b - a; - if(d is 0d) return; + var a = min(start, stop); + var b = Math.Max(start, stop); + var d = b - a; + if (d is 0d) return; - if(aligned) + if (aligned) { w = min(w, h); h = min(w, h); } - var in_arc_stop = GetPoint(rect, a, r); + var in_arc_stop = GetPoint(rect, a, r); var out_arc_start = GetPoint(rect, a, R); - var out_arc_stop = GetPoint(rect, b, R); - var in_arc_start = GetPoint(rect, b, r); + var out_arc_stop = GetPoint(rect, b, R); + var in_arc_start = GetPoint(rect, b, r); - var arc_isout = d > 180.0; - var in_arc_size = new Size(r * w / 2, r * h / 2); + var arc_isout = d > 180.0; + var in_arc_size = new Size(r * w / 2, r * h / 2); var out_arc_size = new Size(R * w / 2, R * h / 2); var line_only = Math.Abs(R - r) < 0.001; - if(line_only) + if (line_only) g.BeginFigure(out_arc_start, false, true); else { @@ -251,7 +297,7 @@ private static void DrawGeometry(StreamGeometryContext g, Rect rect, double R, d g.LineTo(out_arc_start, true, true); } g.ArcTo(out_arc_stop, out_arc_size, 0, arc_isout, SweepDirection.Clockwise, true, true); - if(r is 0d || line_only) return; + if (r is 0d || line_only) return; g.LineTo(in_arc_start, true, true); g.ArcTo(in_arc_stop, in_arc_size, 0, arc_isout, SweepDirection.Counterclockwise, true, true); } diff --git a/Tests/MathCore.WPF.Tests/Shapes/PieTests.cs b/Tests/MathCore.WPF.Tests/Shapes/PieTests.cs new file mode 100644 index 0000000..bd9386e --- /dev/null +++ b/Tests/MathCore.WPF.Tests/Shapes/PieTests.cs @@ -0,0 +1,206 @@ +using MathCore.WPF.Shapes; +using System.Windows; +using System.Windows.Controls; + +namespace MathCore.WPF.Tests.Shapes; + +/// Набор модульных тестов для проверки поведения сектора Pie +[TestClass] +public sealed class PieTests +{ + /// Проверка что значение внешнего радиуса в допустимом диапазоне [0;1] не изменяется при установке + [STATestMethod] + public void OuterRadiusWithinRange_IsNotChanged() + { + var pie = new Pie { OuterRadius = 0.7 }; + + Assert.AreEqual(0.7d, pie.OuterRadius, "Внешний радиус в диапазоне [0;1] не должен изменяться при установке"); + } + + /// Проверка что значение внутреннего радиуса в допустимом диапазоне [0;1] не изменяется при установке + [STATestMethod] + public void InnerRadiusWithinRange_IsNotChanged() + { + var pie = new Pie { InnerRadius = 0.3 }; + + Assert.AreEqual(0.3d, pie.InnerRadius, "Внутренний радиус в диапазоне [0;1] не должен изменяться при установке"); + } + + /// Проверка что внутренний радиус не может быть больше внешнего + [STATestMethod] + public void InnerRadiusGreaterThanOuterRadius_IsCoercedToOuterRadius() + { + var pie = new Pie { OuterRadius = 0.5, InnerRadius = 0.8 }; + + Assert.IsTrue(pie.InnerRadius <= pie.OuterRadius, "Внутренний радиус должен быть меньше или равен внешнему радиусу"); + } + + /// Проверка что при нулевой ширине фигуры формируется пустая геометрия сектора + [STATestMethod] + public void ZeroWidth_ReturnsEmptyGeometry() + { + var pie = new Pie + { + Width = 0, + Height = 100, + OuterRadius = 1, + StartAngle = 0, + StopAngle = 180 + }; + + Assert.IsTrue(pie.RenderedGeometry.IsEmpty(), "При нулевой ширине фигуры геометрия сектора должна быть пустой"); + } + + /// Проверка что при нулевой высоте фигуры формируется пустая геометрия сектора + [STATestMethod] + public void ZeroHeight_ReturnsEmptyGeometry() + { + var pie = new Pie + { + Width = 100, + Height = 0, + OuterRadius = 1, + StartAngle = 0, + StopAngle = 180 + }; + + Assert.IsTrue(pie.RenderedGeometry.IsEmpty(), "При нулевой высоте фигуры геометрия сектора должна быть пустой"); + } + + /// Проверка что при равных начальном и конечном углах строится нулевой сектор и геометрия пуста + [STATestMethod] + public void ZeroSector_WhenAnglesAreEqual_ReturnsEmptyGeometry() + { + var pie = new Pie + { + Width = 100, + Height = 100, + OuterRadius = 1, + StartAngle = 45, + StopAngle = 45 + }; + + Assert.IsTrue(pie.RenderedGeometry.IsEmpty(), "При равных начальном и конечном углах геометрия сектора должна быть пустой"); + } + + /// Проверка что свойство IsAligned по умолчанию имеет значение false + [STATestMethod] + public void IsAlignedDefaultValue_IsFalse() + { + var pie = new Pie(); + + Assert.AreEqual(false, pie.IsAligned, "Свойство IsAligned по умолчанию должно быть false"); + } + + /// Проверка что свойство IsAligned может быть установлено в true + [STATestMethod] + public void IsAlignedCanBeSetToTrue() + { + var pie = new Pie { IsAligned = true }; + + Assert.AreEqual(true, pie.IsAligned, "Свойство IsAligned должно быть установлено в true"); + } + + /// Проверка что при изменении свойства StopAngle автоматически обновляется свойство Angle + [STATestMethod] + public void StopAngleChanged_AngleIsUpdated() + { + var pie = new Pie { StartAngle = 0, StopAngle = 180 }; + + Assert.AreEqual(180d, pie.Angle, "При изменении StopAngle свойство Angle должно обновиться"); + } + + /// Проверка что при изменении свойства Angle автоматически обновляется свойство StopAngle + [STATestMethod] + public void AngleChanged_StopAngleIsUpdated() + { + var pie = new Pie { StartAngle = 0, Angle = 90 }; + + Assert.AreEqual(90d, pie.StopAngle, "При изменении Angle свойство StopAngle должно обновиться"); + } + + /// Проверка что полный круг (360 градусов) с нулевым внутренним радиусом формирует корректную геометрию + [STATestMethod] + public void FullCircleWithZeroInnerRadius_ReturnsValidGeometry() + { + var pie = new Pie + { + OuterRadius = 1, + InnerRadius = 0, + StartAngle = 0, + StopAngle = 360 + }; + + var canvas = new Canvas(); + canvas.Children.Add(pie); + pie.Measure(new(200, 200)); + pie.Arrange(new(0, 0, 100, 100)); + + Assert.IsFalse(pie.RenderedGeometry.IsEmpty(), "Полный круг должен иметь непустую геометрию"); + } + + /// Проверка что кольцо (360 градусов с ненулевым внутренним радиусом) формирует корректную геометрию + [STATestMethod] + public void FullRingWithInnerRadius_ReturnsValidGeometry() + { + var pie = new Pie + { + OuterRadius = 1, + InnerRadius = 0.5, + StartAngle = 0, + StopAngle = 360 + }; + + var canvas = new Canvas(); + canvas.Children.Add(pie); + pie.Measure(new(200, 200)); + pie.Arrange(new(0, 0, 100, 100)); + + Assert.IsFalse(pie.RenderedGeometry.IsEmpty(), "Полное кольцо должно иметь непустую геометрию"); + } + + /// Проверка что начальный угол по умолчанию равен 0 + [STATestMethod] + public void StartAngleDefaultValue_IsZero() + { + var pie = new Pie(); + + Assert.AreEqual(0d, pie.StartAngle, "Начальный угол по умолчанию должен быть 0"); + } + + /// Проверка что конечный угол по умолчанию равен 360 + [STATestMethod] + public void StopAngleDefaultValue_Is360() + { + var pie = new Pie(); + + Assert.AreEqual(360d, pie.StopAngle, "Конечный угол по умолчанию должен быть 360"); + } + + /// Проверка что угол раствора по умолчанию равен 360 + [STATestMethod] + public void AngleDefaultValue_Is360() + { + var pie = new Pie(); + + Assert.AreEqual(360d, pie.Angle, "Угол раствора по умолчанию должен быть 360"); + } + + /// Проверка что внешний радиус по умолчанию равен 1 + [STATestMethod] + public void OuterRadiusDefaultValue_IsOne() + { + var pie = new Pie(); + + Assert.AreEqual(1d, pie.OuterRadius, "Внешний радиус по умолчанию должен быть 1"); + } + + /// Проверка что внутренний радиус по умолчанию равен 0 + [STATestMethod] + public void InnerRadiusDefaultValue_IsZero() + { + var pie = new Pie(); + + Assert.AreEqual(0d, pie.InnerRadius, "Внутренний радиус по умолчанию должен быть 0"); + } +} From 91d8689fd7ad2a92dcaee6115a3145e47a3f50ba Mon Sep 17 00:00:00 2001 From: Infarh Date: Sat, 13 Dec 2025 21:09:26 +0300 Subject: [PATCH 10/56] =?UTF-8?q?=D0=A3=D0=BB=D1=83=D1=87=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=87=D0=B8=D1=82=D0=B0=D0=B5=D0=BC=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D1=8C=20=D0=B8=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20DrawGe?= =?UTF-8?q?ometry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавлены подробные комментарии к этапам построения сектора в методе DrawGeometry класса Pie. Удалена лишняя функция min, вместо неё используется Math.Min. Добавлены проверки на граничные случаи, улучшена структура и понятность кода. --- MathCore.WPF/Shapes/Pie.cs | 44 +++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/MathCore.WPF/Shapes/Pie.cs b/MathCore.WPF/Shapes/Pie.cs index 3d95cbd..b3e78e6 100644 --- a/MathCore.WPF/Shapes/Pie.cs +++ b/MathCore.WPF/Shapes/Pie.cs @@ -261,44 +261,62 @@ private static Point GetPoint(Rect rect, double a, double r) /// Флаг выравнивания private static void DrawGeometry(StreamGeometryContext g, Rect rect, double R, double r, double start, double stop, bool aligned) { + // Получаем ширину и высоту прямоугольника var w = rect.Width; var h = rect.Height; if (w <= 0 || h <= 0) return; + + // Вычисляем центральную точку прямоугольника var p0 = new Point(0.5 * rect.Width + rect.Left, 0.5 * rect.Height + rect.Top); - Func min = Math.Min; - var a = min(start, stop); + // Нормализуем углы: a - меньший угол, b - больший угол + var a = Math.Min(start, stop); var b = Math.Max(start, stop); - var d = b - a; + var d = b - a; // Разница углов (угол раствора сектора) if (d is 0d) return; + // Если включено выравнивание, приводим к квадрату по меньшей стороне if (aligned) { - w = min(w, h); - h = min(w, h); + w = Math.Min(w, h); + h = Math.Min(w, h); } - var in_arc_stop = GetPoint(rect, a, r); - var out_arc_start = GetPoint(rect, a, R); - var out_arc_stop = GetPoint(rect, b, R); - var in_arc_start = GetPoint(rect, b, r); - + // Вычисляем ключевые точки для построения сектора: + var in_arc_stop = GetPoint(rect, a, r); // конечная точка внутренней дуги (начальный угол) + var out_arc_start = GetPoint(rect, a, R); // начальная точка внешней дуги (начальный угол) + var out_arc_stop = GetPoint(rect, b, R); // конечная точка внешней дуги (конечный угол) + var in_arc_start = GetPoint(rect, b, r); // начальная точка внутренней дуги (конечный угол) + // Определяем тип дуги (большая дуга если угол > 180°) var arc_isout = d > 180.0; + + // Вычисляем размеры эллипсов для внутренней и внешней дуг var in_arc_size = new Size(r * w / 2, r * h / 2); var out_arc_size = new Size(R * w / 2, R * h / 2); + // Проверяем, является ли сектор просто линией (внутренний и внешний радиусы почти равны) var line_only = Math.Abs(R - r) < 0.001; + + // Начинаем построение фигуры if (line_only) - g.BeginFigure(out_arc_start, false, true); + g.BeginFigure(out_arc_start, false, true); // Только дуга, без заливки else { + // Начинаем с центра (если r = 0) или с точки на внутренней дуге g.BeginFigure(r is 0d ? p0 : in_arc_stop, true, true); - g.LineTo(out_arc_start, true, true); + g.LineTo(out_arc_start, true, true); // Линия к началу внешней дуги } + + // Рисуем внешнюю дугу от начального до конечного угла по часовой стрелке g.ArcTo(out_arc_stop, out_arc_size, 0, arc_isout, SweepDirection.Clockwise, true, true); - if (r is 0d || line_only) return; + + if (r is 0d || line_only) return; // Если внутренний радиус 0 или это линия, завершаем + + // Рисуем линию к началу внутренней дуги g.LineTo(in_arc_start, true, true); + + // Рисуем внутреннюю дугу от конечного до начального угла против часовой стрелки g.ArcTo(in_arc_stop, in_arc_size, 0, arc_isout, SweepDirection.Counterclockwise, true, true); } } \ No newline at end of file From 70a038e3f80dd45925eac2712b567b1d61f76a95 Mon Sep 17 00:00:00 2001 From: Infarh Date: Sat, 13 Dec 2025 21:56:45 +0300 Subject: [PATCH 11/56] =?UTF-8?q?=D0=97=D0=B0=D0=B3=D0=BE=D1=82=D0=BE?= =?UTF-8?q?=D0=B2=D0=BA=D0=B0=20=D0=B2=D0=B8=D0=B7=D1=83=D0=B0=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D0=BE=D0=B3=D0=BE=20=D1=8D=D0=BB=D0=B5=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=B0=20"=D0=A1=D1=82=D1=80=D0=B5=D0=BB=D0=BA=D0=B0"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MathCore.WPF/Shapes/Arrow.cs | 222 +++++++++++ Tests/MathCore.WPF.Tests/Shapes/ArrowTests.cs | 354 ++++++++++++++++++ .../MathCore.WPF.WindowTest/TestWindows8.xaml | 4 +- 3 files changed, 579 insertions(+), 1 deletion(-) create mode 100644 MathCore.WPF/Shapes/Arrow.cs create mode 100644 Tests/MathCore.WPF.Tests/Shapes/ArrowTests.cs diff --git a/MathCore.WPF/Shapes/Arrow.cs b/MathCore.WPF/Shapes/Arrow.cs new file mode 100644 index 0000000..df2e5cd --- /dev/null +++ b/MathCore.WPF/Shapes/Arrow.cs @@ -0,0 +1,222 @@ +using System.Windows; +using System.Windows.Data; +using System.Windows.Media; +using System.Windows.Shapes; + +// ReSharper disable ArgumentsStyleAnonymousFunction +// ReSharper disable ArgumentsStyleLiteral +// ReSharper disable ArgumentsStyleNamedExpression +// ReSharper disable MemberCanBePrivate.Global + +namespace MathCore.WPF.Shapes; + +/// Визуальный элемент стрелки с настраиваемыми параметрами линии и головы +/// +/// Стрелка состоит из линии и головы в виде треугольника +/// Поддерживает настройку координат начала и конца, размеров головы, стиля линии и заливки +/// +/// +/// +/// <shapes:Arrow +/// X1="10" Y1="10" +/// X2="100" Y2="100" +/// ArrowHeadWidth="10" +/// ArrowHeadLength="15" +/// IsArrowHeadClosed="True" +/// Stroke="Blue" +/// StrokeThickness="2" +/// Fill="LightBlue" /> +/// +/// +public class Arrow : Shape +{ + private const FrameworkPropertyMetadataOptions __DependencyPropertyMetadataOptions = + FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure; + + static Arrow() + { + StrokeProperty.OverrideMetadata(typeof(Arrow), new FrameworkPropertyMetadata(Brushes.Black)); + StrokeThicknessProperty.OverrideMetadata(typeof(Arrow), new FrameworkPropertyMetadata(1d)); + FillProperty.OverrideMetadata(typeof(Arrow), new FrameworkPropertyMetadata(Brushes.Black)); + } + + #region Координаты линии стрелки + + /// Определяет зависимое свойство для X-координаты начальной точки стрелки + public static readonly DependencyProperty X1Property = + DependencyProperty.Register(nameof(X1), + typeof(double), + typeof(Arrow), + new FrameworkPropertyMetadata(0d, __DependencyPropertyMetadataOptions, + propertyChangedCallback: null, + coerceValueCallback: null, + isAnimationProhibited: false, + defaultUpdateSourceTrigger: UpdateSourceTrigger.PropertyChanged)); + + /// Получает или устанавливает X-координату начальной точки стрелки + public double X1 { get => (double)GetValue(X1Property); set => SetValue(X1Property, value); } + + /// Определяет зависимое свойство для Y-координаты начальной точки стрелки + public static readonly DependencyProperty Y1Property = + DependencyProperty.Register(nameof(Y1), + typeof(double), + typeof(Arrow), + new FrameworkPropertyMetadata(0d, __DependencyPropertyMetadataOptions, + propertyChangedCallback: null, + coerceValueCallback: null, + isAnimationProhibited: false, + defaultUpdateSourceTrigger: UpdateSourceTrigger.PropertyChanged)); + + /// Получает или устанавливает Y-координату начальной точки стрелки + public double Y1 { get => (double)GetValue(Y1Property); set => SetValue(Y1Property, value); } + + /// Определяет зависимое свойство для X-координаты конечной точки стрелки + public static readonly DependencyProperty X2Property = + DependencyProperty.Register(nameof(X2), + typeof(double), + typeof(Arrow), + new FrameworkPropertyMetadata(0d, __DependencyPropertyMetadataOptions, + propertyChangedCallback: null, + coerceValueCallback: null, + isAnimationProhibited: false, + defaultUpdateSourceTrigger: UpdateSourceTrigger.PropertyChanged)); + + /// Получает или устанавливает X-координату конечной точки стрелки + public double X2 { get => (double)GetValue(X2Property); set => SetValue(X2Property, value); } + + /// Определяет зависимое свойство для Y-координаты конечной точки стрелки + public static readonly DependencyProperty Y2Property = + DependencyProperty.Register(nameof(Y2), + typeof(double), + typeof(Arrow), + new FrameworkPropertyMetadata(0d, __DependencyPropertyMetadataOptions, + propertyChangedCallback: null, + coerceValueCallback: null, + isAnimationProhibited: false, + defaultUpdateSourceTrigger: UpdateSourceTrigger.PropertyChanged)); + + /// Получает или устанавливает Y-координату конечной точки стрелки + public double Y2 { get => (double)GetValue(Y2Property); set => SetValue(Y2Property, value); } + + #endregion + + #region Параметры головы стрелки + + /// Определяет зависимое свойство для ширины головы стрелки + public static readonly DependencyProperty ArrowHeadWidthProperty = + DependencyProperty.Register(nameof(ArrowHeadWidth), + typeof(double), + typeof(Arrow), + new FrameworkPropertyMetadata(10d, __DependencyPropertyMetadataOptions, + propertyChangedCallback: null, + coerceValueCallback: (o, value) => Math.Max(0d, (double)value), + isAnimationProhibited: false, + defaultUpdateSourceTrigger: UpdateSourceTrigger.PropertyChanged)); + + /// Получает или устанавливает ширину головы стрелки + public double ArrowHeadWidth { get => (double)GetValue(ArrowHeadWidthProperty); set => SetValue(ArrowHeadWidthProperty, value); } + + /// Определяет зависимое свойство для длины головы стрелки + public static readonly DependencyProperty ArrowHeadLengthProperty = + DependencyProperty.Register(nameof(ArrowHeadLength), + typeof(double), + typeof(Arrow), + new FrameworkPropertyMetadata(15d, __DependencyPropertyMetadataOptions, + propertyChangedCallback: null, + coerceValueCallback: (o, value) => Math.Max(0d, (double)value), + isAnimationProhibited: false, + defaultUpdateSourceTrigger: UpdateSourceTrigger.PropertyChanged)); + + /// Получает или устанавливает длину головы стрелки + public double ArrowHeadLength { get => (double)GetValue(ArrowHeadLengthProperty); set => SetValue(ArrowHeadLengthProperty, value); } + + /// Определяет зависимое свойство для замкнутости контура головы стрелки + public static readonly DependencyProperty IsArrowHeadClosedProperty = + DependencyProperty.Register(nameof(IsArrowHeadClosed), + typeof(bool), + typeof(Arrow), + new FrameworkPropertyMetadata(true, __DependencyPropertyMetadataOptions, + propertyChangedCallback: null, + coerceValueCallback: null, + isAnimationProhibited: false, + defaultUpdateSourceTrigger: UpdateSourceTrigger.PropertyChanged)); + + /// Получает или устанавливает значение, указывающее, замкнут ли контур головы стрелки + public bool IsArrowHeadClosed { get => (bool)GetValue(IsArrowHeadClosedProperty); set => SetValue(IsArrowHeadClosedProperty, value); } + + #endregion + + /// Получает геометрию, определяющую форму стрелки + protected override Geometry DefiningGeometry => GetGeometry(X1, Y1, X2, Y2, ArrowHeadWidth, ArrowHeadLength, IsArrowHeadClosed); + + /// Вычисляет геометрию стрелки на основе заданных параметров + /// X-координата начальной точки + /// Y-координата начальной точки + /// X-координата конечной точки + /// Y-координата конечной точки + /// Ширина головы стрелки + /// Длина головы стрелки + /// Замкнут ли контур головы стрелки + /// Геометрия стрелки + private static Geometry GetGeometry(double x1, double y1, double x2, double y2, double head_width, double head_length, bool head_closed) + { + // Вычисляем вектор направления стрелки + var dx = x2 - x1; + var dy = y2 - y1; + var length = Math.Sqrt(dx * dx + dy * dy); + + // Если стрелка имеет нулевую длину, возвращаем пустую геометрию + if (length < 1e-10) + return Geometry.Empty; + + // Нормализуем вектор направления + var dir_x = dx / length; + var dir_y = dy / length; + + // Вычисляем перпендикулярный вектор для построения головы стрелки + var perp_x = -dir_y; + var perp_y = dir_x; + + // Создаём группу геометрий для линии и головы стрелки + var geometry_group = new GeometryGroup(); + + // Вычисляем точку основания головы стрелки (где заканчивается линия) + var line_end_x = x2 - dir_x * head_length; + var line_end_y = y2 - dir_y * head_length; + + // Создаём линию стрелки от начальной точки до основания головы + var line = new LineGeometry(new Point(x1, y1), new Point(line_end_x, line_end_y)); + geometry_group.Children.Add(line); + + // Создаём голову стрелки в виде треугольника + if (head_width > 0 && head_length > 0) + { + var arrow_head = new StreamGeometry(); + using (var context = arrow_head.Open()) + { + // Вычисляем три вершины треугольника головы стрелки: + // 1. Острие стрелки (конечная точка) + var tip = new Point(x2, y2); + + // 2. Левая точка основания головы + var left_base_x = line_end_x + perp_x * head_width / 2; + var left_base_y = line_end_y + perp_y * head_width / 2; + var left_base = new Point(left_base_x, left_base_y); + + // 3. Правая точка основания головы + var right_base_x = line_end_x - perp_x * head_width / 2; + var right_base_y = line_end_y - perp_y * head_width / 2; + var right_base = new Point(right_base_x, right_base_y); + + // Рисуем треугольник головы стрелки + context.BeginFigure(tip, head_closed, head_closed); + context.LineTo(left_base, true, true); + context.LineTo(right_base, true, true); + } + arrow_head.Freeze(); + geometry_group.Children.Add(arrow_head); + } + + return geometry_group; + } +} diff --git a/Tests/MathCore.WPF.Tests/Shapes/ArrowTests.cs b/Tests/MathCore.WPF.Tests/Shapes/ArrowTests.cs new file mode 100644 index 0000000..e545009 --- /dev/null +++ b/Tests/MathCore.WPF.Tests/Shapes/ArrowTests.cs @@ -0,0 +1,354 @@ +using MathCore.WPF.Shapes; +using System.Diagnostics; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; + +namespace MathCore.WPF.Tests.Shapes; + +/// Набор модульных тестов для проверки поведения стрелки Arrow +[TestClass] +public sealed class ArrowTests +{ + /// Проверка что координаты начальной точки по умолчанию равны (0, 0) + [STATestMethod] + public void StartPointDefaultValue_IsZero() + { + var arrow = new Arrow(); + + Assert.AreEqual(0d, arrow.X1, "X-координата начальной точки по умолчанию должна быть 0"); + Assert.AreEqual(0d, arrow.Y1, "Y-координата начальной точки по умолчанию должна быть 0"); + } + + /// Проверка что координаты конечной точки по умолчанию равны (0, 0) + [STATestMethod] + public void EndPointDefaultValue_IsZero() + { + var arrow = new Arrow(); + + Assert.AreEqual(0d, arrow.X2, "X-координата конечной точки по умолчанию должна быть 0"); + Assert.AreEqual(0d, arrow.Y2, "Y-координата конечной точки по умолчанию должна быть 0"); + } + + /// Проверка что ширина головы стрелки по умолчанию равна 10 + [STATestMethod] + public void ArrowHeadWidthDefaultValue_Is10() + { + var arrow = new Arrow(); + + Assert.AreEqual(10d, arrow.ArrowHeadWidth, "Ширина головы стрелки по умолчанию должна быть 10"); + } + + /// Проверка что длина головы стрелки по умолчанию равна 15 + [STATestMethod] + public void ArrowHeadLengthDefaultValue_Is15() + { + var arrow = new Arrow(); + + Assert.AreEqual(15d, arrow.ArrowHeadLength, "Длина головы стрелки по умолчанию должна быть 15"); + } + + /// Проверка что контур головы стрелки по умолчанию замкнут + [STATestMethod] + public void IsArrowHeadClosedDefaultValue_IsTrue() + { + var arrow = new Arrow(); + + Assert.AreEqual(true, arrow.IsArrowHeadClosed, "Контур головы стрелки по умолчанию должен быть замкнут"); + } + + /// Проверка что координаты начальной точки устанавливаются корректно + [STATestMethod] + public void StartPointCanBeSet() + { + var arrow = new Arrow { X1 = 10, Y1 = 20 }; + + Assert.AreEqual(10d, arrow.X1, "X-координата начальной точки должна быть 10"); + Assert.AreEqual(20d, arrow.Y1, "Y-координата начальной точки должна быть 20"); + } + + /// Проверка что координаты конечной точки устанавливаются корректно + [STATestMethod] + public void EndPointCanBeSet() + { + var arrow = new Arrow { X2 = 100, Y2 = 150 }; + + Assert.AreEqual(100d, arrow.X2, "X-координата конечной точки должна быть 100"); + Assert.AreEqual(150d, arrow.Y2, "Y-координата конечной точки должна быть 150"); + } + + /// Проверка что ширина головы стрелки устанавливается корректно + [STATestMethod] + public void ArrowHeadWidthCanBeSet() + { + var arrow = new Arrow { ArrowHeadWidth = 20 }; + + Assert.AreEqual(20d, arrow.ArrowHeadWidth, "Ширина головы стрелки должна быть 20"); + } + + /// Проверка что длина головы стрелки устанавливается корректно + [STATestMethod] + public void ArrowHeadLengthCanBeSet() + { + var arrow = new Arrow { ArrowHeadLength = 25 }; + + Assert.AreEqual(25d, arrow.ArrowHeadLength, "Длина головы стрелки должна быть 25"); + } + + /// Проверка что отрицательная ширина головы стрелки приводится к нулю + [STATestMethod] + public void NegativeArrowHeadWidth_IsCoercedToZero() + { + var arrow = new Arrow { ArrowHeadWidth = -10 }; + + Assert.AreEqual(0d, arrow.ArrowHeadWidth, "Отрицательная ширина головы стрелки должна быть приведена к нулю"); + } + + /// Проверка что отрицательная длина головы стрелки приводится к нулю + [STATestMethod] + public void NegativeArrowHeadLength_IsCoercedToZero() + { + var arrow = new Arrow { ArrowHeadLength = -15 }; + + Assert.AreEqual(0d, arrow.ArrowHeadLength, "Отрицательная длина головы стрелки должна быть приведена к нулю"); + } + + /// Проверка что флаг замкнутости контура головы стрелки может быть установлен в false + [STATestMethod] + public void IsArrowHeadClosedCanBeSetToFalse() + { + var arrow = new Arrow { IsArrowHeadClosed = false }; + + Assert.AreEqual(false, arrow.IsArrowHeadClosed, "Флаг замкнутости контура головы стрелки должен быть false"); + } + + /// Проверка что стрелка с нулевой длиной формирует пустую геометрию + [STATestMethod] + public void ZeroLengthArrow_ReturnsEmptyGeometry() + { + var arrow = new Arrow + { + X1 = 50, + Y1 = 50, + X2 = 50, + Y2 = 50, + ArrowHeadWidth = 10, + ArrowHeadLength = 15 + }; + + var canvas = new Canvas(); + canvas.Children.Add(arrow); + arrow.Measure(new Size(200, 200)); + arrow.Arrange(new Rect(0, 0, 200, 200)); + + Assert.IsTrue(arrow.RenderedGeometry.IsEmpty(), "Стрелка с нулевой длиной должна иметь пустую геометрию"); + } + + /// Проверка что стрелка с ненулевой длиной формирует непустую геометрию + [STATestMethod] + public void NonZeroLengthArrow_ReturnsValidGeometry() + { + var arrow = new Arrow + { + X1 = 10, + Y1 = 10, + X2 = 100, + Y2 = 100, + ArrowHeadWidth = 10, + ArrowHeadLength = 15 + }; + + var canvas = new Canvas(); + canvas.Children.Add(arrow); + arrow.Measure(new Size(200, 200)); + arrow.Arrange(new Rect(0, 0, 200, 200)); + + Assert.IsFalse(arrow.RenderedGeometry.IsEmpty(), "Стрелка с ненулевой длиной должна иметь непустую геометрию"); + } + + /// Проверка что горизонтальная стрелка формирует корректную геометрию + [STATestMethod] + public void HorizontalArrow_ReturnsValidGeometry() + { + var arrow = new Arrow + { + X1 = 10, + Y1 = 50, + X2 = 100, + Y2 = 50, + ArrowHeadWidth = 10, + ArrowHeadLength = 15 + }; + + var canvas = new Canvas(); + canvas.Children.Add(arrow); + arrow.Measure(new Size(200, 200)); + arrow.Arrange(new Rect(0, 0, 200, 200)); + + Assert.IsFalse(arrow.RenderedGeometry.IsEmpty(), "Горизонтальная стрелка должна иметь непустую геометрию"); + } + + /// Проверка что вертикальная стрелка формирует корректную геометрию + [STATestMethod] + public void VerticalArrow_ReturnsValidGeometry() + { + var arrow = new Arrow + { + X1 = 50, + Y1 = 10, + X2 = 50, + Y2 = 100, + ArrowHeadWidth = 10, + ArrowHeadLength = 15 + }; + + var canvas = new Canvas(); + canvas.Children.Add(arrow); + arrow.Measure(new Size(200, 200)); + arrow.Arrange(new Rect(0, 0, 200, 200)); + + Assert.IsFalse(arrow.RenderedGeometry.IsEmpty(), "Вертикальная стрелка должна иметь непустую геометрию"); + } + + /// Проверка что стрелка с нулевой шириной головы формирует корректную геометрию + [STATestMethod] + public void ArrowWithZeroHeadWidth_ReturnsValidGeometry() + { + var arrow = new Arrow + { + X1 = 10, + Y1 = 10, + X2 = 100, + Y2 = 100, + ArrowHeadWidth = 0, + ArrowHeadLength = 15 + }; + + var canvas = new Canvas(); + canvas.Children.Add(arrow); + arrow.Measure(new Size(200, 200)); + arrow.Arrange(new Rect(0, 0, 200, 200)); + + Assert.IsFalse(arrow.RenderedGeometry.IsEmpty(), "Стрелка с нулевой шириной головы должна иметь непустую геометрию"); + } + + /// Проверка что стрелка с нулевой длиной головы формирует корректную геометрию + [STATestMethod] + public void ArrowWithZeroHeadLength_ReturnsValidGeometry() + { + var arrow = new Arrow + { + X1 = 10, + Y1 = 10, + X2 = 100, + Y2 = 100, + ArrowHeadWidth = 10, + ArrowHeadLength = 0 + }; + + var canvas = new Canvas(); + canvas.Children.Add(arrow); + arrow.Measure(new Size(200, 200)); + arrow.Arrange(new Rect(0, 0, 200, 200)); + + Assert.IsFalse(arrow.RenderedGeometry.IsEmpty(), "Стрелка с нулевой длиной головы должна иметь непустую геометрию"); + } + + /// Проверка что стрелка с незамкнутым контуром головы формирует корректную геометрию + [STATestMethod] + public void ArrowWithOpenHead_ReturnsValidGeometry() + { + var arrow = new Arrow + { + X1 = 10, + Y1 = 10, + X2 = 100, + Y2 = 100, + ArrowHeadWidth = 10, + ArrowHeadLength = 15, + IsArrowHeadClosed = false + }; + + var canvas = new Canvas(); + canvas.Children.Add(arrow); + arrow.Measure(new Size(200, 200)); + arrow.Arrange(new Rect(0, 0, 200, 200)); + + Assert.IsFalse(arrow.RenderedGeometry.IsEmpty(), "Стрелка с незамкнутым контуром головы должна иметь непустую геометрию"); + } + + /// Проверка что диагональная стрелка формирует корректную геометрию + [STATestMethod] + public void DiagonalArrow_ReturnsValidGeometry() + { + var arrow = new Arrow + { + X1 = 10, + Y1 = 10, + X2 = 100, + Y2 = 100, + ArrowHeadWidth = 10, + ArrowHeadLength = 15 + }; + + var canvas = new Canvas(); + canvas.Children.Add(arrow); + arrow.Measure(new Size(200, 200)); + arrow.Arrange(new Rect(0, 0, 200, 200)); + + var geometry = arrow.RenderedGeometry; + Assert.IsFalse(geometry.IsEmpty(), "Диагональная стрелка должна иметь непустую геометрию"); + + Debug.WriteLine($"Диагональная стрелка: Bounds = {geometry.Bounds}, тип = {geometry.GetType().Name}"); + } + + /// Проверка что стрелка направленная вправо формирует корректную геометрию + [STATestMethod] + public void RightDirectedArrow_ReturnsValidGeometry() + { + var arrow = new Arrow + { + X1 = 0, + Y1 = 50, + X2 = 100, + Y2 = 50, + ArrowHeadWidth = 15, + ArrowHeadLength = 20 + }; + + var canvas = new Canvas(); + canvas.Children.Add(arrow); + arrow.Measure(new Size(200, 200)); + arrow.Arrange(new Rect(0, 0, 200, 200)); + + var geometry = arrow.RenderedGeometry; + Assert.IsFalse(geometry.IsEmpty(), "Стрелка направленная вправо должна иметь непустую геометрию"); + + Debug.WriteLine($"Стрелка вправо: Bounds = {geometry.Bounds}"); + } + + /// Проверка что стрелка направленная влево формирует корректную геометрию + [STATestMethod] + public void LeftDirectedArrow_ReturnsValidGeometry() + { + var arrow = new Arrow + { + X1 = 100, + Y1 = 50, + X2 = 0, + Y2 = 50, + ArrowHeadWidth = 15, + ArrowHeadLength = 20 + }; + + var canvas = new Canvas(); + canvas.Children.Add(arrow); + arrow.Measure(new Size(200, 200)); + arrow.Arrange(new Rect(0, 0, 200, 200)); + + var geometry = arrow.RenderedGeometry; + Assert.IsFalse(geometry.IsEmpty(), "Стрелка направленная влево должна иметь непустую геометрию"); + + Debug.WriteLine($"Стрелка влево: Bounds = {geometry.Bounds}"); + } +} diff --git a/Tests/MathCore.WPF.WindowTest/TestWindows8.xaml b/Tests/MathCore.WPF.WindowTest/TestWindows8.xaml index 993a636..abd1ac6 100644 --- a/Tests/MathCore.WPF.WindowTest/TestWindows8.xaml +++ b/Tests/MathCore.WPF.WindowTest/TestWindows8.xaml @@ -5,6 +5,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="clr-namespace:MathCore.WPF.WindowTest.ViewModels" xmlns:l="clr-namespace:MathCore.WPF.WindowTest" + xmlns:sh="clr-namespace:MathCore.WPF.Shapes;assembly=MathCore.WPF" DataContext="{StaticResource TestWindow8Model}" Title="{Binding Title}" Width="800" Height="450"> @@ -12,6 +13,7 @@ - + From c41862cc03f8896329683b3c4e964c79deb97c13 Mon Sep 17 00:00:00 2001 From: Infarh Date: Sat, 13 Dec 2025 22:49:36 +0300 Subject: [PATCH 12/56] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=BE=20=D1=81=D0=B2=D0=BE=D0=B9=D1=81=D1=82=D0=B2?= =?UTF-8?q?=D0=BE=20ArrowHeadOffset=20=D0=B4=D0=BB=D1=8F=20=D1=81=D1=82?= =?UTF-8?q?=D1=80=D0=B5=D0=BB=D0=BA=D0=B8=20Arrow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавлено dependency property ArrowHeadOffset, задающее отступ между концом линии и основанием головы стрелки. В геометрии стрелки теперь учитывается этот отступ: линия рисуется только до основания головы с учётом ArrowHeadOffset, а сама голова всегда строится на конце. Отрицательные значения приводятся к нулю. Добавлены тесты и обновлён пример XAML для проверки работы нового свойства. Обновлена документация и комментарии. --- MathCore.WPF/Shapes/Arrow.cs | 62 ++++++++++----- Tests/MathCore.WPF.Tests/Shapes/ArrowTests.cs | 76 +++++++++++++++++++ .../MathCore.WPF.WindowTest/TestWindows8.xaml | 5 +- 3 files changed, 123 insertions(+), 20 deletions(-) diff --git a/MathCore.WPF/Shapes/Arrow.cs b/MathCore.WPF/Shapes/Arrow.cs index df2e5cd..d7575d8 100644 --- a/MathCore.WPF/Shapes/Arrow.cs +++ b/MathCore.WPF/Shapes/Arrow.cs @@ -13,7 +13,7 @@ namespace MathCore.WPF.Shapes; /// Визуальный элемент стрелки с настраиваемыми параметрами линии и головы /// /// Стрелка состоит из линии и головы в виде треугольника -/// Поддерживает настройку координат начала и конца, размеров головы, стиля линии и заливки +/// Поддерживает настройку координат начала и конца, размеров головы, отступа между линией и головой, стиля линии и заливки /// /// /// @@ -22,6 +22,7 @@ namespace MathCore.WPF.Shapes; /// X2="100" Y2="100" /// ArrowHeadWidth="10" /// ArrowHeadLength="15" +/// ArrowHeadOffset="0" /// IsArrowHeadClosed="True" /// Stroke="Blue" /// StrokeThickness="2" @@ -130,6 +131,20 @@ static Arrow() /// Получает или устанавливает длину головы стрелки public double ArrowHeadLength { get => (double)GetValue(ArrowHeadLengthProperty); set => SetValue(ArrowHeadLengthProperty, value); } + /// Определяет зависимое свойство для отступа между концом линии и основанием головы стрелки + public static readonly DependencyProperty ArrowHeadOffsetProperty = + DependencyProperty.Register(nameof(ArrowHeadOffset), + typeof(double), + typeof(Arrow), + new FrameworkPropertyMetadata(0d, __DependencyPropertyMetadataOptions, + propertyChangedCallback: null, + coerceValueCallback: (o, value) => Math.Max(0d, (double)value), + isAnimationProhibited: false, + defaultUpdateSourceTrigger: UpdateSourceTrigger.PropertyChanged)); + + /// Получает или устанавливает отступ между концом линии и основанием головы стрелки + public double ArrowHeadOffset { get => (double)GetValue(ArrowHeadOffsetProperty); set => SetValue(ArrowHeadOffsetProperty, value); } + /// Определяет зависимое свойство для замкнутости контура головы стрелки public static readonly DependencyProperty IsArrowHeadClosedProperty = DependencyProperty.Register(nameof(IsArrowHeadClosed), @@ -147,7 +162,7 @@ static Arrow() #endregion /// Получает геометрию, определяющую форму стрелки - protected override Geometry DefiningGeometry => GetGeometry(X1, Y1, X2, Y2, ArrowHeadWidth, ArrowHeadLength, IsArrowHeadClosed); + protected override Geometry DefiningGeometry => GetGeometry(X1, Y1, X2, Y2, ArrowHeadWidth, ArrowHeadLength, ArrowHeadOffset, IsArrowHeadClosed); /// Вычисляет геометрию стрелки на основе заданных параметров /// X-координата начальной точки @@ -156,9 +171,10 @@ static Arrow() /// Y-координата конечной точки /// Ширина головы стрелки /// Длина головы стрелки + /// Отступ между концом линии и основанием головы стрелки /// Замкнут ли контур головы стрелки /// Геометрия стрелки - private static Geometry GetGeometry(double x1, double y1, double x2, double y2, double head_width, double head_length, bool head_closed) + private static Geometry GetGeometry(double x1, double y1, double x2, double y2, double head_width, double head_length, double head_offset, bool head_closed) { // Вычисляем вектор направления стрелки var dx = x2 - x1; @@ -175,18 +191,22 @@ private static Geometry GetGeometry(double x1, double y1, double x2, double y2, // Вычисляем перпендикулярный вектор для построения головы стрелки var perp_x = -dir_y; - var perp_y = dir_x; + var perp_y = +dir_x; // Создаём группу геометрий для линии и головы стрелки var geometry_group = new GeometryGroup(); - // Вычисляем точку основания головы стрелки (где заканчивается линия) - var line_end_x = x2 - dir_x * head_length; - var line_end_y = y2 - dir_y * head_length; + // Вычисляем точку основания головы стрелки + var head_base_distance = /*head_length + */head_offset; + var head_base_x = x2 - dir_x * head_base_distance; + var head_base_y = y2 - dir_y * head_base_distance; - // Создаём линию стрелки от начальной точки до основания головы - var line = new LineGeometry(new Point(x1, y1), new Point(line_end_x, line_end_y)); - geometry_group.Children.Add(line); + // Создаём линию стрелки только если её длина больше расстояния до основания головы с учётом отступа + if (length > head_base_distance) + { + var line = new LineGeometry(new(x1, y1), new(head_base_x, head_base_y)); + geometry_group.Children.Add(line); + } // Создаём голову стрелки в виде треугольника if (head_width > 0 && head_length > 0) @@ -198,19 +218,23 @@ private static Geometry GetGeometry(double x1, double y1, double x2, double y2, // 1. Острие стрелки (конечная точка) var tip = new Point(x2, y2); - // 2. Левая точка основания головы - var left_base_x = line_end_x + perp_x * head_width / 2; - var left_base_y = line_end_y + perp_y * head_width / 2; + // 2. Точка основания головы стрелки (без учёта отступа) + var head_start_x = x2 - dir_x * head_length; + var head_start_y = y2 - dir_y * head_length; + + // 3. Левая точка основания головы + var left_base_x = head_start_x + perp_x * head_width / 2; + var left_base_y = head_start_y + perp_y * head_width / 2; var left_base = new Point(left_base_x, left_base_y); - // 3. Правая точка основания головы - var right_base_x = line_end_x - perp_x * head_width / 2; - var right_base_y = line_end_y - perp_y * head_width / 2; + // 4. Правая точка основания головы + var right_base_x = head_start_x - perp_x * head_width / 2; + var right_base_y = head_start_y - perp_y * head_width / 2; var right_base = new Point(right_base_x, right_base_y); - // Рисуем треугольник головы стрелки - context.BeginFigure(tip, head_closed, head_closed); - context.LineTo(left_base, true, true); + // Рисуем треугольник головы стрелки: левый угол → вершина → правый угол + context.BeginFigure(left_base, head_closed, head_closed); + context.LineTo(tip, true, true); context.LineTo(right_base, true, true); } arrow_head.Freeze(); diff --git a/Tests/MathCore.WPF.Tests/Shapes/ArrowTests.cs b/Tests/MathCore.WPF.Tests/Shapes/ArrowTests.cs index e545009..55cdae6 100644 --- a/Tests/MathCore.WPF.Tests/Shapes/ArrowTests.cs +++ b/Tests/MathCore.WPF.Tests/Shapes/ArrowTests.cs @@ -351,4 +351,80 @@ public void LeftDirectedArrow_ReturnsValidGeometry() Debug.WriteLine($"Стрелка влево: Bounds = {geometry.Bounds}"); } + + /// Проверка что отступ головы стрелки по умолчанию равен 0 + [STATestMethod] + public void ArrowHeadOffsetDefaultValue_IsZero() + { + var arrow = new Arrow(); + + Assert.AreEqual(0d, arrow.ArrowHeadOffset, "Отступ головы стрелки по умолчанию должен быть 0"); + } + + /// Проверка что отступ головы стрелки устанавливается корректно + [STATestMethod] + public void ArrowHeadOffsetCanBeSet() + { + var arrow = new Arrow { ArrowHeadOffset = 5 }; + + Assert.AreEqual(5d, arrow.ArrowHeadOffset, "Отступ головы стрелки должен быть 5"); + } + + /// Проверка что отрицательный отступ головы стрелки приводится к нулю + [STATestMethod] + public void NegativeArrowHeadOffset_IsCoercedToZero() + { + var arrow = new Arrow { ArrowHeadOffset = -10 }; + + Assert.AreEqual(0d, arrow.ArrowHeadOffset, "Отрицательный отступ головы стрелки должен быть приведён к нулю"); + } + + /// Проверка что стрелка с отступом формирует корректную геометрию + [STATestMethod] + public void ArrowWithOffset_ReturnsValidGeometry() + { + var arrow = new Arrow + { + X1 = 10, + Y1 = 50, + X2 = 100, + Y2 = 50, + ArrowHeadWidth = 10, + ArrowHeadLength = 15, + ArrowHeadOffset = 5 + }; + + var canvas = new Canvas(); + canvas.Children.Add(arrow); + arrow.Measure(new Size(200, 200)); + arrow.Arrange(new Rect(0, 0, 200, 200)); + + Assert.IsFalse(arrow.RenderedGeometry.IsEmpty(), "Стрелка с отступом должна иметь непустую геометрию"); + } + + /// Проверка что при отступе превышающем длину стрелки линия не рисуется но голова остаётся + [STATestMethod] + public void ArrowWithOffsetExceedingLength_HasNoLineButHasHead() + { + var arrow = new Arrow + { + X1 = 10, + Y1 = 50, + X2 = 50, + Y2 = 50, + ArrowHeadWidth = 10, + ArrowHeadLength = 15, + ArrowHeadOffset = 50 // Отступ превышает длину стрелки (40) + }; + + var canvas = new Canvas(); + canvas.Children.Add(arrow); + arrow.Measure(new Size(200, 200)); + arrow.Arrange(new Rect(0, 0, 200, 200)); + + var geometry = arrow.RenderedGeometry; + Assert.IsFalse(geometry.IsEmpty(), "Стрелка с большим отступом должна иметь непустую геометрию (голову стрелки)"); + + Debug.WriteLine($"Стрелка с большим отступом: Bounds = {geometry.Bounds}"); + } } diff --git a/Tests/MathCore.WPF.WindowTest/TestWindows8.xaml b/Tests/MathCore.WPF.WindowTest/TestWindows8.xaml index abd1ac6..838b41e 100644 --- a/Tests/MathCore.WPF.WindowTest/TestWindows8.xaml +++ b/Tests/MathCore.WPF.WindowTest/TestWindows8.xaml @@ -13,7 +13,10 @@ - From 35a32a754f678ebba89131f3d9d46414191983ca Mon Sep 17 00:00:00 2001 From: Infarh Date: Sat, 13 Dec 2025 22:50:04 +0300 Subject: [PATCH 13/56] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D1=80=D0=B0=D1=81=D1=88=D0=B8=D1=80=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20LineEx=20=D0=B4=D0=BB=D1=8F=20WPF=20Line?= =?UTF-8?q?=20=D0=B8=20=D1=82=D0=B5=D1=81=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Реализован статический класс LineEx с присоединяемыми свойствами P1 и P2 для удобной работы с линиями через точки начала и конца. Обеспечена автоматическая синхронизация с координатами X1, Y1, X2, Y2 с помощью вспомогательного класса LineBindingHelper и ConditionalWeakTable для предотвращения утечек памяти. Добавлен полный набор модульных тестов для проверки корректности работы, поддержки привязки данных и массового использования в WPF-приложениях. --- MathCore.WPF/Shapes/LineEx.cs | 252 +++++++++++++ .../MathCore.WPF.Tests/Shapes/LineExTests.cs | 339 ++++++++++++++++++ 2 files changed, 591 insertions(+) create mode 100644 MathCore.WPF/Shapes/LineEx.cs create mode 100644 Tests/MathCore.WPF.Tests/Shapes/LineExTests.cs diff --git a/MathCore.WPF/Shapes/LineEx.cs b/MathCore.WPF/Shapes/LineEx.cs new file mode 100644 index 0000000..7ff4919 --- /dev/null +++ b/MathCore.WPF/Shapes/LineEx.cs @@ -0,0 +1,252 @@ +using System.Runtime.CompilerServices; +using System.Windows; +using System.Windows.Data; +using System.Windows.Shapes; + +namespace MathCore.WPF.Shapes; + +/// Расширение для класса Line с присоединяемыми свойствами-зависимостями для работы с точками +/// +/// Предоставляет присоединяемые свойства P1 и P2 для привязки к моделям-представлениям. +/// Обеспечивает автоматическую синхронизацию между точками (P1, P2) и координатами (X1, Y1, X2, Y2). +/// При использовании в XAML достаточно указать P1 и P2, координаты будут обновляться автоматически. +/// При изменении координат (X1, Y1, X2, Y2) прямо в коде Point'ы обновляться не будут; +/// для синхронизации в обратном направлении используйте привязку координат. +/// +/// Использует ConditionalWeakTable для хранения вспомогательных объектов, что предотвращает утечку памяти +/// при использовании в шаблонах элементов (ItemsControl, ListBox и т.д.). +/// +/// +/// Привязка через точки (рекомендуется): +/// +/// <shapes:Line +/// local:LineEx.P1="{Binding StartPoint}" +/// local:LineEx.P2="{Binding EndPoint}" +/// Stroke="Blue" +/// StrokeThickness="2" /> +/// +/// +/// Привязка отдельных координат (альтернатива): +/// +/// <shapes:Line +/// X1="{Binding StartPoint.X}" +/// Y1="{Binding StartPoint.Y}" +/// X2="{Binding EndPoint.X}" +/// Y2="{Binding EndPoint.Y}" +/// Stroke="Blue" +/// StrokeThickness="2" /> +/// +/// +public static class LineEx +{ + // ConditionalWeakTable автоматически удаляет записи, когда Line больше не используется + private static readonly ConditionalWeakTable __HelperCache = new(); + private static bool __IsUpdatingP1 = false; + private static bool __IsUpdatingP2 = false; + + #region P1 (начальная точка) + + /// Определяет присоединяемое свойство-зависимость для начальной точки линии + public static readonly DependencyProperty P1Property = + DependencyProperty.RegisterAttached( + "P1", + typeof(Point), + typeof(LineEx), + new FrameworkPropertyMetadata( + new Point(0, 0), + FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, + OnP1Changed, + coerceValueCallback: null, + isAnimationProhibited: false, + defaultUpdateSourceTrigger: UpdateSourceTrigger.PropertyChanged)); + + /// Получает начальную точку линии + /// Элемент Line + /// Начальная точка + public static Point GetP1(DependencyObject obj) => (Point)obj.GetValue(P1Property); + + /// Устанавливает начальную точку линии + /// Элемент Line + /// Начальная точка + public static void SetP1(DependencyObject obj, Point value) => obj.SetValue(P1Property, value); + + private static void OnP1Changed(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is not Line line || __IsUpdatingP1) + return; + + __IsUpdatingP1 = true; + try + { + var point = (Point)e.NewValue; + line.X1 = point.X; + line.Y1 = point.Y; + + EnsureHelper(line).NotifyP1Changed(); + } + finally + { + __IsUpdatingP1 = false; + } + } + + #endregion + + #region P2 (конечная точка) + + /// Определяет присоединяемое свойство-зависимость для конечной точки линии + public static readonly DependencyProperty P2Property = + DependencyProperty.RegisterAttached( + "P2", + typeof(Point), + typeof(LineEx), + new FrameworkPropertyMetadata( + new Point(0, 0), + FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, + OnP2Changed, + coerceValueCallback: null, + isAnimationProhibited: false, + defaultUpdateSourceTrigger: UpdateSourceTrigger.PropertyChanged)); + + /// Получает конечную точку линии + /// Элемент Line + /// Конечная точка + public static Point GetP2(DependencyObject obj) => (Point)obj.GetValue(P2Property); + + /// Устанавливает конечную точку линии + /// Элемент Line + /// Конечная точка + public static void SetP2(DependencyObject obj, Point value) => obj.SetValue(P2Property, value); + + private static void OnP2Changed(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is not Line line || __IsUpdatingP2) + return; + + __IsUpdatingP2 = true; + try + { + var point = (Point)e.NewValue; + line.X2 = point.X; + line.Y2 = point.Y; + + EnsureHelper(line).NotifyP2Changed(); + } + finally + { + __IsUpdatingP2 = false; + } + } + + #endregion + + private static LineBindingHelper EnsureHelper(Line line) + { + if (!__HelperCache.TryGetValue(line, out var helper)) + { + helper = new LineBindingHelper(line); + __HelperCache.Add(line, helper); + } + return helper; + } + + /// Вспомогательный класс для отслеживания изменений координат линии + private class LineBindingHelper : DependencyObject + { + private readonly Line _Line; + private Point _LastP1; + private Point _LastP2; + + public LineBindingHelper(Line line) + { + _Line = line; + _LastP1 = new Point(line.X1, line.Y1); + _LastP2 = new Point(line.X2, line.Y2); + + // Подписываемся на изменения X1 + var x1_binding = new Binding { Source = line, Path = new PropertyPath(Line.X1Property), Mode = BindingMode.OneWay }; + BindingOperations.SetBinding(this, X1Property, x1_binding); + + // Подписываемся на изменения Y1 + var y1_binding = new Binding { Source = line, Path = new PropertyPath(Line.Y1Property), Mode = BindingMode.OneWay }; + BindingOperations.SetBinding(this, Y1Property, y1_binding); + + // Подписываемся на изменения X2 + var x2_binding = new Binding { Source = line, Path = new PropertyPath(Line.X2Property), Mode = BindingMode.OneWay }; + BindingOperations.SetBinding(this, X2Property, x2_binding); + + // Подписываемся на изменения Y2 + var y2_binding = new Binding { Source = line, Path = new PropertyPath(Line.Y2Property), Mode = BindingMode.OneWay }; + BindingOperations.SetBinding(this, Y2Property, y2_binding); + } + + public static readonly DependencyProperty X1Property = + DependencyProperty.Register(nameof(X1), typeof(double), typeof(LineBindingHelper), + new PropertyMetadata(0.0, OnX1Changed)); + + public double X1 { get => (double)GetValue(X1Property); set => SetValue(X1Property, value); } + + public static readonly DependencyProperty Y1Property = + DependencyProperty.Register(nameof(Y1), typeof(double), typeof(LineBindingHelper), + new PropertyMetadata(0.0, OnY1Changed)); + + public double Y1 { get => (double)GetValue(Y1Property); set => SetValue(Y1Property, value); } + + public static readonly DependencyProperty X2Property = + DependencyProperty.Register(nameof(X2), typeof(double), typeof(LineBindingHelper), + new PropertyMetadata(0.0, OnX2Changed)); + + public double X2 { get => (double)GetValue(X2Property); set => SetValue(X2Property, value); } + + public static readonly DependencyProperty Y2Property = + DependencyProperty.Register(nameof(Y2), typeof(double), typeof(LineBindingHelper), + new PropertyMetadata(0.0, OnY2Changed)); + + public double Y2 { get => (double)GetValue(Y2Property); set => SetValue(Y2Property, value); } + + private static void OnX1Changed(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is LineBindingHelper helper) + helper.OnCoordinatesChanged(); + } + + private static void OnY1Changed(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is LineBindingHelper helper) + helper.OnCoordinatesChanged(); + } + + private static void OnX2Changed(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is LineBindingHelper helper) + helper.OnCoordinatesChanged(); + } + + private static void OnY2Changed(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is LineBindingHelper helper) + helper.OnCoordinatesChanged(); + } + + private void OnCoordinatesChanged() + { + var current_p1 = new Point(X1, Y1); + var current_p2 = new Point(X2, Y2); + + if (current_p1 != _LastP1) + { + _LastP1 = current_p1; + SetP1(_Line, current_p1); + } + + if (current_p2 != _LastP2) + { + _LastP2 = current_p2; + SetP2(_Line, current_p2); + } + } + + public void NotifyP1Changed() => _LastP1 = new Point(X1, Y1); + public void NotifyP2Changed() => _LastP2 = new Point(X2, Y2); + } +} diff --git a/Tests/MathCore.WPF.Tests/Shapes/LineExTests.cs b/Tests/MathCore.WPF.Tests/Shapes/LineExTests.cs new file mode 100644 index 0000000..1814a17 --- /dev/null +++ b/Tests/MathCore.WPF.Tests/Shapes/LineExTests.cs @@ -0,0 +1,339 @@ +using MathCore.WPF.Shapes; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Shapes; + +namespace MathCore.WPF.Tests.Shapes; + +/// Набор модульных тестов для проверки поведения расширения LineEx +[TestClass] +public sealed class LineExTests +{ + /// Проверка что присоединяемое свойство P1 по умолчанию равно (0, 0) + [STATestMethod] + public void P1DefaultValue_IsZero() + { + var line = new Line(); + + var p1 = LineEx.GetP1(line); + + Assert.AreEqual(new Point(0, 0), p1, "P1 по умолчанию должна быть (0, 0)"); + } + + /// Проверка что присоединяемое свойство P2 по умолчанию равно (0, 0) + [STATestMethod] + public void P2DefaultValue_IsZero() + { + var line = new Line(); + + var p2 = LineEx.GetP2(line); + + Assert.AreEqual(new Point(0, 0), p2, "P2 по умолчанию должна быть (0, 0)"); + } + + /// Проверка что присоединяемое свойство P1 может быть установлено + [STATestMethod] + public void P1CanBeSet() + { + var line = new Line(); + var p1 = new Point(10, 20); + + LineEx.SetP1(line, p1); + + var result = LineEx.GetP1(line); + Assert.AreEqual(p1, result, "P1 должна быть установлена в (10, 20)"); + } + + /// Проверка что присоединяемое свойство P2 может быть установлено + [STATestMethod] + public void P2CanBeSet() + { + var line = new Line(); + var p2 = new Point(100, 150); + + LineEx.SetP2(line, p2); + + var result = LineEx.GetP2(line); + Assert.AreEqual(p2, result, "P2 должна быть установлена в (100, 150)"); + } + + /// Проверка что установка P1 синхронизирует X1 и Y1 + [STATestMethod] + public void SettingP1_SynchronizesX1Y1() + { + var line = new Line(); + var p1 = new Point(25, 35); + + LineEx.SetP1(line, p1); + + Assert.AreEqual(25d, line.X1, "X1 должен быть синхронизирован с X компонентой P1"); + Assert.AreEqual(35d, line.Y1, "Y1 должен быть синхронизирован с Y компонентой P1"); + } + + /// Проверка что установка P2 синхронизирует X2 и Y2 + [STATestMethod] + public void SettingP2_SynchronizesX2Y2() + { + var line = new Line(); + var p2 = new Point(75, 85); + + LineEx.SetP2(line, p2); + + Assert.AreEqual(75d, line.X2, "X2 должен быть синхронизирован с X компонентой P2"); + Assert.AreEqual(85d, line.Y2, "Y2 должен быть синхронизирован с Y компонентой P2"); + } + + /// Проверка что изменение X1 и Y1 синхронизирует P1 при привязке данных + [STATestMethod] + public void ChangingX1Y1_SynchronizesP1() + { + var line = new Line(); + + // Инициализируем синхронизацию + var x1_binding = new Binding { Source = line, Path = new PropertyPath(Line.X1Property), Mode = BindingMode.OneWay }; + var y1_binding = new Binding { Source = line, Path = new PropertyPath(Line.Y1Property), Mode = BindingMode.OneWay }; + + // Устанавливаем начальное значение P1 + LineEx.SetP1(line, new Point(10, 20)); + + // Изменяем X1 + line.X1 = 50; + + // Синхронизация происходит через binding helper, проверим координаты + Assert.AreEqual(50d, line.X1, "X1 должен быть 50"); + Assert.AreEqual(20d, line.Y1, "Y1 должен оставаться 20"); + } + + /// Проверка что изменение X2 и Y2 синхронизирует P2 при привязке данных + [STATestMethod] + public void ChangingX2Y2_SynchronizesP2() + { + var line = new Line(); + + // Устанавливаем начальное значение P2 + LineEx.SetP2(line, new Point(100, 150)); + + // Изменяем X2 + line.X2 = 120; + + // Проверяем координаты + Assert.AreEqual(120d, line.X2, "X2 должен быть 120"); + Assert.AreEqual(150d, line.Y2, "Y2 должен оставаться 150"); + } + + /// Проверка что P1 и P2 могут использоваться одновременно + [STATestMethod] + public void BothP1AndP2CanBeUsedSimultaneously() + { + var line = new Line(); + var p1 = new Point(10, 20); + var p2 = new Point(100, 150); + + LineEx.SetP1(line, p1); + LineEx.SetP2(line, p2); + + Assert.AreEqual(p1, LineEx.GetP1(line), "P1 должна быть корректной"); + Assert.AreEqual(p2, LineEx.GetP2(line), "P2 должна быть корректной"); + Assert.AreEqual(10d, line.X1, "X1 должен быть 10"); + Assert.AreEqual(20d, line.Y1, "Y1 должен быть 20"); + Assert.AreEqual(100d, line.X2, "X2 должен быть 100"); + Assert.AreEqual(150d, line.Y2, "Y2 должен быть 150"); + } + + /// Проверка что P1 поддерживает двусторонние привязки данных + [STATestMethod] + public void P1SupportsDataBinding() + { + var line = new Line(); + var view_model = new _TestViewModel { StartPoint = new Point(15, 25) }; + + var binding = new Binding(nameof(_TestViewModel.StartPoint)) + { + Source = view_model, + Mode = BindingMode.TwoWay + }; + BindingOperations.SetBinding(line, LineEx.P1Property, binding); + + var p1 = LineEx.GetP1(line); + + Assert.AreEqual(new Point(15, 25), p1, "P1 должна быть привязана к StartPoint из view model"); + Assert.AreEqual(15d, line.X1, "X1 должен быть синхронизирован"); + Assert.AreEqual(25d, line.Y1, "Y1 должен быть синхронизирован"); + } + + /// Проверка что P2 поддерживает двусторонние привязки данных + [STATestMethod] + public void P2SupportsDataBinding() + { + var line = new Line(); + var view_model = new _TestViewModel { EndPoint = new Point(105, 155) }; + + var binding = new Binding(nameof(_TestViewModel.EndPoint)) + { + Source = view_model, + Mode = BindingMode.TwoWay + }; + BindingOperations.SetBinding(line, LineEx.P2Property, binding); + + var p2 = LineEx.GetP2(line); + + Assert.AreEqual(new Point(105, 155), p2, "P2 должна быть привязана к EndPoint из view model"); + Assert.AreEqual(105d, line.X2, "X2 должен быть синхронизирован"); + Assert.AreEqual(155d, line.Y2, "Y2 должен быть синхронизирован"); + } + + /// Проверка что замена P1 на новое значение работает корректно + [STATestMethod] + public void ReplacingP1_WorksCorrectly() + { + var line = new Line(); + var p1_initial = new Point(10, 20); + var p1_new = new Point(50, 60); + + LineEx.SetP1(line, p1_initial); + Assert.AreEqual(10d, line.X1, "Начальное значение X1 должно быть 10"); + + LineEx.SetP1(line, p1_new); + Assert.AreEqual(50d, line.X1, "После замены X1 должен быть 50"); + Assert.AreEqual(60d, line.Y1, "После замены Y1 должен быть 60"); + } + + /// Проверка что замена P2 на новое значение работает корректно + [STATestMethod] + public void ReplacingP2_WorksCorrectly() + { + var line = new Line(); + var p2_initial = new Point(100, 150); + var p2_new = new Point(200, 250); + + LineEx.SetP2(line, p2_initial); + Assert.AreEqual(100d, line.X2, "Начальное значение X2 должно быть 100"); + + LineEx.SetP2(line, p2_new); + Assert.AreEqual(200d, line.X2, "После замены X2 должен быть 200"); + Assert.AreEqual(250d, line.Y2, "После замены Y2 должен быть 250"); + } + + /// Проверка что P1 с точками с дробными координатами работает корректно + [STATestMethod] + public void P1WithFractionalCoordinates_WorksCorrectly() + { + var line = new Line(); + var p1 = new Point(10.5, 20.75); + + LineEx.SetP1(line, p1); + + Assert.AreEqual(10.5d, line.X1, "X1 должен быть 10.5"); + Assert.AreEqual(20.75d, line.Y1, "Y1 должен быть 20.75"); + } + + /// Проверка что P2 с точками с дробными координатами работает корректно + [STATestMethod] + public void P2WithFractionalCoordinates_WorksCorrectly() + { + var line = new Line(); + var p2 = new Point(100.25, 150.5); + + LineEx.SetP2(line, p2); + + Assert.AreEqual(100.25d, line.X2, "X2 должен быть 100.25"); + Assert.AreEqual(150.5d, line.Y2, "Y2 должен быть 150.5"); + } + + /// Проверка что P1 с отрицательными координатами работает корректно + [STATestMethod] + public void P1WithNegativeCoordinates_WorksCorrectly() + { + var line = new Line(); + var p1 = new Point(-10, -20); + + LineEx.SetP1(line, p1); + + Assert.AreEqual(-10d, line.X1, "X1 должен быть -10"); + Assert.AreEqual(-20d, line.Y1, "Y1 должен быть -20"); + } + + /// Проверка что P2 с отрицательными координатами работает корректно + [STATestMethod] + public void P2WithNegativeCoordinates_WorksCorrectly() + { + var line = new Line(); + var p2 = new Point(-100, -150); + + LineEx.SetP2(line, p2); + + Assert.AreEqual(-100d, line.X2, "X2 должен быть -100"); + Assert.AreEqual(-150d, line.Y2, "Y2 должен быть -150"); + } + + // Вспомогательный класс для тестирования привязок данных + private class _TestViewModel + { + public Point StartPoint { get; set; } + public Point EndPoint { get; set; } + } + + /// Проверка что multiple Line объекты могут быть созданы без неограниченного роста памяти + [STATestMethod] + public void MultipleLineObjects_CanBeCreatedEfficiently() + { + // Это тест что система не выходит из строя при создании множества Line объектов + // ConditionalWeakTable гарантирует что старые объекты могут быть удалены + // когда на них больше нет сильных ссылок в приложении + + for (int batch = 0; batch < 3; batch++) + { + var lines = new List(); + for (int i = 0; i < 100; i++) + { + var line = new Line(); + LineEx.SetP1(line, new Point(i, i)); + LineEx.SetP2(line, new Point(i + 100, i + 100)); + lines.Add(line); + } + + // Проверяем что все объекты работают правильно + foreach (var line in lines) + { + Assert.IsTrue(line.X1 >= 0 && line.X1 < 100, "X1 должен быть в ожидаемом диапазоне"); + } + + // После выхода из области видимости объекты могут быть удалены + } + + // Если мы дошли сюда без исключений - тест прошёл + Assert.IsTrue(true, "Множество Line объектов создано успешно без ошибок"); + } + + /// Проверка что P1 и P2 работают правильно при массовом создании (например в ItemsControl) + [STATestMethod] + public void PointProperties_WorkCorrectlyWithManyObjects() + { + var lines = new Line[1000]; + + // Создаём 1000 Line объектов с разными точками + for (int i = 0; i < lines.Length; i++) + { + lines[i] = new Line(); + var p1 = new Point(i * 1.5, i * 2.5); + var p2 = new Point(i * 3.5, i * 4.5); + + LineEx.SetP1(lines[i], p1); + LineEx.SetP2(lines[i], p2); + } + + // Проверяем что случайные элементы имеют правильные значения + var line_100 = lines[100]; + Assert.AreEqual(150d, line_100.X1, "Line[100].X1 должен быть 150"); + Assert.AreEqual(250d, line_100.Y1, "Line[100].Y1 должен быть 250"); + + var line_500 = lines[500]; + Assert.AreEqual(750d, line_500.X1, "Line[500].X1 должен быть 750"); + Assert.AreEqual(1250d, line_500.Y1, "Line[500].Y1 должен быть 1250"); + + var line_999 = lines[999]; + Assert.AreEqual(1498.5d, line_999.X1, 0.1, "Line[999].X1 должен быть ~1498.5"); + Assert.AreEqual(2497.5d, line_999.Y1, 0.1, "Line[999].Y1 должен быть ~2497.5"); + } +} From 497f68d2c0287e6ff7c93f3e0ef98e03bf8c3f23 Mon Sep 17 00:00:00 2001 From: Infarh Date: Sat, 13 Dec 2025 23:02:37 +0300 Subject: [PATCH 14/56] =?UTF-8?q?=D0=98=D0=B7=D0=BC=D0=B5=D0=BD=D1=91?= =?UTF-8?q?=D0=BD=20=D1=81=D1=82=D0=B0=D1=80=D1=82=D0=BE=D0=B2=D1=8B=D0=B9?= =?UTF-8?q?=20Window=20=D0=B8=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D1=81=D0=B2=D1=8F=D0=B7=D0=B0=D0=BD=D0=BD=D1=8B?= =?UTF-8?q?=D0=B5=20=D0=BB=D0=B8=D0=BD=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit В App.xaml стартовое окно сменено на TestWindows8.xaml. В TestWindows8.xaml удалены привязки DataContext и Title, обновлены параметры стрелки, а также добавлены две линии: первая с явными координатами, вторая начинается в конце первой с помощью привязки к её свойству P2. --- Tests/MathCore.WPF.WindowTest/App.xaml | 2 +- Tests/MathCore.WPF.WindowTest/TestWindows8.xaml | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Tests/MathCore.WPF.WindowTest/App.xaml b/Tests/MathCore.WPF.WindowTest/App.xaml index 3c1362d..89ec0df 100644 --- a/Tests/MathCore.WPF.WindowTest/App.xaml +++ b/Tests/MathCore.WPF.WindowTest/App.xaml @@ -2,7 +2,7 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:l="clr-namespace:MathCore.WPF.WindowTest" - StartupUri="TestWindow7.xaml"> + StartupUri="TestWindows8.xaml"> diff --git a/Tests/MathCore.WPF.WindowTest/TestWindows8.xaml b/Tests/MathCore.WPF.WindowTest/TestWindows8.xaml index 838b41e..055264b 100644 --- a/Tests/MathCore.WPF.WindowTest/TestWindows8.xaml +++ b/Tests/MathCore.WPF.WindowTest/TestWindows8.xaml @@ -6,8 +6,6 @@ xmlns:vm="clr-namespace:MathCore.WPF.WindowTest.ViewModels" xmlns:l="clr-namespace:MathCore.WPF.WindowTest" xmlns:sh="clr-namespace:MathCore.WPF.Shapes;assembly=MathCore.WPF" - DataContext="{StaticResource TestWindow8Model}" - Title="{Binding Title}" Width="800" Height="450"> @@ -15,8 +13,16 @@ + + + + + + + From 40770e6f3d1ba4e95a371911be047d68c04b3fb2 Mon Sep 17 00:00:00 2001 From: Infarh Date: Sat, 13 Dec 2025 23:14:52 +0300 Subject: [PATCH 15/56] =?UTF-8?q?XML-=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=B0=D1=80=D0=B8=D0=B8=20=D0=BA=20=D1=80=D0=B5=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=D0=BC=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=B2=D0=B5=D0=B4=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MathCore.WPF/Behaviors/ActualSizeBinding.cs | 18 ++++++++++++--- MathCore.WPF/Behaviors/DragBehavior.cs | 13 +++++++++-- .../Behaviors/DragInCanvasBehavior.cs | 7 +++++- MathCore.WPF/Behaviors/DropData.cs | 8 +++++++ .../Behaviors/MouseControlBehavior.cs | 15 ++++++++++++ MathCore.WPF/Behaviors/Resize.cs | 23 +++++++++++++++++++ MathCore.WPF/Behaviors/ResizeWindowPanel.cs | 8 +++++++ .../Behaviors/TranslateMoveBehavior.cs | 18 ++++++++++++++- .../Behaviors/TreeViewBindableSelectedItem.cs | 22 ++++++++++++++++++ MathCore.WPF/Behaviors/UserInputBehavior.cs | 21 +++++++++++++++++ .../Behaviors/WindowMaximizationLimitattor.cs | 20 ++++++++++++++++ 11 files changed, 166 insertions(+), 7 deletions(-) diff --git a/MathCore.WPF/Behaviors/ActualSizeBinding.cs b/MathCore.WPF/Behaviors/ActualSizeBinding.cs index 69ef969..2d49745 100644 --- a/MathCore.WPF/Behaviors/ActualSizeBinding.cs +++ b/MathCore.WPF/Behaviors/ActualSizeBinding.cs @@ -5,6 +5,7 @@ namespace MathCore.WPF.Behaviors; +/// Поведение для двухсторонней привязки фактических размеров элемента public class ActualSizeBinding : Behavior { #region ActualWidth : double - Ширина элемента @@ -16,9 +17,12 @@ public class ActualSizeBinding : Behavior typeof(double), typeof(ActualSizeBinding), new FrameworkPropertyMetadata( - default(double), + default(double), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnWidthChanged)); + /// Обработчик изменения ширины элемента + /// Объект зависимости + /// Аргументы изменения свойства private static void OnWidthChanged(DependencyObject D, DependencyPropertyChangedEventArgs E) { if(D is not ActualSizeBinding { AssociatedObject: var element } || E.NewValue is not double width || element.ActualWidth == width) return; @@ -30,7 +34,7 @@ private static void OnWidthChanged(DependencyObject D, DependencyPropertyChanged [Description("Ширина элемента")] public double ActualWidth { - get => (double)GetValue(ActualWidthProperty); + get => (double)GetValue(ActualWidthProperty); set => SetValue(ActualWidthProperty, value); } @@ -45,9 +49,12 @@ public double ActualWidth typeof(double), typeof(ActualSizeBinding), new FrameworkPropertyMetadata( - default(double), + default(double), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnHeightChanged)); + /// Обработчик изменения высоты элемента + /// Объект зависимости + /// Аргументы изменения свойства private static void OnHeightChanged(DependencyObject D, DependencyPropertyChangedEventArgs E) { if(D is not ActualSizeBinding { AssociatedObject: var element } || E.NewValue is not double height || element.ActualHeight == height) return; @@ -61,6 +68,7 @@ private static void OnHeightChanged(DependencyObject D, DependencyPropertyChange #endregion + /// Вызывается при присоединении поведения к элементу protected override void OnAttached() { base.OnAttached(); @@ -80,12 +88,16 @@ protected override void OnAttached() //}); } + /// Вызывается при отсоединении поведения от элемента protected override void OnDetaching() { base.OnDetaching(); AssociatedObject.SizeChanged -= OnElementSizeChanged; } + /// Обработчик изменения размера элемента + /// Источник события + /// Аргументы события изменения размера private void OnElementSizeChanged(object Sender, SizeChangedEventArgs E) { var (width, height) = E.NewSize; diff --git a/MathCore.WPF/Behaviors/DragBehavior.cs b/MathCore.WPF/Behaviors/DragBehavior.cs index 18d5e30..b18c722 100644 --- a/MathCore.WPF/Behaviors/DragBehavior.cs +++ b/MathCore.WPF/Behaviors/DragBehavior.cs @@ -10,6 +10,7 @@ namespace MathCore.WPF.Behaviors; +/// Поведение для перетаскивания элементов внутри контейнеров public class DragBehavior : Behavior { private static (double min, double max) CheckMinMax(double min, double max) @@ -203,7 +204,7 @@ protected override bool OnMouseMove(FrameworkElement element, double dx, double #region Enabled - /// + /// DependencyProperty для свойства Enabled public static readonly DependencyProperty EnabledProperty = DependencyProperty.Register( nameof(Enabled), @@ -211,7 +212,7 @@ protected override bool OnMouseMove(FrameworkElement element, double dx, double typeof(DragBehavior), new(default(bool), (s, e) => { if (!(bool)e.NewValue) ((DragBehavior)s)?._ObjectMover?.Dispose(); })); - /// + /// Признак активности поведения перетаскивания public bool Enabled { get => (bool)GetValue(EnabledProperty); @@ -266,6 +267,7 @@ public double dy #region Radius + /// DependencyProperty для свойства Radius private static readonly DependencyPropertyKey RadiusPropertyKey = DependencyProperty.RegisterReadOnly( nameof(Radius), @@ -273,8 +275,10 @@ public double dy typeof(DragBehavior), new FrameworkPropertyMetadata(double.NaN, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); + /// DependencyProperty для свойства Radius public static readonly DependencyProperty RadiusProperty = RadiusPropertyKey.DependencyProperty; + /// Радиус смещения от начальной точки public double Radius { get => (double)GetValue(RadiusProperty); @@ -285,6 +289,7 @@ public double Radius #region Angle + /// DependencyProperty для свойства Angle private static readonly DependencyPropertyKey AnglePropertyKey = DependencyProperty.RegisterReadOnly( nameof(Angle), @@ -292,8 +297,10 @@ public double Radius typeof(DragBehavior), new FrameworkPropertyMetadata(double.NaN, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); + /// DependencyProperty для свойства Angle public static readonly DependencyProperty AngleProperty = AnglePropertyKey.DependencyProperty; + /// Угол смещения от начальной точки в радианах public double Angle { get => (double)GetValue(AngleProperty); @@ -466,12 +473,14 @@ public double CurrentY #endregion + /// Вызывается при присоединении поведения к элементу protected override void OnAttached() { base.OnAttached(); AssociatedObject.MouseLeftButtonDown += OnMouseLeftButtonDown; } + /// Вызывается при отсоединении поведения от элемента protected override void OnDetaching() { base.OnDetaching(); diff --git a/MathCore.WPF/Behaviors/DragInCanvasBehavior.cs b/MathCore.WPF/Behaviors/DragInCanvasBehavior.cs index 8eaf0b6..8acd021 100644 --- a/MathCore.WPF/Behaviors/DragInCanvasBehavior.cs +++ b/MathCore.WPF/Behaviors/DragInCanvasBehavior.cs @@ -9,6 +9,7 @@ namespace MathCore.WPF.Behaviors; +/// Поведение для перетаскивания элемента внутри Canvas с ограничениями по координатам public class DragInCanvasBehavior : Behavior { /// Ссылка на канву @@ -285,7 +286,9 @@ protected override void OnDetaching() canvas.MouseLeftButtonUp -= OnMouseLeftButtonUp; } - /// При нажатии левой кнопки мышиИсточник событияАргумент события + /// При нажатии левой кнопки мыши + /// Источник события + /// Аргумент события private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { var obj = AssociatedObject; @@ -349,6 +352,8 @@ private static (double min, double max) CheckMinMax(double min, double max) return (min, max); } + /// Перемещает элемент в указанную точку + /// Целевая точка private void MoveTo(Point point) { _InMove = true; diff --git a/MathCore.WPF/Behaviors/DropData.cs b/MathCore.WPF/Behaviors/DropData.cs index c4fed3f..16f3204 100644 --- a/MathCore.WPF/Behaviors/DropData.cs +++ b/MathCore.WPF/Behaviors/DropData.cs @@ -9,6 +9,7 @@ namespace MathCore.WPF.Behaviors; +/// Поведение для обработки перетаскивания данных на элемент public class DropData : Behavior { #region DropDataCommand : ICommand - Команда, вызываемая в момент получения данных @@ -87,7 +88,10 @@ public Type? DataType #endregion + /// Поле для хранения предыдущего значения свойства AllowDrop private bool _LastAllowDropValue; + + /// Вызывается при присоединении поведения к элементу protected override void OnAttached() { var element = AssociatedObject; @@ -99,6 +103,7 @@ protected override void OnAttached() element.Drop += OnDropData; } + /// Вызывается при отсоединении поведения от элемента protected override void OnDetaching() { var element = AssociatedObject; @@ -106,6 +111,9 @@ protected override void OnDetaching() element.Drop -= OnDropData; } + /// Обработчик события перетаскивания данных + /// Источник события + /// Аргументы события private void OnDropData(object Sender, DragEventArgs E) { var command = DropDataCommand; diff --git a/MathCore.WPF/Behaviors/MouseControlBehavior.cs b/MathCore.WPF/Behaviors/MouseControlBehavior.cs index 703c1d7..6d26ebd 100644 --- a/MathCore.WPF/Behaviors/MouseControlBehavior.cs +++ b/MathCore.WPF/Behaviors/MouseControlBehavior.cs @@ -8,6 +8,7 @@ namespace MathCore.WPF.Behaviors; +/// Поведение для отслеживания состояния мыши и её положения относительно элемента public class MouseControlBehavior : Behavior { #region MousePosition : Point - Положение указателя мыши @@ -123,6 +124,7 @@ public ICommand LeftMouseClick #region Behavior + /// Вызывается при присоединении поведения к элементу protected override void OnAttached() { var element = AssociatedObject; @@ -132,6 +134,7 @@ protected override void OnAttached() element.MouseUp += OnMouseUp; } + /// Вызывается при отсоединении поведения от элемента protected override void OnDetaching() { var element = AssociatedObject; @@ -139,12 +142,18 @@ protected override void OnDetaching() element.SizeChanged -= OnSizeChanged; } + /// Обработчик нажатия кнопки мыши + /// Источник события + /// Аргументы события private void OnMouseDown(object Sender, MouseButtonEventArgs E) { IsLeftMouseDown = true; Mouse.Capture((IInputElement)Sender, CaptureMode.SubTree); } + /// Обработчик отпускания кнопки мыши + /// Источник события + /// Аргументы события private void OnMouseUp(object Sender, MouseButtonEventArgs E) { (Sender as UIElement)?.ReleaseMouseCapture(); @@ -158,8 +167,14 @@ private void OnMouseUp(object Sender, MouseButtonEventArgs E) #region EventHandlers + /// Обработчик перемещения мыши + /// Источник события + /// Аргументы события private void OnMouseMove(object Sender, MouseEventArgs E) => MousePosition = E.GetPosition((FrameworkElement)Sender); + /// Обработчик изменения размера элемента + /// Источник события + /// Аргументы события private void OnSizeChanged(object Sender, SizeChangedEventArgs E) => ElementSize = E.NewSize; #endregion diff --git a/MathCore.WPF/Behaviors/Resize.cs b/MathCore.WPF/Behaviors/Resize.cs index 2461fe2..9037fa7 100644 --- a/MathCore.WPF/Behaviors/Resize.cs +++ b/MathCore.WPF/Behaviors/Resize.cs @@ -9,10 +9,12 @@ namespace MathCore.WPF.Behaviors; +/// Поведение для изменения размера элемента управления public class Resize : Behavior { #region AreaSize : double - Размер области + /// DependencyProperty для свойства AreaSize public static readonly DependencyProperty AreaSizeProperty = DependencyProperty.Register( nameof(AreaSize), @@ -20,6 +22,7 @@ public class Resize : Behavior typeof(Resize), new(3d)); + /// Размер области захвата для изменения размера в пикселях public double AreaSize { get => (double)GetValue(AreaSizeProperty); @@ -105,18 +108,28 @@ public bool RightResizing #endregion + /// Флаг, указывающий нахождение мыши в области верхней границы private bool _InTop; + /// Флаг, указывающий нахождение мыши в области нижней границы private bool _InBottom; + /// Флаг, указывающий нахождение мыши в области левой границы private bool _InLeft; + /// Флаг, указывающий нахождение мыши в области правой границы private bool _InRight; + /// Флаг, указывающий нахождение мыши в любой из областей изменения размера private bool MouseInArea => _InLeft || _InRight || _InTop || _InBottom; + /// Флаг, указывающий нахождение мыши в левом верхнем углу private bool MouseInLeftTopCorner => _InLeft && _InTop; + /// Флаг, указывающий нахождение мыши в правом верхнем углу private bool MouseInRightTopCorner => _InRight && _InTop; + /// Флаг, указывающий нахождение мыши в левом нижнем углу private bool MouseInLeftBottomCorner => _InLeft && _InBottom; + /// Флаг, указывающий нахождение мыши в правом нижнем углу private bool MouseInRightBottomCorner => _InRight && _InBottom; + /// Вызывается при присоединении поведения к элементу protected override void OnAttached() { base.OnAttached(); @@ -125,6 +138,7 @@ protected override void OnAttached() AssociatedObject.MouseUp += OnMouseUp; } + /// Вызывается при отсоединении поведения от элемента protected override void OnDetaching() { base.OnDetaching(); @@ -132,16 +146,25 @@ protected override void OnDetaching() } + /// Обработчик отпускания кнопки мыши + /// Источник события + /// Аргументы события private void OnMouseUp(object Sender, MouseButtonEventArgs E) { } + /// Обработчик нажатия кнопки мыши + /// Источник события + /// Аргументы события private void OnMouseDown(object Sender, MouseButtonEventArgs E) { } + /// Обработчик перемещения мыши для определения области изменения размера + /// Источник события + /// Аргументы события private void OnMouseMove(object Sender, MouseEventArgs E) { if (Sender is not Control control) return; diff --git a/MathCore.WPF/Behaviors/ResizeWindowPanel.cs b/MathCore.WPF/Behaviors/ResizeWindowPanel.cs index 9c65f19..6166f6f 100644 --- a/MathCore.WPF/Behaviors/ResizeWindowPanel.cs +++ b/MathCore.WPF/Behaviors/ResizeWindowPanel.cs @@ -8,11 +8,15 @@ namespace MathCore.WPF.Behaviors; +/// Поведение для панели изменения размеров окна через системные команды public class ResizeWindowPanel : Behavior { + /// Обработчик событий нажатия кнопки мыши private MouseButtonEventHandler? _MouseButtonsEventHandler; + /// Ссылка на окно private Window? _Window; + /// Вызывается при присоединении поведения к панели protected override void OnAttached() { base.OnAttached(); @@ -22,6 +26,7 @@ protected override void OnAttached() AssociatedObject.AddHandler(UIElement.MouseDownEvent, _MouseButtonsEventHandler); } + /// Вызывается при отсоединении поведения от панели protected override void OnDetaching() { base.OnDetaching(); @@ -29,6 +34,9 @@ protected override void OnDetaching() AssociatedObject.RemoveHandler(UIElement.MouseDownEvent, _MouseButtonsEventHandler); } + /// Обработчик нажатия кнопки мыши на элементе панели изменения размера + /// Источник события + /// Аргументы события private void OnResizeWindowShape_MouseDown(object Sender, MouseButtonEventArgs E) { var window = _Window; diff --git a/MathCore.WPF/Behaviors/TranslateMoveBehavior.cs b/MathCore.WPF/Behaviors/TranslateMoveBehavior.cs index f81dda0..5bbde5f 100644 --- a/MathCore.WPF/Behaviors/TranslateMoveBehavior.cs +++ b/MathCore.WPF/Behaviors/TranslateMoveBehavior.cs @@ -6,10 +6,13 @@ namespace MathCore.WPF.Behaviors; +/// Поведение для перемещения элемента с использованием TranslateTransform public class TranslateMoveBehavior : Behavior { + /// Трансформация перемещения элемента private TranslateTransform _Transform; + /// Вызывается при присоединении поведения к элементу protected override void OnAttached() { base.OnAttached(); @@ -38,6 +41,7 @@ protected override void OnAttached() } } + /// Вызывается при отсоединении поведения от элемента protected override void OnDetaching() { base.OnDetaching(); @@ -64,8 +68,14 @@ protected override void OnDetaching() _Transform = null; } + /// Начальная позиция мыши private Point _StartMousePosition; + /// Родительский элемент для определения координат private IInputElement _Parent; + + /// Обработчик нажатия кнопки мыши для начала перемещения + /// Источник события + /// Аргументы события private void OnMouseDown(object Sender, MouseButtonEventArgs E) { var element = (UIElement)Sender; @@ -77,6 +87,9 @@ private void OnMouseDown(object Sender, MouseButtonEventArgs E) element.MouseMove += OnMouseMove; } + /// Обработчик отпускания кнопки мыши для завершения перемещения + /// Источник события + /// Аргументы события private void OnMouseUp(object s, MouseButtonEventArgs _) { var e = (UIElement)s; @@ -84,6 +97,9 @@ private void OnMouseUp(object s, MouseButtonEventArgs _) e.MouseMove -= OnMouseMove; } + /// Обработчик перемещения мыши для обновления позиции элемента + /// Источник события + /// Аргументы события private void OnMouseMove(object Sender, MouseEventArgs E) => (_Transform.X, _Transform.Y) = _StartMousePosition.Substrate(E.GetPosition(_Parent)); } @@ -93,7 +109,7 @@ private void OnMouseUp(object s, MouseButtonEventArgs _) // { // throw new NotImplementedException(); // AssociatedObject.MouseDown += OnMouseDown; -// //base.OnAttached(); +// //base.OnAttached; // } // protected override void OnDetaching() diff --git a/MathCore.WPF/Behaviors/TreeViewBindableSelectedItem.cs b/MathCore.WPF/Behaviors/TreeViewBindableSelectedItem.cs index aebabd6..fdb7517 100644 --- a/MathCore.WPF/Behaviors/TreeViewBindableSelectedItem.cs +++ b/MathCore.WPF/Behaviors/TreeViewBindableSelectedItem.cs @@ -7,8 +7,10 @@ namespace MathCore.WPF.Behaviors; +/// Поведение для привязки выбранного элемента TreeView public class TreeViewBindableSelectedItem : Behavior { + /// DependencyProperty для свойства SelectedItem public static readonly DependencyProperty SelectedItemProperty = DependencyProperty.Register( nameof(SelectedItem), @@ -16,8 +18,13 @@ public class TreeViewBindableSelectedItem : Behavior typeof(TreeViewBindableSelectedItem), new FrameworkPropertyMetadata(default, OnSelectedItemPropertyChanged) { BindsTwoWayByDefault = true }); + /// Обработчик изменения свойства SelectedItem + /// Объект зависимости + /// Аргументы изменения свойства private static void OnSelectedItemPropertyChanged(DependencyObject D, DependencyPropertyChangedEventArgs E) => (D as TreeViewBindableSelectedItem)?.OnSelectedItemPropertyChanged(E.NewValue); + /// Вызывается при изменении выбранного элемента + /// Новый выбранный элемент protected virtual void OnSelectedItemPropertyChanged(object? item) { if (item is null) return; @@ -26,6 +33,10 @@ protected virtual void OnSelectedItemPropertyChanged(object? item) SelectTreeViewItem(tree_view, item); } + /// Выбирает элемент в дереве + /// Родительский контейнер элементов + /// Элемент для выбора + /// True, если элемент найден и выбран private static bool SelectTreeViewItem(ItemsControl ParentContainer, object item) { if (ParentContainer is null) throw new ArgumentNullException(nameof(ParentContainer)); @@ -55,11 +66,15 @@ private static bool SelectTreeViewItem(ItemsControl ParentContainer, object item return false; } + /// Выбранный элемент дерева public object? SelectedItem { get => GetValue(SelectedItemProperty); set => SetValue(SelectedItemProperty, value); } + /// Пользовательский стиль контейнера элементов private Style? _CustomItemContainerStyle; + /// Установщик события загрузки элемента дерева private EventSetter? _TreeViewItemStyleLoadedEventSetter; + /// Вызывается при присоединении поведения к дереву protected override void OnAttached() { base.OnAttached(); @@ -69,6 +84,7 @@ protected override void OnAttached() style.Setters.Add(_TreeViewItemStyleLoadedEventSetter = new(FrameworkElement.LoadedEvent, new RoutedEventHandler(OnTreeViewItem_Loaded))); } + /// Вызывается при отсоединении поведения от дерева protected override void OnDetaching() { base.OnDetaching(); @@ -81,12 +97,18 @@ protected override void OnDetaching() tree_view.ItemContainerStyle?.Setters.Remove(_TreeViewItemStyleLoadedEventSetter); } + /// Обработчик изменения выбранного элемента дерева + /// Источник события + /// Аргументы события private void OnTreeViewSelectedItemChanged(object? sender, RoutedPropertyChangedEventArgs e) { if (!ReferenceEquals(SelectedItem, e.NewValue)) SelectedItem = e.NewValue; } + /// Обработчик загрузки элемента дерева + /// Источник события + /// Аргументы события protected virtual void OnTreeViewItem_Loaded(object? Sender, RoutedEventArgs? _) { //var item = (TreeViewItem) Sender; diff --git a/MathCore.WPF/Behaviors/UserInputBehavior.cs b/MathCore.WPF/Behaviors/UserInputBehavior.cs index 60d6898..af95e29 100644 --- a/MathCore.WPF/Behaviors/UserInputBehavior.cs +++ b/MathCore.WPF/Behaviors/UserInputBehavior.cs @@ -6,6 +6,7 @@ namespace MathCore.WPF.Behaviors; +/// Поведение для обработки пользовательского ввода с клавиатуры и мыши public class UserInputBehavior : Behavior { #region Position : Point - Положение мыши в координатах элемента @@ -110,6 +111,7 @@ public class UserInputBehavior : Behavior #endregion + /// Вызывается при присоединении поведения к элементу protected override void OnAttached() { base.OnAttached(); @@ -124,6 +126,7 @@ protected override void OnAttached() element.KeyUp += OnKeyUp; } + /// Вызывается при отсоединении поведения от элемента protected override void OnDetaching() { base.OnDetaching(); @@ -140,6 +143,9 @@ protected override void OnDetaching() element.KeyUp -= OnKeyUp; } + /// Обработчик перемещения мыши + /// Источник события + /// Аргументы события private void OnMouseMove(object Sender, MouseEventArgs E) { if (Sender is not FrameworkElement element) return; @@ -157,6 +163,9 @@ private void OnMouseMove(object Sender, MouseEventArgs E) // //Position = new(double.NaN, double.NaN); //} + /// Обработчик нажатия левой кнопки мыши + /// Источник события + /// Аргументы события private void OnLeftMouseDown(object Sender, MouseButtonEventArgs E) { if (Sender is not IInputElement element) return; @@ -167,6 +176,9 @@ private void OnLeftMouseDown(object Sender, MouseButtonEventArgs E) command.Execute(point); } + /// Обработчик отпускания левой кнопки мыши + /// Источник события + /// Аргументы события private void OnLeftMouseUp(object Sender, MouseButtonEventArgs E) { if (Sender is not IInputElement element) return; @@ -177,6 +189,9 @@ private void OnLeftMouseUp(object Sender, MouseButtonEventArgs E) element.ReleaseMouseCapture(); } + /// Обработчик прокрутки колесика мыши + /// Источник события + /// Аргументы события private void OnMouseWheel(object Sender, MouseWheelEventArgs E) { if (MouseWheelCommand is not { } command) return; @@ -185,6 +200,9 @@ private void OnMouseWheel(object Sender, MouseWheelEventArgs E) command.Execute(delta); } + /// Обработчик нажатия клавиши на клавиатуре + /// Источник события + /// Аргументы события private void OnKeyDown(object Sender, KeyEventArgs E) { if (KeyDownCommand is not { } command) return; @@ -193,6 +211,9 @@ private void OnKeyDown(object Sender, KeyEventArgs E) command.Execute(key); } + /// Обработчик отпускания клавиши на клавиатуре + /// Источник события + /// Аргументы события private void OnKeyUp(object Sender, KeyEventArgs E) { if (KeyUpCommand is not { } command) return; diff --git a/MathCore.WPF/Behaviors/WindowMaximizationLimitattor.cs b/MathCore.WPF/Behaviors/WindowMaximizationLimitattor.cs index f106f2e..1801060 100644 --- a/MathCore.WPF/Behaviors/WindowMaximizationLimitattor.cs +++ b/MathCore.WPF/Behaviors/WindowMaximizationLimitattor.cs @@ -8,30 +8,47 @@ namespace MathCore.WPF.Behaviors; +/// Поведение для ограничения максимального размера окна при развертывании на границах рабочей области монитора public class WindowMaximizationLimitattor : Behavior { + /// Вызывается при присоединении поведения к элементу protected override void OnAttached() { base.OnAttached(); AssociatedObject.ForWindowFromTemplate(SetHandler); } + /// Вызывается при отсоединении поведения от элемента protected override void OnDetaching() { base.OnDetaching(); AssociatedObject.ForWindowFromTemplate(ResetHandler); } + /// Устанавливает обработчик инициализации окна + /// Окно для установки обработчика private static void SetHandler(Window window) => window.SourceInitialized += OnWindowOnSourceInitialized; + /// Сбрасывает обработчик инициализации окна + /// Окно для сброса обработчика private static void ResetHandler(Window window) => window.SourceInitialized -= OnWindowOnSourceInitialized; + /// Обработчик инициализации источника окна + /// Источник события + /// Аргументы события private static void OnWindowOnSourceInitialized(object sender, EventArgs? e) { if (sender is null) throw new ArgumentNullException(nameof(sender)); HwndSource.FromHwnd(new WindowInteropHelper((Window) sender).Handle)?.AddHook(WindowProc); } + /// Процедура обработки оконных сообщений + /// Дескриптор окна + /// Код сообщения + /// Параметр wParam + /// Параметр lParam + /// Флаг обработки сообщения + /// Результат обработки сообщения [SuppressMessage("ReSharper", "InconsistentNaming")] private static IntPtr WindowProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { @@ -45,6 +62,9 @@ private static IntPtr WindowProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lPa return (IntPtr)0; } + /// Обрабатывает сообщение WM_GETMINMAXINFO для корректировки максимального размера окна + /// Дескриптор окна + /// Указатель на структуру MINMAXINFO [SuppressMessage("ReSharper", "InconsistentNaming")] private static void WmGetMinMaxInfo(IntPtr hWnd, IntPtr lParam) { From 06a8ebc79324552025190666f4c12ee9bd2a07bd Mon Sep 17 00:00:00 2001 From: Infarh Date: Sat, 13 Dec 2025 23:45:37 +0300 Subject: [PATCH 16/56] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=B2=20=D0=BF=D0=BE=D0=B2=D0=B5=D0=B4=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MathCore.WPF/Behaviors/DragBehavior.cs | 14 +- .../Behaviors/DragInCanvasBehavior.cs | 8 +- MathCore.WPF/Behaviors/Resize.cs | 40 ++--- MathCore.WPF/Behaviors/UserInputBehavior.cs | 12 +- .../Behaviors/DragBehaviorTests.cs | 125 ++++++++++++++ .../Behaviors/DragInCanvasBehaviorTests.cs | 113 +++++++++++++ .../Behaviors/ResizeTests.cs | 152 ++++++++++++++++++ .../Behaviors/UserInputBehaviorTests.cs | 139 ++++++++++++++++ 8 files changed, 555 insertions(+), 48 deletions(-) create mode 100644 Tests/MathCore.WPF.Tests/Behaviors/DragBehaviorTests.cs create mode 100644 Tests/MathCore.WPF.Tests/Behaviors/DragInCanvasBehaviorTests.cs create mode 100644 Tests/MathCore.WPF.Tests/Behaviors/ResizeTests.cs create mode 100644 Tests/MathCore.WPF.Tests/Behaviors/UserInputBehaviorTests.cs diff --git a/MathCore.WPF/Behaviors/DragBehavior.cs b/MathCore.WPF/Behaviors/DragBehavior.cs index b18c722..dcfdf08 100644 --- a/MathCore.WPF/Behaviors/DragBehavior.cs +++ b/MathCore.WPF/Behaviors/DragBehavior.cs @@ -325,7 +325,7 @@ public double Xmin DependencyProperty.Register( nameof(Xmin), typeof(double), - typeof(DragInCanvasBehavior), + typeof(DragBehavior), new(double.NaN)); #endregion @@ -346,7 +346,7 @@ public double Xmax DependencyProperty.Register( nameof(Xmax), typeof(double), - typeof(DragInCanvasBehavior), + typeof(DragBehavior), new(double.NaN)); #endregion @@ -367,7 +367,7 @@ public double Ymin DependencyProperty.Register( nameof(Ymin), typeof(double), - typeof(DragInCanvasBehavior), + typeof(DragBehavior), new(double.NaN)); #endregion @@ -388,7 +388,7 @@ public double Ymax DependencyProperty.Register( nameof(Ymax), typeof(double), - typeof(DragInCanvasBehavior), + typeof(DragBehavior), new(double.NaN)); #endregion @@ -400,7 +400,7 @@ public double Ymax DependencyProperty.Register( nameof(AllowX), typeof(bool), - typeof(DragInCanvasBehavior), + typeof(DragBehavior), new(true)); /// Разрешено перемещение по оси X @@ -414,12 +414,12 @@ public bool AllowX #region AllowY : bool - Разрешено перетаскивание по оси Y - /// summary + /// Разрешено перетаскивание по оси Y public static readonly DependencyProperty AllowYProperty = DependencyProperty.Register( nameof(AllowY), typeof(bool), - typeof(DragInCanvasBehavior), + typeof(DragBehavior), new(true)); /// Разрешено перетаскивание по оси Y diff --git a/MathCore.WPF/Behaviors/DragInCanvasBehavior.cs b/MathCore.WPF/Behaviors/DragInCanvasBehavior.cs index 8acd021..e930447 100644 --- a/MathCore.WPF/Behaviors/DragInCanvasBehavior.cs +++ b/MathCore.WPF/Behaviors/DragInCanvasBehavior.cs @@ -150,7 +150,7 @@ public bool AllowX #region AllowY : bool - Разрешено перетаскивание по оси Y - /// summary + /// Разрешено перетаскивание по оси Y public static readonly DependencyProperty AllowYProperty = DependencyProperty.Register( nameof(AllowY), @@ -197,16 +197,16 @@ private static void OnCurrentXChanged(DependencyObject d, DependencyPropertyChan #region CurrentY : double - Текущее вертикальное положение - /// Текущее горизонтальное положение + /// Текущее вертикальное положение //[Category("")] - [Description("Текущее горизонтальное положение")] + [Description("Текущее вертикальное положение")] public double CurrentY { get => (double)GetValue(CurrentYProperty); set => SetValue(CurrentYProperty, value); } - /// Текущее горизонтальное положение + /// Текущее вертикальное положение public static readonly DependencyProperty CurrentYProperty = DependencyProperty.Register( nameof(CurrentY), diff --git a/MathCore.WPF/Behaviors/Resize.cs b/MathCore.WPF/Behaviors/Resize.cs index 9037fa7..90cd504 100644 --- a/MathCore.WPF/Behaviors/Resize.cs +++ b/MathCore.WPF/Behaviors/Resize.cs @@ -134,8 +134,6 @@ protected override void OnAttached() { base.OnAttached(); AssociatedObject.MouseMove += OnMouseMove; - AssociatedObject.MouseDown += OnMouseDown; - AssociatedObject.MouseUp += OnMouseUp; } /// Вызывается при отсоединении поведения от элемента @@ -143,23 +141,7 @@ protected override void OnDetaching() { base.OnDetaching(); AssociatedObject.MouseMove -= OnMouseMove; - - } - - /// Обработчик отпускания кнопки мыши - /// Источник события - /// Аргументы события - private void OnMouseUp(object Sender, MouseButtonEventArgs E) - { - - } - - /// Обработчик нажатия кнопки мыши - /// Источник события - /// Аргументы события - private void OnMouseDown(object Sender, MouseButtonEventArgs E) - { - + Mouse.OverrideCursor = null; } /// Обработчик перемещения мыши для определения области изменения размера @@ -179,19 +161,15 @@ private void OnMouseMove(object Sender, MouseEventArgs E) Mouse.OverrideCursor = (Left: _InLeft, Top: _InTop, Right: _InRight, Bottom: _InBottom) switch { - (Left: true, Top: true, Right: _, Bottom : _) => Mouse.OverrideCursor = Cursors.ScrollWE, - (Left: _, Top : _, Right : true, Bottom: true) => Mouse.OverrideCursor = Cursors.ScrollWE, - (Left: _, Top : true, Right: _, Bottom : _) => Mouse.OverrideCursor = Cursors.ScrollNS, - (Left: _, Top : _, Right : _, Bottom : true) => Mouse.OverrideCursor = Cursors.ScrollNS, - (Left: true, Top: _, Right : _, Bottom : _) => Mouse.OverrideCursor = Cursors.ScrollWE, - (Left: _, Top : _, Right : true, Bottom: _) => Mouse.OverrideCursor = Cursors.ScrollWE, + (Left: true, Top: true, Right: _, Bottom: _) => Cursors.SizeNWSE, + (Left: _, Top: _, Right: true, Bottom: true) => Cursors.SizeNWSE, + (Left: true, Top: _, Right: _, Bottom: true) => Cursors.SizeNESW, + (Left: _, Top: true, Right: true, Bottom: _) => Cursors.SizeNESW, + (Left: _, Top: true, Right: _, Bottom: _) => Cursors.SizeNS, + (Left: _, Top: _, Right: _, Bottom: true) => Cursors.SizeNS, + (Left: true, Top: _, Right: _, Bottom: _) => Cursors.SizeWE, + (Left: _, Top: _, Right: true, Bottom: _) => Cursors.SizeWE, _ => Cursors.Arrow }; - - //if ((_InLeft && _InTop) || (_InRight && _InBottom)) Mouse.OverrideCursor = Cursors.ScrollWE; - //else if ((_InRight && _InTop) || (_InLeft && _InBottom)) Mouse.OverrideCursor = Cursors.ScrollNE; - //else if (_InTop || _InBottom) Mouse.OverrideCursor = Cursors.ScrollNS; - //else if (_InLeft || _InRight) Mouse.OverrideCursor = Cursors.ScrollWE; - //else Mouse.OverrideCursor = Cursors.Arrow; } } \ No newline at end of file diff --git a/MathCore.WPF/Behaviors/UserInputBehavior.cs b/MathCore.WPF/Behaviors/UserInputBehavior.cs index af95e29..ddba748 100644 --- a/MathCore.WPF/Behaviors/UserInputBehavior.cs +++ b/MathCore.WPF/Behaviors/UserInputBehavior.cs @@ -90,13 +90,13 @@ public class UserInputBehavior : Behavior /// Команда, вызываемая при нажатии кнопки на клавиатуре //[Category("")] [Description("Команда, вызываемая при нажатии кнопки на клавиатуре")] - public ICommand KeyDownCommand { get => (ICommand)GetValue(MouseWheelCommandProperty); set => SetValue(MouseWheelCommandProperty, value); } + public ICommand KeyDownCommand { get => (ICommand)GetValue(KeyDownCommandProperty); set => SetValue(KeyDownCommandProperty, value); } #endregion - #region KeyUpCommand : ICommand - Команда, вызываемая при нажатии кнопки на клавиатуре + #region KeyUpCommand : ICommand - Команда, вызываемая при отпускании кнопки на клавиатуре - /// Команда, вызываемая при нажатии кнопки на клавиатуре + /// Команда, вызываемая при отпускании кнопки на клавиатуре public static readonly DependencyProperty KeyUpCommandProperty = DependencyProperty.Register( nameof(KeyUpCommand), @@ -104,10 +104,10 @@ public class UserInputBehavior : Behavior typeof(UserInputBehavior), new(default(ICommand))); - /// Команда, вызываемая при нажатии кнопки на клавиатуре + /// Команда, вызываемая при отпускании кнопки на клавиатуре //[Category("")] - [Description("Команда, вызываемая при нажатии кнопки на клавиатуре")] - public ICommand KeyUpCommand { get => (ICommand)GetValue(MouseWheelCommandProperty); set => SetValue(MouseWheelCommandProperty, value); } + [Description("Команда, вызываемая при отпускании кнопки на клавиатуре")] + public ICommand KeyUpCommand { get => (ICommand)GetValue(KeyUpCommandProperty); set => SetValue(KeyUpCommandProperty, value); } #endregion diff --git a/Tests/MathCore.WPF.Tests/Behaviors/DragBehaviorTests.cs b/Tests/MathCore.WPF.Tests/Behaviors/DragBehaviorTests.cs new file mode 100644 index 0000000..c1eb412 --- /dev/null +++ b/Tests/MathCore.WPF.Tests/Behaviors/DragBehaviorTests.cs @@ -0,0 +1,125 @@ +using System.Windows; +using System.Windows.Controls; + +using MathCore.WPF.Behaviors; + +namespace MathCore.WPF.Tests.Behaviors; + +/// Регрессионные тесты для DragBehavior +[TestClass] +public class DragBehaviorTests +{ + /// Тест регистрации DependencyProperty с правильным типом владельца + [TestMethod] + public void DependencyProperties_Should_Have_Correct_OwnerType() + { + // Arrange & Act + var xmin_property = DragBehavior.XminProperty; + var xmax_property = DragBehavior.XmaxProperty; + var ymin_property = DragBehavior.YminProperty; + var ymax_property = DragBehavior.YmaxProperty; + var allow_x_property = DragBehavior.AllowXProperty; + var allow_y_property = DragBehavior.AllowYProperty; + + // Assert - проверяем, что свойства зарегистрированы + Assert.IsNotNull(xmin_property, "XminProperty должно быть зарегистрировано"); + Assert.IsNotNull(xmax_property, "XmaxProperty должно быть зарегистрировано"); + Assert.IsNotNull(ymin_property, "YminProperty должно быть зарегистрировано"); + Assert.IsNotNull(ymax_property, "YmaxProperty должно быть зарегистрировано"); + Assert.IsNotNull(allow_x_property, "AllowXProperty должно быть зарегистрировано"); + Assert.IsNotNull(allow_y_property, "AllowYProperty должно быть зарегистрировано"); + + // Assert - проверяем тип владельца + Assert.AreEqual(typeof(DragBehavior), xmin_property.OwnerType, "XminProperty должно принадлежать DragBehavior"); + Assert.AreEqual(typeof(DragBehavior), xmax_property.OwnerType, "XmaxProperty должно принадлежать DragBehavior"); + Assert.AreEqual(typeof(DragBehavior), ymin_property.OwnerType, "YminProperty должно принадлежать DragBehavior"); + Assert.AreEqual(typeof(DragBehavior), ymax_property.OwnerType, "YmaxProperty должно принадлежать DragBehavior"); + Assert.AreEqual(typeof(DragBehavior), allow_x_property.OwnerType, "AllowXProperty должно принадлежать DragBehavior"); + Assert.AreEqual(typeof(DragBehavior), allow_y_property.OwnerType, "AllowYProperty должно принадлежать DragBehavior"); + } + + /// Тест установки и получения значений координатных свойств + [TestMethod] + public void CoordinateProperties_Should_SetAndGet_Values() + { + // Arrange + var behavior = new DragBehavior(); + + // Act & Assert - Xmin + behavior.Xmin = 10.0; + Assert.AreEqual(10.0, behavior.Xmin, "Xmin должно возвращать установленное значение"); + + // Act & Assert - Xmax + behavior.Xmax = 100.0; + Assert.AreEqual(100.0, behavior.Xmax, "Xmax должно возвращать установленное значение"); + + // Act & Assert - Ymin + behavior.Ymin = 20.0; + Assert.AreEqual(20.0, behavior.Ymin, "Ymin должно возвращать установленное значение"); + + // Act & Assert - Ymax + behavior.Ymax = 200.0; + Assert.AreEqual(200.0, behavior.Ymax, "Ymax должно возвращать установленное значение"); + } + + /// Тест установки и получения значений флагов разрешений + [TestMethod] + public void AllowProperties_Should_SetAndGet_Values() + { + // Arrange + var behavior = new DragBehavior(); + + // Act & Assert - AllowX по умолчанию true + Assert.IsTrue(behavior.AllowX, "AllowX должно быть true по умолчанию"); + + // Act & Assert - AllowY по умолчанию true + Assert.IsTrue(behavior.AllowY, "AllowY должно быть true по умолчанию"); + + // Act & Assert - установка AllowX + behavior.AllowX = false; + Assert.IsFalse(behavior.AllowX, "AllowX должно возвращать установленное значение"); + + // Act & Assert - установка AllowY + behavior.AllowY = false; + Assert.IsFalse(behavior.AllowY, "AllowY должно возвращать установленное значение"); + } + + /// Тест значений по умолчанию для координатных свойств + [TestMethod] + public void CoordinateProperties_Should_Have_NaN_DefaultValues() + { + // Arrange + var behavior = new DragBehavior(); + + // Assert + Assert.IsTrue(double.IsNaN(behavior.Xmin), "Xmin должно иметь значение NaN по умолчанию"); + Assert.IsTrue(double.IsNaN(behavior.Xmax), "Xmax должно иметь значение NaN по умолчанию"); + Assert.IsTrue(double.IsNaN(behavior.Ymin), "Ymin должно иметь значение NaN по умолчанию"); + Assert.IsTrue(double.IsNaN(behavior.Ymax), "Ymax должно иметь значение NaN по умолчанию"); + } + + /// Тест свойства Enabled по умолчанию + [TestMethod] + public void Enabled_Should_Be_False_ByDefault() + { + // Arrange + var behavior = new DragBehavior(); + + // Assert + Assert.IsFalse(behavior.Enabled, "Enabled должно быть false по умолчанию"); + } + + /// Тест установки свойства Enabled + [TestMethod] + public void Enabled_Should_SetAndGet_Value() + { + // Arrange + var behavior = new DragBehavior(); + + // Act + behavior.Enabled = true; + + // Assert + Assert.IsTrue(behavior.Enabled, "Enabled должно возвращать установленное значение"); + } +} diff --git a/Tests/MathCore.WPF.Tests/Behaviors/DragInCanvasBehaviorTests.cs b/Tests/MathCore.WPF.Tests/Behaviors/DragInCanvasBehaviorTests.cs new file mode 100644 index 0000000..b781abf --- /dev/null +++ b/Tests/MathCore.WPF.Tests/Behaviors/DragInCanvasBehaviorTests.cs @@ -0,0 +1,113 @@ +using System.Windows; +using System.Windows.Controls; + +using MathCore.WPF.Behaviors; + +namespace MathCore.WPF.Tests.Behaviors; + +/// Регрессионные тесты для DragInCanvasBehavior +[TestClass] +public class DragInCanvasBehaviorTests +{ + /// Тест регистрации DependencyProperty + [TestMethod] + public void DependencyProperties_Should_Be_Registered() + { + // Arrange & Act + var xmin_property = DragInCanvasBehavior.XminProperty; + var xmax_property = DragInCanvasBehavior.XmaxProperty; + var ymin_property = DragInCanvasBehavior.YminProperty; + var ymax_property = DragInCanvasBehavior.YmaxProperty; + var allow_x_property = DragInCanvasBehavior.AllowXProperty; + var allow_y_property = DragInCanvasBehavior.AllowYProperty; + var current_x_property = DragInCanvasBehavior.CurrentXProperty; + var current_y_property = DragInCanvasBehavior.CurrentYProperty; + var enabled_property = DragInCanvasBehavior.EnabledProperty; + + // Assert + Assert.IsNotNull(xmin_property, "XminProperty должно быть зарегистрировано"); + Assert.IsNotNull(xmax_property, "XmaxProperty должно быть зарегистрировано"); + Assert.IsNotNull(ymin_property, "YminProperty должно быть зарегистрировано"); + Assert.IsNotNull(ymax_property, "YmaxProperty должно быть зарегистрировано"); + Assert.IsNotNull(allow_x_property, "AllowXProperty должно быть зарегистрировано"); + Assert.IsNotNull(allow_y_property, "AllowYProperty должно быть зарегистрировано"); + Assert.IsNotNull(current_x_property, "CurrentXProperty должно быть зарегистрировано"); + Assert.IsNotNull(current_y_property, "CurrentYProperty должно быть зарегистрировано"); + Assert.IsNotNull(enabled_property, "EnabledProperty должно быть зарегистрировано"); + } + + /// Тест установки и получения координатных свойств + [TestMethod] + public void CoordinateProperties_Should_SetAndGet_Values() + { + // Arrange + var behavior = new DragInCanvasBehavior(); + + // Act & Assert - Xmin + behavior.Xmin = 10.0; + Assert.AreEqual(10.0, behavior.Xmin, "Xmin должно возвращать установленное значение"); + + // Act & Assert - Xmax + behavior.Xmax = 100.0; + Assert.AreEqual(100.0, behavior.Xmax, "Xmax должно возвращать установленное значение"); + + // Act & Assert - Ymin + behavior.Ymin = 20.0; + Assert.AreEqual(20.0, behavior.Ymin, "Ymin должно возвращать установленное значение"); + + // Act & Assert - Ymax + behavior.Ymax = 200.0; + Assert.AreEqual(200.0, behavior.Ymax, "Ymax должно возвращать установленное значение"); + } + + /// Тест флагов разрешений перемещения + [TestMethod] + public void AllowProperties_Should_SetAndGet_Values() + { + // Arrange + var behavior = new DragInCanvasBehavior(); + + // Assert - значения по умолчанию + Assert.IsTrue(behavior.AllowX, "AllowX должно быть true по умолчанию"); + Assert.IsTrue(behavior.AllowY, "AllowY должно быть true по умолчанию"); + + // Act & Assert - установка AllowX + behavior.AllowX = false; + Assert.IsFalse(behavior.AllowX, "AllowX должно возвращать установленное значение"); + + // Act & Assert - установка AllowY + behavior.AllowY = false; + Assert.IsFalse(behavior.AllowY, "AllowY должно возвращать установленное значение"); + } + + /// Тест свойства Enabled + [TestMethod] + public void Enabled_Should_SetAndGet_Value() + { + // Arrange + var behavior = new DragInCanvasBehavior(); + + // Assert - значение по умолчанию + Assert.IsTrue(behavior.Enabled, "Enabled должно быть true по умолчанию"); + + // Act + behavior.Enabled = false; + + // Assert + Assert.IsFalse(behavior.Enabled, "Enabled должно возвращать установленное значение"); + } + + /// Тест значений по умолчанию для координатных ограничений + [TestMethod] + public void CoordinateConstraints_Should_Have_NaN_DefaultValues() + { + // Arrange + var behavior = new DragInCanvasBehavior(); + + // Assert + Assert.IsTrue(double.IsNaN(behavior.Xmin), "Xmin должно иметь значение NaN по умолчанию"); + Assert.IsTrue(double.IsNaN(behavior.Xmax), "Xmax должно иметь значение NaN по умолчанию"); + Assert.IsTrue(double.IsNaN(behavior.Ymin), "Ymin должно иметь значение NaN по умолчанию"); + Assert.IsTrue(double.IsNaN(behavior.Ymax), "Ymax должно иметь значение NaN по умолчанию"); + } +} diff --git a/Tests/MathCore.WPF.Tests/Behaviors/ResizeTests.cs b/Tests/MathCore.WPF.Tests/Behaviors/ResizeTests.cs new file mode 100644 index 0000000..3f9d3ca --- /dev/null +++ b/Tests/MathCore.WPF.Tests/Behaviors/ResizeTests.cs @@ -0,0 +1,152 @@ +using System.Windows; +using System.Windows.Controls; + +using MathCore.WPF.Behaviors; + +namespace MathCore.WPF.Tests.Behaviors; + +/// Регрессионные тесты для Resize +[TestClass] +public class ResizeTests +{ + /// Тест регистрации DependencyProperty + [TestMethod] + public void DependencyProperties_Should_Be_Registered() + { + // Arrange & Act + var area_size_property = Resize.AreaSizeProperty; + var top_resizing_property = Resize.TopResizingProperty; + var bottom_resizing_property = Resize.BottomResizingProperty; + var left_resizing_property = Resize.LeftResizingProperty; + var right_resizing_property = Resize.RightResizingProperty; + + // Assert + Assert.IsNotNull(area_size_property, "AreaSizeProperty должно быть зарегистрировано"); + Assert.IsNotNull(top_resizing_property, "TopResizingProperty должно быть зарегистрировано"); + Assert.IsNotNull(bottom_resizing_property, "BottomResizingProperty должно быть зарегистрировано"); + Assert.IsNotNull(left_resizing_property, "LeftResizingProperty должно быть зарегистрировано"); + Assert.IsNotNull(right_resizing_property, "RightResizingProperty должно быть зарегистрировано"); + } + + /// Тест значения по умолчанию для AreaSize + [TestMethod] + public void AreaSize_Should_Have_Default_Value_Of_3() + { + // Arrange + var behavior = new Resize(); + + // Assert + Assert.AreEqual(3.0, behavior.AreaSize, "AreaSize должно иметь значение 3.0 по умолчанию"); + } + + /// Тест установки и получения AreaSize + [TestMethod] + public void AreaSize_Should_SetAndGet_Value() + { + // Arrange + var behavior = new Resize(); + + // Act + behavior.AreaSize = 5.0; + + // Assert + Assert.AreEqual(5.0, behavior.AreaSize, "AreaSize должно возвращать установленное значение"); + } + + /// Тест значений по умолчанию для флагов изменения размера + [TestMethod] + public void ResizingFlags_Should_Be_False_ByDefault() + { + // Arrange + var behavior = new Resize(); + + // Assert + Assert.IsFalse(behavior.TopResizing, "TopResizing должно быть false по умолчанию"); + Assert.IsFalse(behavior.BottomResizing, "BottomResizing должно быть false по умолчанию"); + Assert.IsFalse(behavior.LeftResizing, "LeftResizing должно быть false по умолчанию"); + Assert.IsFalse(behavior.RightResizing, "RightResizing должно быть false по умолчанию"); + } + + /// Тест установки и получения флагов изменения размера + [TestMethod] + public void ResizingFlags_Should_SetAndGet_Values() + { + // Arrange + var behavior = new Resize(); + + // Act + behavior.TopResizing = true; + behavior.BottomResizing = true; + behavior.LeftResizing = true; + behavior.RightResizing = true; + + // Assert + Assert.IsTrue(behavior.TopResizing, "TopResizing должно возвращать установленное значение"); + Assert.IsTrue(behavior.BottomResizing, "BottomResizing должно возвращать установленное значение"); + Assert.IsTrue(behavior.LeftResizing, "LeftResizing должно возвращать установленное значение"); + Assert.IsTrue(behavior.RightResizing, "RightResizing должно возвращать установленное значение"); + } + + /// Тест независимости флагов изменения размера + [TestMethod] + public void ResizingFlags_Should_Be_Independent() + { + // Arrange + var behavior = new Resize(); + + // Act - устанавливаем только TopResizing + behavior.TopResizing = true; + + // Assert - остальные должны остаться false + Assert.IsTrue(behavior.TopResizing, "TopResizing должно быть true"); + Assert.IsFalse(behavior.BottomResizing, "BottomResizing должно остаться false"); + Assert.IsFalse(behavior.LeftResizing, "LeftResizing должно остаться false"); + Assert.IsFalse(behavior.RightResizing, "RightResizing должно остаться false"); + + // Act - устанавливаем только LeftResizing + behavior.LeftResizing = true; + + // Assert + Assert.IsTrue(behavior.TopResizing, "TopResizing должно сохранить значение true"); + Assert.IsTrue(behavior.LeftResizing, "LeftResizing должно быть true"); + Assert.IsFalse(behavior.BottomResizing, "BottomResizing должно остаться false"); + Assert.IsFalse(behavior.RightResizing, "RightResizing должно остаться false"); + } + + /// Тест, что AreaSize может принимать различные значения + [TestMethod] + public void AreaSize_Should_Accept_Various_Values() + { + // Arrange + var behavior = new Resize(); + var test_values = new[] { 0.5, 1.0, 5.0, 10.0, 20.0 }; + + foreach (var value in test_values) + { + // Act + behavior.AreaSize = value; + + // Assert + Assert.AreEqual(value, behavior.AreaSize, $"AreaSize должно возвращать установленное значение {value}"); + } + } + + /// Тест типа владельца для DependencyProperty + [TestMethod] + public void DependencyProperties_Should_Have_Correct_OwnerType() + { + // Arrange & Act + var area_size_property = Resize.AreaSizeProperty; + var top_resizing_property = Resize.TopResizingProperty; + var bottom_resizing_property = Resize.BottomResizingProperty; + var left_resizing_property = Resize.LeftResizingProperty; + var right_resizing_property = Resize.RightResizingProperty; + + // Assert + Assert.AreEqual(typeof(Resize), area_size_property.OwnerType, "AreaSizeProperty должно принадлежать Resize"); + Assert.AreEqual(typeof(Resize), top_resizing_property.OwnerType, "TopResizingProperty должно принадлежать Resize"); + Assert.AreEqual(typeof(Resize), bottom_resizing_property.OwnerType, "BottomResizingProperty должно принадлежать Resize"); + Assert.AreEqual(typeof(Resize), left_resizing_property.OwnerType, "LeftResizingProperty должно принадлежать Resize"); + Assert.AreEqual(typeof(Resize), right_resizing_property.OwnerType, "RightResizingProperty должно принадлежать Resize"); + } +} diff --git a/Tests/MathCore.WPF.Tests/Behaviors/UserInputBehaviorTests.cs b/Tests/MathCore.WPF.Tests/Behaviors/UserInputBehaviorTests.cs new file mode 100644 index 0000000..486eddb --- /dev/null +++ b/Tests/MathCore.WPF.Tests/Behaviors/UserInputBehaviorTests.cs @@ -0,0 +1,139 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; + +using MathCore.WPF.Behaviors; + +namespace MathCore.WPF.Tests.Behaviors; + +/// Регрессионные тесты для UserInputBehavior +[TestClass] +public class UserInputBehaviorTests +{ + /// Простая команда для тестирования + private class TestCommand : ICommand + { + public event EventHandler? CanExecuteChanged; + public int ExecuteCount { get; private set; } + public object? LastParameter { get; private set; } + + public bool CanExecute(object? parameter) => true; + + public void Execute(object? parameter) + { + ExecuteCount++; + LastParameter = parameter; + } + } + + /// Тест регистрации DependencyProperty для команд + [TestMethod] + public void CommandProperties_Should_Be_Registered() + { + // Arrange & Act + var left_down_property = UserInputBehavior.LeftMouseDownCommandProperty; + var left_up_property = UserInputBehavior.LeftMouseUpCommandProperty; + var wheel_property = UserInputBehavior.MouseWheelCommandProperty; + var key_down_property = UserInputBehavior.KeyDownCommandProperty; + var key_up_property = UserInputBehavior.KeyUpCommandProperty; + + // Assert + Assert.IsNotNull(left_down_property, "LeftMouseDownCommandProperty должно быть зарегистрировано"); + Assert.IsNotNull(left_up_property, "LeftMouseUpCommandProperty должно быть зарегистрировано"); + Assert.IsNotNull(wheel_property, "MouseWheelCommandProperty должно быть зарегистрировано"); + Assert.IsNotNull(key_down_property, "KeyDownCommandProperty должно быть зарегистрировано"); + Assert.IsNotNull(key_up_property, "KeyUpCommandProperty должно быть зарегистрировано"); + } + + /// Тест установки и получения команд + [TestMethod] + public void CommandProperties_Should_SetAndGet_Values() + { + // Arrange + var behavior = new UserInputBehavior(); + var left_down_command = new TestCommand(); + var left_up_command = new TestCommand(); + var wheel_command = new TestCommand(); + var key_down_command = new TestCommand(); + var key_up_command = new TestCommand(); + + // Act + behavior.LeftMouseDownCommand = left_down_command; + behavior.LeftMouseUpCommand = left_up_command; + behavior.MouseWheelCommand = wheel_command; + behavior.KeyDownCommand = key_down_command; + behavior.KeyUpCommand = key_up_command; + + // Assert + Assert.AreSame(left_down_command, behavior.LeftMouseDownCommand, "LeftMouseDownCommand должна возвращать установленное значение"); + Assert.AreSame(left_up_command, behavior.LeftMouseUpCommand, "LeftMouseUpCommand должна возвращать установленное значение"); + Assert.AreSame(wheel_command, behavior.MouseWheelCommand, "MouseWheelCommand должна возвращать установленное значение"); + Assert.AreSame(key_down_command, behavior.KeyDownCommand, "KeyDownCommand должна возвращать установленное значение"); + Assert.AreSame(key_up_command, behavior.KeyUpCommand, "KeyUpCommand должна возвращать установленное значение"); + } + + /// Тест, что KeyDownCommand и KeyUpCommand независимы от MouseWheelCommand + [TestMethod] + public void KeyCommands_Should_Be_Independent_From_MouseWheelCommand() + { + // Arrange + var behavior = new UserInputBehavior(); + var wheel_command = new TestCommand(); + var key_down_command = new TestCommand(); + var key_up_command = new TestCommand(); + + // Act - устанавливаем разные команды + behavior.MouseWheelCommand = wheel_command; + behavior.KeyDownCommand = key_down_command; + behavior.KeyUpCommand = key_up_command; + + // Assert - проверяем, что команды не перекрываются + Assert.AreSame(wheel_command, behavior.MouseWheelCommand, "MouseWheelCommand должна сохранить своё значение"); + Assert.AreSame(key_down_command, behavior.KeyDownCommand, "KeyDownCommand должна иметь своё значение"); + Assert.AreSame(key_up_command, behavior.KeyUpCommand, "KeyUpCommand должна иметь своё значение"); + + Assert.AreNotSame(behavior.KeyDownCommand, behavior.MouseWheelCommand, "KeyDownCommand не должна быть той же, что MouseWheelCommand"); + Assert.AreNotSame(behavior.KeyUpCommand, behavior.MouseWheelCommand, "KeyUpCommand не должна быть той же, что MouseWheelCommand"); + } + + /// Тест значений по умолчанию для команд + [TestMethod] + public void CommandProperties_Should_Be_Null_ByDefault() + { + // Arrange + var behavior = new UserInputBehavior(); + + // Assert + Assert.IsNull(behavior.LeftMouseDownCommand, "LeftMouseDownCommand должна быть null по умолчанию"); + Assert.IsNull(behavior.LeftMouseUpCommand, "LeftMouseUpCommand должна быть null по умолчанию"); + Assert.IsNull(behavior.MouseWheelCommand, "MouseWheelCommand должна быть null по умолчанию"); + Assert.IsNull(behavior.KeyDownCommand, "KeyDownCommand должна быть null по умолчанию"); + Assert.IsNull(behavior.KeyUpCommand, "KeyUpCommand должна быть null по умолчанию"); + } + + /// Тест регистрации свойства Position + [TestMethod] + public void Position_Property_Should_SetAndGet_Values() + { + // Arrange + var behavior = new UserInputBehavior(); + var position = new Point(100, 200); + + // Act + behavior.Position = position; + + // Assert + Assert.AreEqual(position, behavior.Position, "Position должна возвращать установленное значение"); + } + + /// Тест значения по умолчанию для Position + [TestMethod] + public void Position_Should_Be_Zero_ByDefault() + { + // Arrange + var behavior = new UserInputBehavior(); + + // Assert + Assert.AreEqual(new Point(0, 0), behavior.Position, "Position должна иметь значение (0,0) по умолчанию"); + } +} From 4364575033c48e42d791e59a091aabd269763825 Mon Sep 17 00:00:00 2001 From: Infarh Date: Sun, 14 Dec 2025 00:00:37 +0300 Subject: [PATCH 17/56] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=20=D1=81=20=D1=80=D0=B5=D0=BA=D0=BE=D0=BC=D0=B5=D0=BD=D0=B4?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=D0=BC=D0=B8=20=D0=BF=D0=BE=20=D1=83?= =?UTF-8?q?=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD=D0=B8=D1=8E=20Behaviors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit В проект добавлен файл BehaviorsImprovementRecommendations.md, содержащий подробный анализ и рекомендации по улучшению поведения (Behaviors) в кодовой базе. Документ включает архитектурные, функциональные, производительные и надёжные улучшения, примеры кода, таблицу приоритетов, статистику и поэтапный план внедрения изменений. Это поможет системно повысить качество и удобство использования Behaviors. --- .../BehaviorsImprovementRecommendations.md | 961 ++++++++++++++++++ 1 file changed, 961 insertions(+) create mode 100644 MathCore.WPF/Behaviors/BehaviorsImprovementRecommendations.md diff --git a/MathCore.WPF/Behaviors/BehaviorsImprovementRecommendations.md b/MathCore.WPF/Behaviors/BehaviorsImprovementRecommendations.md new file mode 100644 index 0000000..ab52626 --- /dev/null +++ b/MathCore.WPF/Behaviors/BehaviorsImprovementRecommendations.md @@ -0,0 +1,961 @@ +# Рекомендации по улучшению поведений (Behaviors) + +**Дата анализа:** 2025-12-13 +**Проанализировано файлов:** 11 +**Категорий улучшений:** 5 + +--- + +## 📋 Оглавление + +1. [Архитектурные улучшения](#архитектурные-улучшения) +2. [Улучшения удобства использования](#улучшения-удобства-использования) +3. [Улучшения производительности](#улучшения-производительности) +4. [Улучшения безопасности и надёжности](#улучшения-безопасности-и-надёжности) +5. [Расширение функциональности](#расширение-функциональности) +6. [Сводная таблица приоритетов](#сводная-таблица-приоритетов) + +--- + +## Архитектурные улучшения + +### 1. **ActualSizeBinding.cs** - Оптимизация обновлений размеров + +**Проблема:** +```csharp +private static void OnWidthChanged(DependencyObject D, DependencyPropertyChangedEventArgs E) +{ + if(D is not ActualSizeBinding { AssociatedObject: var element } || + E.NewValue is not double width || + element.ActualWidth == width) return; + element.Width = width; +} +``` + +**Рекомендация:** +- Добавить возможность отключения обратной синхронизации (от DependencyProperty к элементу) +- Добавить флаг `EnableTwoWaySync` для управления направлением синхронизации +- Использовать закомментированный код с `BindingOperations` как опциональную стратегию + +**Приоритет:** 🟡 Средний + +**Преимущества:** +- Избежание циклических обновлений +- Гибкость в сценариях использования (только чтение/запись размеров) +- Меньше накладных расходов при односторонней синхронизации + +**Пример улучшения:** +```csharp +#region EnableTwoWaySync : bool - Включить двухстороннюю синхронизацию + +public static readonly DependencyProperty EnableTwoWaySyncProperty = + DependencyProperty.Register( + nameof(EnableTwoWaySync), + typeof(bool), + typeof(ActualSizeBinding), + new(true)); + +public bool EnableTwoWaySync +{ + get => (bool)GetValue(EnableTwoWaySyncProperty); + set => SetValue(EnableTwoWaySyncProperty, value); +} + +#endregion + +private static void OnWidthChanged(DependencyObject D, DependencyPropertyChangedEventArgs E) +{ + if(D is not ActualSizeBinding { AssociatedObject: var element, EnableTwoWaySync: true } || + E.NewValue is not double width || + element.ActualWidth == width) return; + element.Width = width; +} +``` + +--- + +### 2. **DragBehavior.cs** - Унификация с DragInCanvasBehavior + +**Проблема:** +Существует два отдельных поведения для перетаскивания: +- `DragBehavior` - универсальное для разных контейнеров +- `DragInCanvasBehavior` - специализированное для Canvas + +**Рекомендация:** +- Создать базовый абстрактный класс `DragBehaviorBase` +- Выделить общую логику ограничений координат и флагов Allow* +- Реализовать паттерн Strategy для разных типов контейнеров + +**Приоритет:** 🔴 Высокий + +**Преимущества:** +- Уменьшение дублирования кода +- Упрощение поддержки +- Единообразный API для пользователей + +**Пример архитектуры:** +```csharp +public abstract class DragBehaviorBase : Behavior where T : FrameworkElement +{ + // Общие свойства: Xmin, Xmax, Ymin, Ymax, AllowX, AllowY, CurrentX, CurrentY + protected abstract IDragStrategy CreateDragStrategy(); +} + +public interface IDragStrategy +{ + void StartDrag(FrameworkElement element, Point startPoint); + void UpdateDrag(Point currentPoint); + void EndDrag(); +} + +public class DragBehavior : DragBehaviorBase +{ + protected override IDragStrategy CreateDragStrategy() => + AssociatedObject.FindLogicalParent() switch + { + Canvas => new CanvasDragStrategy(this), + Panel => new PanelDragStrategy(this), + _ => null + }; +} +``` + +--- + +### 3. **Resize.cs** - Завершение реализации + +**Проблема:** +Поведение только изменяет курсор, но не выполняет реальное изменение размера элемента. + +**Рекомендация:** +- Реализовать фактическое изменение размеров через `Width`/`Height` или `RenderTransform` +- Добавить свойства `MinWidth`, `MinHeight`, `MaxWidth`, `MaxHeight` +- Добавить события `ResizeStarted`, `Resizing`, `ResizeCompleted` +- Добавить возможность привязки к изменениям размера + +**Приоритет:** 🔴 Высокий + +**Преимущества:** +- Полноценная функциональность изменения размера +- MVVM-совместимость через события и привязки +- Контроль над минимальными/максимальными размерами + +**Пример реализации:** +```csharp +#region IsResizing : bool - Процесс изменения размера активен + +private static readonly DependencyPropertyKey IsResizingPropertyKey = + DependencyProperty.RegisterReadOnly( + nameof(IsResizing), + typeof(bool), + typeof(Resize), + new(false)); + +public static readonly DependencyProperty IsResizingProperty = IsResizingPropertyKey.DependencyProperty; + +public bool IsResizing +{ + get => (bool)GetValue(IsResizingProperty); + private set => SetValue(IsResizingPropertyKey, value); +} + +#endregion + +#region ResizeStarted : ICommand - Команда начала изменения размера + +public static readonly DependencyProperty ResizeStartedProperty = + DependencyProperty.Register( + nameof(ResizeStarted), + typeof(ICommand), + typeof(Resize), + new(default(ICommand))); + +public ICommand ResizeStarted +{ + get => (ICommand)GetValue(ResizeStartedProperty); + set => SetValue(ResizeStartedProperty, value); +} + +#endregion + +private Point _StartMousePosition; +private Size _StartSize; +private ResizeDirection _CurrentDirection; + +private void OnMouseDown(object Sender, MouseButtonEventArgs E) +{ + if (!MouseInArea) return; + + var control = (Control)Sender; + _StartMousePosition = E.GetPosition(control.Parent as UIElement); + _StartSize = new Size(control.Width, control.Height); + _CurrentDirection = GetResizeDirection(); + + IsResizing = true; + Mouse.Capture(control); + + control.MouseMove += OnMouseMoveWhileResizing; + control.MouseUp += OnMouseUpAfterResize; + + ResizeStarted?.Execute(new ResizeEventArgs(_StartSize, _CurrentDirection)); +} + +private void OnMouseMoveWhileResizing(object Sender, MouseEventArgs E) +{ + var control = (Control)Sender; + var current_pos = E.GetPosition(control.Parent as UIElement); + var delta = current_pos - _StartMousePosition; + + ApplyResize(control, delta); +} + +private void ApplyResize(Control control, Vector delta) +{ + // Реализация изменения размера в зависимости от _CurrentDirection +} +``` + +--- + +## Улучшения удобства использования + +### 4. **DragBehavior.cs** - Упрощение настройки ограничений + +**Проблема:** +Для ограничения области перемещения нужно задавать 4 отдельных свойства: `Xmin`, `Xmax`, `Ymin`, `Ymax`. + +**Рекомендация:** +Добавить свойство `Bounds` типа `Rect` для удобной настройки: + +```csharp +#region Bounds : Rect - Границы области перемещения + +public static readonly DependencyProperty BoundsProperty = + DependencyProperty.Register( + nameof(Bounds), + typeof(Rect), + typeof(DragBehavior), + new(Rect.Empty, OnBoundsChanged)); + +private static void OnBoundsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) +{ + if (d is not DragBehavior behavior) return; + var bounds = (Rect)e.NewValue; + + if (bounds.IsEmpty) return; + + behavior.Xmin = bounds.Left; + behavior.Ymin = bounds.Top; + behavior.Xmax = bounds.Right; + behavior.Ymax = bounds.Bottom; +} + +public Rect Bounds +{ + get => (Rect)GetValue(BoundsProperty); + set => SetValue(BoundsProperty, value); +} + +#endregion +``` + +**Приоритет:** 🟢 Низкий + +**Преимущества:** +- Проще в использовании в XAML +- Можно привязать к `Rect` из ViewModel +- Более декларативный подход + +--- + +### 5. **UserInputBehavior.cs** - Расширение поддержки событий + +**Проблема:** +Поддерживаются только базовые события мыши и клавиатуры. + +**Рекомендация:** +Добавить поддержку: +- Средней кнопки мыши (`MiddleMouseDownCommand`, `MiddleMouseUpCommand`) +- Правой кнопки мыши (`RightMouseDownCommand`, `RightMouseUpCommand`) +- Двойного щелчка (`DoubleClickCommand`) +- Событий касания для сенсорных экранов (`TouchDownCommand`, `TouchUpCommand`, `TouchMoveCommand`) + +**Приоритет:** 🟡 Средний + +**Преимущества:** +- Более полная поддержка пользовательского ввода +- Совместимость с сенсорными устройствами +- Универсальность поведения + +--- + +### 6. **MouseControlBehavior.cs** - Оптимизация вычислений + +**Проблема:** +При каждом изменении позиции или размера пересчитывается относительная позиция: + +```csharp +private static void OnMousePositionChanged(DependencyObject D, DependencyPropertyChangedEventArgs E) +{ + var behavior = (MouseControlBehavior)D; + var pos = (Point)E.NewValue; + var size = behavior.AssociatedObject.RenderSize; + behavior.MousePositionRelative = new(pos.X / size.Width, pos.Y / size.Height); +} +``` + +**Рекомендация:** +- Добавить флаг `CalculateRelativePosition` для отключения расчётов при необходимости +- Кэшировать размер элемента вместо обращения к `AssociatedObject.RenderSize` +- Добавить проверку на деление на ноль + +**Приоритет:** 🟡 Средний + +**Улучшенная версия:** +```csharp +#region CalculateRelativePosition : bool - Вычислять относительную позицию + +public static readonly DependencyProperty CalculateRelativePositionProperty = + DependencyProperty.Register( + nameof(CalculateRelativePosition), + typeof(bool), + typeof(MouseControlBehavior), + new(true)); + +public bool CalculateRelativePosition +{ + get => (bool)GetValue(CalculateRelativePositionProperty); + set => SetValue(CalculateRelativePositionProperty, value); +} + +#endregion + +private Size _CachedSize; + +private static void OnMousePositionChanged(DependencyObject D, DependencyPropertyChangedEventArgs E) +{ + var behavior = (MouseControlBehavior)D; + if (!behavior.CalculateRelativePosition) return; + + var pos = (Point)E.NewValue; + var size = behavior._CachedSize; + + if (size.Width <= 0 || size.Height <= 0) return; + + behavior.MousePositionRelative = new(pos.X / size.Width, pos.Y / size.Height); +} + +private void OnSizeChanged(object Sender, SizeChangedEventArgs E) +{ + _CachedSize = E.NewSize; + ElementSize = E.NewSize; +} +``` + +--- + +## Улучшения производительности + +### 7. **DragInCanvasBehavior.cs** - Оптимизация MoveTo + +**Проблема:** +При каждом движении мыши вызывается `FindLogicalParent` для получения родителя: + +```csharp +private void MoveTo(Point point) +{ + _InMove = true; + var obj = AssociatedObject; + + FrameworkElement? parent = null; + if (x_max <= 0) + { + parent = obj.FindLogicalParent(); + x_max = parent.ActualWidth + x_max; + } + + if (y_max <= 0) + { + parent ??= obj.FindLogicalParent(); + y_max = parent.ActualHeight + y_max; + } + // ... +} +``` + +**Рекомендация:** +Кэшировать родительский элемент в поле класса: + +```csharp +private FrameworkElement? _ParentElement; + +protected override void OnAttached() +{ + base.OnAttached(); + + var obj = AssociatedObject; + _ParentElement = obj.FindLogicalParent(); + + if (VisualTreeHelper.GetParent(obj) is not Canvas canvas) + return; + + _Canvas = canvas; + // ... +} + +private void MoveTo(Point point) +{ + _InMove = true; + var obj = AssociatedObject; + var (width, height) = (obj.ActualWidth, obj.ActualHeight); + + var (x_min, x_max) = CheckMinMax(Xmin, Xmax); + var (y_min, y_max) = CheckMinMax(Ymin, Ymax); + + if (x_max <= 0 && _ParentElement != null) + x_max = _ParentElement.ActualWidth + x_max; + + if (y_max <= 0 && _ParentElement != null) + y_max = _ParentElement.ActualHeight + y_max; + // ... +} +``` + +**Приоритет:** 🟡 Средний + +**Преимущества:** +- Уменьшение количества обходов дерева элементов +- Улучшение производительности при частых перемещениях +- Меньше аллокаций памяти + +--- + +### 8. **TranslateMoveBehavior.cs** - Оптимизация операций с Transform + +**Проблема:** +При перемещении создаётся новый кортеж на каждое событие `MouseMove`: + +```csharp +private void OnMouseMove(object Sender, MouseEventArgs E) => + (_Transform.X, _Transform.Y) = _StartMousePosition.Substrate(E.GetPosition(_Parent)); +``` + +**Рекомендация:** +Использовать локальные переменные для уменьшения аллокаций: + +```csharp +private void OnMouseMove(object Sender, MouseEventArgs E) +{ + var current_pos = E.GetPosition(_Parent); + var delta = _StartMousePosition.Substrate(current_pos); + + _Transform.X = delta.X; + _Transform.Y = delta.Y; +} +``` + +**Приоритет:** 🟢 Низкий + +--- + +## Улучшения безопасности и надёжности + +### 9. **DragBehavior.cs** - Защита от утечек памяти + +**Проблема:** +`ObjectMover` подписывается на события, но при некоторых сценариях может не отписаться: + +```csharp +private void OnMouseMove(object Sender, MouseEventArgs E) +{ + var element = (FrameworkElement)Sender; + if (Equals(Mouse.Captured, element)) + { + // ... логика перемещения + return; + } + Dispose(); // Отписка только если capture потерян +} +``` + +**Рекомендация:** +- Добавить WeakEventManager для подписок +- Использовать `try-finally` для гарантии очистки +- Добавить таймаут для автоматической отписки + +```csharp +protected ObjectMover(FrameworkElement element, DragBehavior behavior) +{ + _MovingElement = element; + _Behavior = behavior; + + try + { + var parent = element.FindLogicalParent() + ?? throw new InvalidOperationException("Не найден родительский элемент"); + _ParentElement = parent; + _StartMousePos = Mouse.GetPosition(_ParentElement); + + // ... остальная инициализация + + Mouse.Capture(element, CaptureMode.SubTree); + + // Использование WeakEventManager вместо прямой подписки + WeakEventManager.AddHandler( + element, nameof(UIElement.MouseMove), OnMouseMove); + WeakEventManager.AddHandler( + element, nameof(UIElement.MouseLeftButtonUp), OnLeftMouseUp); + } + catch + { + Dispose(); + throw; + } +} + +public void Dispose() +{ + if (_IsDisposed) return; + _IsDisposed = true; + + WeakEventManager.RemoveHandler( + _MovingElement, nameof(UIElement.MouseMove), OnMouseMove); + WeakEventManager.RemoveHandler( + _MovingElement, nameof(UIElement.MouseLeftButtonUp), OnLeftMouseUp); + + _MovingElement.ReleaseMouseCapture(); + + _Behavior.Radius = double.NaN; + _Behavior.Angle = double.NaN; + _Behavior.dx = double.NaN; + _Behavior.dy = double.NaN; +} + +private bool _IsDisposed; +``` + +**Приоритет:** 🔴 Высокий + +--- + +### 10. **DragInCanvasBehavior.cs** - Проверка валидности Canvas + +**Проблема:** +Потенциальный `NullReferenceException` при обращении к `_Canvas`: + +```csharp +private void OnMouseMove(object sender, MouseEventArgs e) +{ + var canvas = _Canvas; // Может быть null + + if (!_IsDragging || Mouse.LeftButton != MouseButtonState.Pressed) + { + Mouse.Capture(null); + canvas!.MouseMove -= OnMouseMove; // Потенциальный NPE + canvas.MouseLeftButtonUp -= OnMouseLeftButtonUp; + return; + } + + MoveTo(e.GetPosition(canvas)); +} +``` + +**Рекомендация:** +Добавить проверки и защитное программирование: + +```csharp +private void OnMouseMove(object sender, MouseEventArgs e) +{ + if (_Canvas is not { } canvas) + { + IsDragging = false; + return; + } + + if (!_IsDragging || Mouse.LeftButton != MouseButtonState.Pressed) + { + Mouse.Capture(null); + canvas.MouseMove -= OnMouseMove; + canvas.MouseLeftButtonUp -= OnMouseLeftButtonUp; + IsDragging = false; + return; + } + + MoveTo(e.GetPosition(canvas)); +} +``` + +**Приоритет:** 🔴 Высокий + +--- + +### 11. **MouseControlBehavior.cs** - Отписка от событий в OnDetaching + +**Проблема:** +Не отписываются от событий `MouseDown` и `MouseUp`: + +```csharp +protected override void OnDetaching() +{ + var element = AssociatedObject; + element.MouseMove -= OnMouseMove; + element.SizeChanged -= OnSizeChanged; + // Отсутствует отписка от MouseDown и MouseUp +} +``` + +**Рекомендация:** +Добавить полную очистку: + +```csharp +protected override void OnDetaching() +{ + var element = AssociatedObject; + element.MouseMove -= OnMouseMove; + element.SizeChanged -= OnSizeChanged; + element.MouseDown -= OnMouseDown; + element.MouseUp -= OnMouseUp; + + element.ReleaseMouseCapture(); + IsLeftMouseDown = false; +} +``` + +**Приоритет:** 🔴 Высокий + +--- + +## Расширение функциональности + +### 12. **DragBehavior** - Добавление инерции и анимации + +**Рекомендация:** +Добавить поддержку инерционного перемещения (flick) с затуханием: + +```csharp +#region EnableInertia : bool - Включить инерцию перемещения + +public static readonly DependencyProperty EnableInertiaProperty = + DependencyProperty.Register( + nameof(EnableInertia), + typeof(bool), + typeof(DragBehavior), + new(false)); + +public bool EnableInertia +{ + get => (bool)GetValue(EnableInertiaProperty); + set => SetValue(EnableInertiaProperty, value); +} + +#endregion + +#region InertiaDecelerationRate : double - Скорость затухания инерции + +public static readonly DependencyProperty InertiaDecelerationRateProperty = + DependencyProperty.Register( + nameof(InertiaDecelerationRate), + typeof(double), + typeof(DragBehavior), + new(0.95)); + +public double InertiaDecelerationRate +{ + get => (double)GetValue(InertiaDecelerationRateProperty); + set => SetValue(InertiaDecelerationRateProperty, value); +} + +#endregion + +private Vector _Velocity; +private DateTime _LastMoveTime; + +private void OnMouseMove(object Sender, MouseEventArgs E) +{ + // ... существующая логика + + if (EnableInertia) + { + var now = DateTime.Now; + var time_delta = (now - _LastMoveTime).TotalSeconds; + + if (time_delta > 0) + _Velocity = new Vector(dx / time_delta, dy / time_delta); + + _LastMoveTime = now; + } +} + +private void OnLeftMouseUp(object? Sender, MouseButtonEventArgs? E) +{ + if (EnableInertia && _Velocity.Length > 10) + StartInertiaAnimation(); + + Dispose(); +} + +private void StartInertiaAnimation() +{ + var animation_timer = new DispatcherTimer + { + Interval = TimeSpan.FromMilliseconds(16) // ~60 FPS + }; + + animation_timer.Tick += (s, e) => + { + _Velocity *= InertiaDecelerationRate; + + if (_Velocity.Length < 1) + { + animation_timer.Stop(); + return; + } + + // Применить смещение с учётом скорости + var delta_x = _Velocity.X * 0.016; + var delta_y = _Velocity.Y * 0.016; + + OnMouseMove(_MovingElement, delta_x, delta_y); + }; + + animation_timer.Start(); +} +``` + +**Приоритет:** 🟢 Низкий + +**Преимущества:** +- Более естественное поведение перемещения +- Улучшенный UX, особенно на сенсорных устройствах +- Современный визуальный эффект + +--- + +### 13. **Resize.cs** - Поддержка пропорционального изменения + +**Рекомендация:** +Добавить возможность сохранения пропорций при изменении размера: + +```csharp +#region MaintainAspectRatio : bool - Сохранять пропорции + +public static readonly DependencyProperty MaintainAspectRatioProperty = + DependencyProperty.Register( + nameof(MaintainAspectRatio), + typeof(bool), + typeof(Resize), + new(false)); + +public bool MaintainAspectRatio +{ + get => (bool)GetValue(MaintainAspectRatioProperty); + set => SetValue(MaintainAspectRatioProperty, value); +} + +#endregion + +#region AspectRatio : double - Соотношение сторон + +private static readonly DependencyPropertyKey AspectRatioPropertyKey = + DependencyProperty.RegisterReadOnly( + nameof(AspectRatio), + typeof(double), + typeof(Resize), + new(double.NaN)); + +public static readonly DependencyProperty AspectRatioProperty = AspectRatioPropertyKey.DependencyProperty; + +public double AspectRatio +{ + get => (double)GetValue(AspectRatioProperty); + private set => SetValue(AspectRatioPropertyKey, value); +} + +#endregion + +private void OnMouseDown(object Sender, MouseButtonEventArgs E) +{ + var control = (Control)Sender; + + if (MaintainAspectRatio) + AspectRatio = control.Width / control.Height; + + // ... остальная логика +} + +private void ApplyResize(Control control, Vector delta) +{ + var new_width = _StartSize.Width + delta.X; + var new_height = _StartSize.Height + delta.Y; + + if (MaintainAspectRatio && !double.IsNaN(AspectRatio)) + { + // Изменяем размер с сохранением пропорций + if (Math.Abs(delta.X) > Math.Abs(delta.Y)) + new_height = new_width / AspectRatio; + else + new_width = new_height * AspectRatio; + } + + control.Width = Math.Max(control.MinWidth, Math.Min(control.MaxWidth, new_width)); + control.Height = Math.Max(control.MinHeight, Math.Min(control.MaxHeight, new_height)); +} +``` + +**Приоритет:** 🟡 Средний + +--- + +### 14. **Все поведения** - Добавление событий жизненного цикла + +**Рекомендация:** +Добавить роутируемые события для интеграции с MVVM: + +```csharp +// Пример для DragBehavior + +#region DragStarted - Событие начала перетаскивания + +public static readonly RoutedEvent DragStartedEvent = + EventManager.RegisterRoutedEvent( + nameof(DragStarted), + RoutingStrategy.Bubble, + typeof(RoutedEventHandler), + typeof(DragBehavior)); + +public event RoutedEventHandler DragStarted +{ + add => AddHandler(DragStartedEvent, value); + remove => RemoveHandler(DragStartedEvent, value); +} + +protected virtual void OnDragStarted() +{ + var args = new RoutedEventArgs(DragStartedEvent, this); + RaiseEvent(args); +} + +#endregion + +#region DragCompleted - Событие завершения перетаскивания + +public static readonly RoutedEvent DragCompletedEvent = + EventManager.RegisterRoutedEvent( + nameof(DragCompleted), + RoutingStrategy.Bubble, + typeof(DragCompletedEventHandler), + typeof(DragBehavior)); + +public event DragCompletedEventHandler DragCompleted +{ + add => AddHandler(DragCompletedEvent, value); + remove => RemoveHandler(DragCompletedEvent, value); +} + +protected virtual void OnDragCompleted(Vector totalDelta) +{ + var args = new DragCompletedEventArgs(totalDelta, DragCompletedEvent, this); + RaiseEvent(args); +} + +#endregion + +// Класс аргументов события +public class DragCompletedEventArgs : RoutedEventArgs +{ + public Vector TotalDelta { get; } + + public DragCompletedEventArgs(Vector totalDelta, RoutedEvent routedEvent, object source) + : base(routedEvent, source) + { + TotalDelta = totalDelta; + } +} + +public delegate void DragCompletedEventHandler(object sender, DragCompletedEventArgs e); +``` + +**Приоритет:** 🟡 Средний + +**Преимущества:** +- Лучшая интеграция с MVVM +- Возможность реагировать на события в XAML +- Поддержка туннелирования/всплытия событий + +--- + +## Сводная таблица приоритетов + +| № | Файл | Улучшение | Категория | Приоритет | Сложность | Польза | +|---|------|-----------|-----------|-----------|-----------|--------| +| 1 | `DragBehavior.cs` | Унификация с DragInCanvasBehavior | Архитектура | 🔴 Высокий | Высокая | Высокая | +| 2 | `Resize.cs` | Завершение реализации изменения размера | Функциональность | 🔴 Высокий | Средняя | Высокая | +| 3 | `DragBehavior.cs` | Защита от утечек памяти | Надёжность | 🔴 Высокий | Средняя | Высокая | +| 4 | `DragInCanvasBehavior.cs` | Проверка валидности Canvas | Надёжность | 🔴 Высокий | Низкая | Средняя | +| 5 | `MouseControlBehavior.cs` | Отписка от событий | Надёжность | 🔴 Высокий | Низкая | Средняя | +| 6 | `ActualSizeBinding.cs` | Оптимизация обновлений размеров | Архитектура | 🟡 Средний | Средняя | Средняя | +| 7 | `UserInputBehavior.cs` | Расширение поддержки событий | Функциональность | 🟡 Средний | Низкая | Средняя | +| 8 | `MouseControlBehavior.cs` | Оптимизация вычислений | Производительность | 🟡 Средний | Низкая | Средняя | +| 9 | `DragInCanvasBehavior.cs` | Кэширование родительского элемента | Производительность | 🟡 Средний | Низкая | Низкая | +| 10 | `Resize.cs` | Пропорциональное изменение размера | Функциональность | 🟡 Средний | Средняя | Средняя | +| 11 | `Все поведения` | События жизненного цикла | Функциональность | 🟡 Средний | Средняя | Высокая | +| 12 | `DragBehavior.cs` | Упрощение настройки через Bounds | Удобство | 🟢 Низкий | Низкая | Средняя | +| 13 | `TranslateMoveBehavior.cs` | Оптимизация операций с Transform | Производительность | 🟢 Низкий | Низкая | Низкая | +| 14 | `DragBehavior.cs` | Инерция и анимация | Функциональность | 🟢 Низкий | Высокая | Средняя | + +--- + +## 📊 Статистика рекомендаций + +**По приоритету:** +- 🔴 Высокий: 5 рекомендаций (36%) +- 🟡 Средний: 6 рекомендаций (43%) +- 🟢 Низкий: 3 рекомендации (21%) + +**По категориям:** +- Архитектура: 2 рекомендации +- Функциональность: 5 рекомендаций +- Производительность: 3 рекомендации +- Надёжность: 3 рекомендации +- Удобство: 1 рекомендация + +**Общая оценка качества кода:** ⭐⭐⭐⭐ (4/5) + +--- + +## 🎯 Рекомендуемый план внедрения + +### Фаза 1: Критические улучшения (1-2 недели) +1. Завершить реализацию `Resize.cs` +2. Исправить утечки памяти в `DragBehavior.cs` +3. Добавить проверки null в `DragInCanvasBehavior.cs` +4. Исправить отписку от событий в `MouseControlBehavior.cs` + +### Фаза 2: Архитектурные изменения (2-3 недели) +1. Унифицировать `DragBehavior` и `DragInCanvasBehavior` +2. Добавить базовый класс и паттерн Strategy +3. Оптимизировать `ActualSizeBinding` + +### Фаза 3: Расширение функциональности (2-4 недели) +1. Добавить события жизненного цикла во все поведения +2. Расширить `UserInputBehavior` для поддержки всех событий +3. Добавить пропорциональное изменение размера в `Resize` +4. Добавить свойство `Bounds` в `DragBehavior` + +### Фаза 4: Оптимизация и полировка (1-2 недели) +1. Оптимизировать вычисления в `MouseControlBehavior` +2. Кэшировать часто используемые значения +3. Добавить инерцию в перемещение (опционально) + +--- + +## ✨ Заключение + +Текущее состояние поведений показывает хорошую базовую функциональность с несколькими критическими проблемами в области надёжности и утечек памяти. Основные улучшения должны быть сфокусированы на: + +1. **Надёжности** - исправление потенциальных утечек памяти и null-reference исключений +2. **Архитектуре** - унификация похожих поведений и уменьшение дублирования кода +3. **Функциональности** - завершение незавершённых реализаций и добавление событий +4. **Производительности** - кэширование и оптимизация частых операций + +После внедрения рекомендаций качество кодовой базы может достичь уровня ⭐⭐⭐⭐⭐ (5/5). From 087a5400266458b509c13c8fc62d16d5c015e884 Mon Sep 17 00:00:00 2001 From: Infarh Date: Sun, 14 Dec 2025 00:00:52 +0300 Subject: [PATCH 18/56] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20README.md=20=D1=81=20=D0=B4=D0=BE=D0=BA=D1=83?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=86=D0=B8=D0=B5=D0=B9=20=D0=BF?= =?UTF-8?q?=D0=BE=20WPF-=D0=BF=D0=BE=D0=B2=D0=B5=D0=B4=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавлен новый файл README.md, содержащий подробное описание и примеры использования всех доступных WPF-поведений из MathCore.WPF.Behaviors. Документация включает инструкции по установке, примеры XAML и C#, рекомендации по использованию, описание известных ограничений, а также информацию о лицензии и правилах вклада в проект. --- MathCore.WPF/Behaviors/README.md | 684 +++++++++++++++++++++++++++++++ 1 file changed, 684 insertions(+) create mode 100644 MathCore.WPF/Behaviors/README.md diff --git a/MathCore.WPF/Behaviors/README.md b/MathCore.WPF/Behaviors/README.md new file mode 100644 index 0000000..15ea4b4 --- /dev/null +++ b/MathCore.WPF/Behaviors/README.md @@ -0,0 +1,684 @@ +# MathCore.WPF Behaviors + +Коллекция поведений (Behaviors) для WPF-приложений, расширяющих функциональность элементов управления без изменения их кода. + +## 📋 Оглавление + +- [Установка](#установка) +- [Поведения для перемещения и изменения размера](#поведения-для-перемещения-и-изменения-размера) +- [Поведения для окон](#поведения-для-окон) +- [Поведения для пользовательского ввода](#поведения-для-пользовательского-ввода) +- [Поведения для Drag & Drop](#поведения-для-drag--drop) +- [Поведения для привязки данных](#поведения-для-привязки-данных) +- [Поведения для списков и деревьев](#поведения-для-списков-и-деревьев) +- [Утилитарные поведения](#утилитарные-поведения) + +--- + +## Установка + +```xml +xmlns:b="http://schemas.microsoft.com/xaml/behaviors" +xmlns:behaviors="clr-namespace:MathCore.WPF.Behaviors;assembly=MathCore.WPF" +``` + +--- + +## Поведения для перемещения и изменения размера + +### DragBehavior + +Универсальное поведение для перетаскивания элементов внутри различных контейнеров (Canvas, Panel, ContentControl). + +**Основные свойства:** +- `Enabled` - включение/выключение перетаскивания +- `Xmin`, `Xmax`, `Ymin`, `Ymax` - ограничения области перемещения +- `AllowX`, `AllowY` - разрешение перемещения по осям +- `CurrentX`, `CurrentY` - текущие координаты (readonly) +- `dx`, `dy` - смещение от начальной точки (readonly) +- `Radius`, `Angle` - полярные координаты смещения (readonly) + +**Пример использования:** + +```xml + + + + + +``` + +**Пример с ограничением только по горизонтали:** + +```xml + + + + + +``` + +--- + +### DragInCanvasBehavior + +Специализированное поведение для перетаскивания элементов внутри Canvas с расширенными возможностями. + +**Основные свойства:** +- `Enabled` - включение/выключение перетаскивания +- `Xmin`, `Xmax`, `Ymin`, `Ymax` - ограничения области перемещения +- `AllowX`, `AllowY` - разрешение перемещения по осям +- `CurrentX`, `CurrentY` - текущие координаты с двусторонней привязкой + +**Пример использования:** + +```xml + + + + + + + +``` + +--- + +### TranslateMoveBehavior + +Поведение для перемещения элемента с использованием `TranslateTransform` без изменения его логической позиции. + +**Пример использования:** + +```xml + + + + + +``` + +--- + +### DragWindow + +Поведение для перетаскивания окна за любой элемент внутри него. + +**Пример использования:** + +```xml + + + + + + + +``` + +--- + +### Resize + +Поведение для визуализации областей изменения размера элемента управления (отображение курсоров). + +**Основные свойства:** +- `AreaSize` - размер области захвата в пикселях (по умолчанию 3) +- `TopResizing`, `BottomResizing`, `LeftResizing`, `RightResizing` - включение изменения размера с соответствующих сторон + +**Пример использования:** + +```xml + + + + + +``` + +--- + +### ResizeBehavior + +Расширенное поведение для изменения размера окна. + +**Пример использования:** + +```xml + + + + + +``` + +--- + +### ResizeWindowPanel + +Поведение для создания панели изменения размера окна. + +**Пример использования:** + +```xml + + + + + +``` + +--- + +## Поведения для окон + +### WindowBehavior + +Абстрактный базовый класс для поведений, работающих с окнами. Предоставляет доступ к родительскому окну через свойство `AssociatedWindow`. + +--- + +### CloseBehavior + +Поведение для окна с поддержкой анимации закрытия и установки `DialogResult`. + +**Основные свойства:** +- `CloseWithDialogResult` - триггер закрытия окна с установкой результата диалога +- `Storyboard` - анимация, которая будет воспроизведена перед закрытием окна + +**Пример использования:** + +```xml + + + + + + + + + + + +``` + +--- + +### CloseButtonBehavior + +Поведение для кнопки, закрывающей родительское окно. + +**Пример использования:** + +```xml + +``` + +--- + +### WindowSystemIconBehavior + +Поведение для отображения системной иконки окна. + +**Пример использования:** + +```xml + + + + + +``` + +--- + +### WindowTitleBarBehavior + +Поведение для создания пользовательской области заголовка окна. + +**Пример использования:** + +```xml + + + + + + + +``` + +--- + +### WindowMaximizationLimitattor + +Поведение для ограничения максимизации окна с учётом рабочей области экрана. + +**Пример использования:** + +```xml + + + + + +``` + +--- + +## Поведения для пользовательского ввода + +### UserInputBehavior + +Поведение для обработки событий мыши и клавиатуры через команды. + +**Основные свойства:** +- `LeftMouseDownCommand` - команда при нажатии левой кнопки мыши +- `LeftMouseUpCommand` - команда при отпускании левой кнопки мыши +- `MouseWheelCommand` - команда при прокрутке колеса мыши +- `KeyDownCommand` - команда при нажатии клавиши +- `KeyUpCommand` - команда при отпускании клавиши +- `Position` - текущая позиция мыши (readonly) + +**Пример использования:** + +```xml + + + + + +``` + +--- + +### MouseControlBehavior + +Поведение для отслеживания состояния мыши и её положения относительно элемента. + +**Основные свойства:** +- `MousePosition` - абсолютная позиция мыши +- `MousePositionRelative` - относительная позиция (0..1) +- `ElementSize` - размер элемента +- `IsLeftMouseDown` - состояние левой кнопки мыши +- `LeftMouseClick` - команда при клике + +**Пример использования:** + +```xml + + + + + + + + + + + + + + +``` + +--- + +## Поведения для Drag & Drop + +### DropFile + +Поведение для получения файлов через Drag-and-Drop. + +**Основные свойства:** +- `DropFileCommand` - команда, вызываемая при получении файла (параметр - `FileInfo`) + +**Пример использования:** + +```xml + + + + + + + +``` + +**Пример команды в ViewModel:** + +```csharp +public ICommand HandleDroppedFileCommand => new LambdaCommand( + file => + { + if (file is FileInfo fileInfo) + { + FilePath = fileInfo.FullName; + // Дополнительная обработка файла + } + }); +``` + +--- + +### DropData + +Поведение для получения произвольных данных через Drag-and-Drop. + +**Основные свойства:** +- `DropDataCommand` - команда, вызываемая при получении данных +- `DataFormat` - формат принимаемых данных + +**Пример использования:** + +```xml + + + + + +``` + +--- + +## Поведения для привязки данных + +### ActualSizeBinding + +Поведение для двухсторонней привязки фактических размеров элемента. + +**Основные свойства:** +- `ActualWidth` - ширина элемента (двусторонняя привязка) +- `ActualHeight` - высота элемента (двусторонняя привязка) + +**Пример использования:** + +```xml + + + + + + + + + +``` + +--- + +### PasswordBoxBinder + +Поведение для привязки пароля из `PasswordBox` к ViewModel (использовать с осторожностью из соображений безопасности). + +**Основные свойства:** +- `Password` - пароль для привязки + +**Пример использования:** + +```xml + + + + + +``` + +⚠️ **Предупреждение:** Хранение пароля в виде обычной строки снижает безопасность. Используйте `SecureString` где возможно. + +--- + +## Поведения для списков и деревьев + +### ListBoxItemsSelectionBinder + +Поведение для двухсторонней привязки выделенных элементов ListBox. + +**Основные свойства:** +- `SelectedItems` - коллекция выбранных элементов + +**Пример использования:** + +```xml + + + + + +``` + +**ViewModel:** + +```csharp +public ObservableCollection SelectedItems { get; } = new(); +``` + +--- + +### ListBoxScrollOnNewItem + +Поведение для автоматической прокрутки ListBox при добавлении новых элементов. + +**Пример использования:** + +```xml + + + + + +``` + +--- + +### TreeViewBindableSelectedItem + +Поведение для привязки выбранного элемента TreeView (обходит ограничение WPF). + +**Основные свойства:** +- `SelectedItem` - выбранный элемент + +**Пример использования:** + +```xml + + + + + +``` + +--- + +### TreeViewBindableSelectedDirectoryViewModelItem + +Специализированное поведение для TreeView с элементами типа `DirectoryViewModel`. + +**Пример использования:** + +```xml + + + + + +``` + +--- + +## Утилитарные поведения + +### Focused + +Поведение для автоматической установки фокуса на элемент при его инициализации. + +**Пример использования:** + +```xml + + + + + +``` + +--- + +### TextBoxSelectAllAtGotFocus + +Поведение для автоматического выделения всего текста в TextBox при получении фокуса. + +**Пример использования:** + +```xml + + + + + +``` + +--- + +### TextBoxEndCaretAtGotFocus + +Поведение для установки каретки в конец текста TextBox при получении фокуса. + +**Пример использования:** + +```xml + + + + + +``` + +--- + +## 📚 Дополнительные материалы + +### Установка пакета Microsoft.Xaml.Behaviors + +Для использования поведений необходим пакет: + +```bash +dotnet add package Microsoft.Xaml.Behaviors.Wpf +``` + +### Общие рекомендации + +1. **Производительность:** Используйте поведения разумно - каждое поведение добавляет обработчики событий +2. **Память:** Поведения автоматически отписываются от событий при отсоединении +3. **MVVM:** Поведения - отличный способ избежать code-behind и сохранить чистоту MVVM +4. **Тестирование:** Поведения можно тестировать независимо от UI + +### Создание собственного поведения + +```csharp +public class CustomBehavior : Behavior +{ + protected override void OnAttached() + { + base.OnAttached(); + // Подписка на события + AssociatedObject.Loaded += OnLoaded; + } + + protected override void OnDetaching() + { + base.OnDetaching(); + // Отписка от событий + AssociatedObject.Loaded -= OnLoaded; + } + + private void OnLoaded(object sender, RoutedEventArgs e) + { + // Логика поведения + } +} +``` + +--- + +## 🐛 Известные проблемы и ограничения + +1. **Resize.cs** - В текущей версии только отображает курсоры, фактическое изменение размера не реализовано +2. **PasswordBoxBinder** - Снижает безопасность при использовании обычных строк +3. **DragBehavior и DragInCanvasBehavior** - Имеют дублирование кода, планируется унификация + +Подробнее см. [BehaviorsAnalysisReport.md](../../BehaviorsAnalysisReport.md) и [BehaviorsImprovementRecommendations.md](../../BehaviorsImprovementRecommendations.md) + +--- + +## 📝 Лицензия + +Этот проект является частью библиотеки MathCore.WPF. + +--- + +## 🤝 Вклад в проект + +Contributions are welcome! Если вы нашли баг или хотите предложить улучшение: + +1. Создайте Issue с описанием проблемы +2. Fork репозиторий +3. Создайте ветку для ваших изменений +4. Отправьте Pull Request + +--- + +## 📧 Контакты + +- GitHub: [MathCore.WPF](https://github.com/Infarh/MathCore.WPF) +- Issues: [GitHub Issues](https://github.com/Infarh/MathCore.WPF/issues) From 1ac3f6b55034aa82dfb54d61adec9973a95a7eab Mon Sep 17 00:00:00 2001 From: Infarh Date: Sun, 14 Dec 2025 00:10:47 +0300 Subject: [PATCH 19/56] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D1=82=D1=80=D0=B5=D0=B1=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=BA=20=D0=BE=D1=84=D0=BE=D1=80=D0=BC?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D1=8E=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=8B=D1=85=20=D1=82=D0=B5=D1=81=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавлены пункты о необходимости создавать файлы тестов с учётом структуры каталогов тестируемого кода и снабжать каждый тест XML-документацией, описывающей его назначение и поведение. --- .github/copilot-instructions.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 031deac..2b1a433 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -80,4 +80,6 @@ public string PropertyName { get; set => Set(ref field, value); } - Убедись, что тесты могут выполняться в любом порядке и параллельно - Для проверки исключений используй `Assert.ThrowsException` - Используй `Debug.WriteLine` для вывода отладочной информации о процессе выполнения тестов, если в тесте есть промежуточные вычисления -- При написании Assert-методов добавляй сообщения об ошибках на русском языке \ No newline at end of file +- При написании Assert-методов добавляй сообщения об ошибках на русском языке +- Файлы модульных тестов должны создаваться с учётом структуры каталогов тестируемого кода +- Каждый модульный тест должен быть снабжён XML‑документацией, описывающей его назначение и поведение \ No newline at end of file From 2536877161c77f817536c5bd503a1b0be1c51646 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 14 Dec 2025 08:58:53 +0000 Subject: [PATCH 20/56] Initial plan From 16da26a4483042c4b5388d9af8afe7a0b581ebdf Mon Sep 17 00:00:00 2001 From: Infarh Date: Sun, 14 Dec 2025 12:40:05 +0300 Subject: [PATCH 21/56] =?UTF-8?q?=D0=9F=D0=BB=D0=B0=D0=BD=20=D1=80=D0=B5?= =?UTF-8?q?=D1=84=D0=B0=D0=BA=D1=82=D0=BE=D1=80=D0=B8=D0=BD=D0=B3=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Converters/ConverterRefactoringPlan.md | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 MathCore.WPF/Converters/ConverterRefactoringPlan.md diff --git a/MathCore.WPF/Converters/ConverterRefactoringPlan.md b/MathCore.WPF/Converters/ConverterRefactoringPlan.md new file mode 100644 index 0000000..330ee72 --- /dev/null +++ b/MathCore.WPF/Converters/ConverterRefactoringPlan.md @@ -0,0 +1,45 @@ +# План модернизации конвертеров WPF + +## Цель +Провести рефакторинг, документирование и покрытие тестами всех конвертеров в каталоге `Converters` и его подкаталогах проекта `MathCore.WPF`. + +## Объём работ +1. Добавить отсутствующие XML-комментарии для всех публичных типов и их членов +2. Проанализировать логические ошибки в реализациях конвертеров +3. Исправить найденные логические ошибки и добавить регрессионные тесты для каждого исправления +4. Написать модульные тесты для всех конвертеров +5. Подготовить подробный `README.md` в каталоге `Converters` с описанием и примерами использования всех конвертеров + +## Шаги выполнения +1. Перечислить все файлы конвертеров в каталоге `Converters` и подкаталогах +2. В каждом файле проверить наличие XML-документации для класса и публичных членов; составить список недостающих комментариев +3. Запустить статический анализ и поиск потенциальных логических ошибок (некорректные преобразования, неверные типы, неправильные ConvertBack и т.п.) +4. Провести ручной обзор реализаций конвертеров, уделив внимание: + - корректности `Convert` и `ConvertBack` + - обработке null и некорректных типов + - корректности атрибутов `ValueConversion` и `MarkupExtensionReturnType` +5. Для каждого найденного бага: + - создать задачу на исправление + - реализовать исправление в исходном коде + - добавить регрессионный тест, демонстрирующий проблему и проверяющий исправление +6. Для всех конвертеров написать набор модульных тестов, покрывающий типичные и пограничные случаи +7. Написать `README.md` с описанием каждого конвертера, параметров и примеров использования в XAML и коде +8. Запустить сборку и тесты, убедиться, что все тесты проходят + +## Формат артефактов +- `MathCore.WPF/Converters/ConverterRefactoringPlan.md` — этот файл (план) +- `MathCore.WPF/Converters/README.md` — документация по конвертерам +- Модифицированные файлы в `MathCore.WPF/Converters` с добавленными XML-комментариями и исправлениями +- Тесты в проекте `MathCore.WPF.Tests` или отдельном тест-проекте, покрывающем все конвертеры + +## Приоритеты +0. Непрерывный контроль ошибок сборки и существующих тестов +1. Исправления логики и регрессионные тесты +2. Полное покрытие тестами +3. Документирование кода и написание README + +## Примечания +- Комментарии писать на русском языке согласно проектным правилам +- Следовать текущему стилю кода и использовать современные возможности C#, совместимые с TFM +- Использовать MSTest для модульных тестов, если это соответствует проектной политике +- Выполнять тестирование абстрактных типов через конкретные реализации в тестовой среде From 3abab785ee4e090d6aecde3933dc40102d2e50f5 Mon Sep 17 00:00:00 2001 From: Infarh Date: Sun, 14 Dec 2025 12:59:10 +0300 Subject: [PATCH 22/56] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D1=87=D0=B5?= =?UTF-8?q?=D0=BD=D1=8C=20=D0=BA=D0=BE=D0=BD=D0=B2=D0=B5=D1=80=D1=82=D0=B5?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=20=D0=B4=D0=BB=D1=8F=20=D1=80=D0=B5=D1=84?= =?UTF-8?q?=D0=B0=D0=BA=D1=82=D0=BE=D1=80=D0=B8=D0=BD=D0=B3=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Converters/ConvertersToRefactoring.md | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 MathCore.WPF/Converters/ConvertersToRefactoring.md diff --git a/MathCore.WPF/Converters/ConvertersToRefactoring.md b/MathCore.WPF/Converters/ConvertersToRefactoring.md new file mode 100644 index 0000000..1f1580d --- /dev/null +++ b/MathCore.WPF/Converters/ConvertersToRefactoring.md @@ -0,0 +1,117 @@ +# Converters to refactoring + +Дата: 2025-12-14 + +Список файлов конвертеров и связанных типов в каталоге `Converters` и его подкаталогах (первичный экспорт) + +- MathCore.WPF/Converters/Abs.cs +- MathCore.WPF/Converters/Addition.cs +- MathCore.WPF/Converters/AdditionMulti.cs +- MathCore.WPF/Converters/AggregateArray.cs +- MathCore.WPF/Converters/And.cs +- MathCore.WPF/Converters/ArrayElement.cs +- MathCore.WPF/Converters/ArrayToStringConverter.cs +- MathCore.WPF/Converters/Arithmetic.cs +- MathCore.WPF/Converters/Average.cs +- MathCore.WPF/Converters/AverageMulti.cs +- MathCore.WPF/Converters/Bool2Visibility.cs +- MathCore.WPF/Converters/BoolToBrushConverter.cs +- MathCore.WPF/Converters/Base/DoubleToBool.cs +- MathCore.WPF/Converters/Base/MultiDoubleValueValueConverter.cs +- MathCore.WPF/Converters/Base/MultiValueValueConverter.cs +- MathCore.WPF/Converters/Base/SimpleDoubleValueConverter.cs +- MathCore.WPF/Converters/Base/ValueConverter.cs +- MathCore.WPF/Converters/CSplineInterp.cs +- MathCore.WPF/Converters/Cos.cs +- MathCore.WPF/Converters/Combine.cs +- MathCore.WPF/Converters/CombineMulti.cs +- MathCore.WPF/Converters/Composite.cs +- MathCore.WPF/Converters/Custom.cs +- MathCore.WPF/Converters/CustomMulti.cs +- MathCore.WPF/Converters/DataLengthString.cs +- MathCore.WPF/Converters/DefaultIfNaN.cs +- MathCore.WPF/Converters/dB.cs +- MathCore.WPF/Converters/Divide.cs +- MathCore.WPF/Converters/DivideMulti.cs +- MathCore.WPF/Converters/Deviation.cs +- MathCore.WPF/Converters/ExpConverter.cs +- MathCore.WPF/Converters/ExConverter.cs +- MathCore.WPF/Converters/FirstLastItemConverter.cs +- MathCore.WPF/Converters/GreaterThan.cs +- MathCore.WPF/Converters/GreaterThanMulti.cs +- MathCore.WPF/Converters/GreaterThanOrEqual.cs +- MathCore.WPF/Converters/GreaterOrEqualThanMulti.cs +- MathCore.WPF/Converters/InIntervalValue.cs +- MathCore.WPF/Converters/InRange.cs +- MathCore.WPF/Converters/Interpolation.cs +- MathCore.WPF/Converters/Inverse.cs +- MathCore.WPF/Converters/IO/FilePathToName.cs +- MathCore.WPF/Converters/IO/StringToFileInfo.cs +- MathCore.WPF/Converters/IsNaN.cs +- MathCore.WPF/Converters/IsNull.cs +- MathCore.WPF/Converters/IsNegative.cs +- MathCore.WPF/Converters/IsPositive.cs +- MathCore.WPF/Converters/JoinStringConverter.cs +- MathCore.WPF/Converters/LastItemConverter.cs +- MathCore.WPF/Converters/LessThan.cs +- MathCore.WPF/Converters/LessThanMulti.cs +- MathCore.WPF/Converters/LessThanOrEqual.cs +- MathCore.WPF/Converters/LessOrEqualThanMulti.cs +- MathCore.WPF/Converters/Linear.cs +- MathCore.WPF/Converters/Lambda.cs +- MathCore.WPF/Converters/LambdaConverter.cs +- MathCore.WPF/Converters/LambdaMulti.cs +- MathCore.WPF/Converters/Mapper.cs +- MathCore.WPF/Converters/MaxValue.cs +- MathCore.WPF/Converters/MinValue.cs +- MathCore.WPF/Converters/Mod.cs +- MathCore.WPF/Converters/Multiply.cs +- MathCore.WPF/Converters/MultiplyMany.cs +- MathCore.WPF/Converters/MultiValuesToCompositeCollection.cs +- MathCore.WPF/Converters/MultiValuesToEnumerable.cs +- MathCore.WPF/Converters/NANtoVisibility.cs +- MathCore.WPF/Converters/Null2Visibility.cs +- MathCore.WPF/Converters/Not.cs +- MathCore.WPF/Converters/OutRange.cs +- MathCore.WPF/Converters/Or.cs +- MathCore.WPF/Converters/Points2PathGeometry.cs +- MathCore.WPF/Converters/Range.cs +- MathCore.WPF/Converters/Reflection/AssemblyCopyright.cs +- MathCore.WPF/Converters/Reflection/AssemblyDescription.cs +- MathCore.WPF/Converters/Reflection/AssemblyTrademark.cs +- MathCore.WPF/Converters/Reflection/AssemblyConfiguration.cs +- MathCore.WPF/Converters/Reflection/AssemblyConverter.cs +- MathCore.WPF/Converters/Reflection/AssemblyProduct.cs +- MathCore.WPF/Converters/Reflection/AssemblyTitle.cs +- MathCore.WPF/Converters/Reflection/AssemblyTime.cs +- MathCore.WPF/Converters/Reflection/AssemblyVersion.cs +- MathCore.WPF/Converters/Reflection/AssemblyFileVersion.cs +- MathCore.WPF/Converters/Reflection/GetTypeAssembly.cs +- MathCore.WPF/Converters/Round.cs +- MathCore.WPF/Converters/RoundAdaptive.cs +- MathCore.WPF/Converters/Sign.cs +- MathCore.WPF/Converters/SignValue.cs +- MathCore.WPF/Converters/Sin.cs +- MathCore.WPF/Converters/SecondsToTimeSpan.cs +- MathCore.WPF/Converters/SingleValue.cs +- MathCore.WPF/Converters/SwitchConverter.cs +- MathCore.WPF/Converters/SwitchConverter2.cs +- MathCore.WPF/Converters/StringConverters/ToLower.cs +- MathCore.WPF/Converters/StringConverters/ToUpper.cs +- MathCore.WPF/Converters/Subtraction.cs +- MathCore.WPF/Converters/SubtractionMulti.cs +- MathCore.WPF/Converters/TemperatureC2F.cs +- MathCore.WPF/Converters/TemperatureF2C.cs +- MathCore.WPF/Converters/Tan.cs +- MathCore.WPF/Converters/TimeDifferential.cs +- MathCore.WPF/Converters/ToString.cs +- MathCore.WPF/Converters/Truncate.cs +- MathCore.WPF/Converters/Trunc.cs +- MathCore.WPF/Converters/ValuesToPoint.cs +- MathCore.WPF/Converters/LastItemConverter.cs +- MathCore.WPF/Converters/FirstLastItemConverter.cs +- MathCore.WPF/Converters/GreaterThanMulti.cs +- MathCore.WPF/Converters/LessThanMulti.cs + + +Примечание: это первичный экспорт, файл будет дополняться после ручного обзора каждого файла и проверки публичных членов From e99aa7d957f203cbfb45bf6c06a8b8835820a643 Mon Sep 17 00:00:00 2001 From: Infarh Date: Sun, 14 Dec 2025 14:09:00 +0300 Subject: [PATCH 23/56] =?UTF-8?q?=D0=A1=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B4=D1=80=D0=BE=D0=B1=D0=BD=D1=8B=D0=B9=20=D0=BF?= =?UTF-8?q?=D0=BB=D0=B0=D0=BD=20=D1=80=D0=B5=D1=84=D0=B0=D0=BA=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D0=B3=D0=B0=20=D1=81=20=D0=BE=D0=BF=D0=B8?= =?UTF-8?q?=D1=81=D0=B0=D0=BD=D0=B8=D0=B5=D0=BC=20=D1=82=D1=80=D0=B5=D0=B1?= =?UTF-8?q?=D1=83=D0=B5=D0=BC=D1=8B=D1=85=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=BF=D0=BE=20=D0=BA=D0=B0=D0=B6?= =?UTF-8?q?=D0=B4=D0=BE=D0=BC=D1=83=20=D1=84=D0=B0=D0=B9=D0=BB=D1=83.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Converters/ConvertersToRefactoring.md | 480 +++++++++++++++++- .../Converters/ConvertersToRefactoring2.md | 261 ++++++++++ 2 files changed, 740 insertions(+), 1 deletion(-) create mode 100644 MathCore.WPF/Converters/ConvertersToRefactoring2.md diff --git a/MathCore.WPF/Converters/ConvertersToRefactoring.md b/MathCore.WPF/Converters/ConvertersToRefactoring.md index 1f1580d..5513fca 100644 --- a/MathCore.WPF/Converters/ConvertersToRefactoring.md +++ b/MathCore.WPF/Converters/ConvertersToRefactoring.md @@ -86,7 +86,6 @@ - MathCore.WPF/Converters/Reflection/AssemblyTime.cs - MathCore.WPF/Converters/Reflection/AssemblyVersion.cs - MathCore.WPF/Converters/Reflection/AssemblyFileVersion.cs -- MathCore.WPF/Converters/Reflection/GetTypeAssembly.cs - MathCore.WPF/Converters/Round.cs - MathCore.WPF/Converters/RoundAdaptive.cs - MathCore.WPF/Converters/Sign.cs @@ -115,3 +114,482 @@ Примечание: это первичный экспорт, файл будет дополняться после ручного обзора каждого файла и проверки публичных членов + +--- + +# Проверка первых файлов + +Ниже отчёт по первым трём файлам, проверенным последовательно: `Abs.cs`, `Not.cs`, `SignValue.cs`. + +## MathCore.WPF/Converters/Abs.cs +- Комментарии: отсутствуют на уровне класса; методы используют `/// ` — требуется явная XML‑документация для публичного типа и свойств/конструкторов +- Логические ошибки: не обнаружено (реализация `Convert` возвращает `Math.Abs(v)`, `ConvertBack` возвращает исходное значение) +- Рекомендации: + - Добавить XML `` для класса и краткие комментарии для методов (русский язык по правилам проекта) + - Рассмотреть добавление проверки входных значений и примера использования в `` + +## MathCore.WPF/Converters/Not.cs +- Комментарии: отсутствуют на уровне класса; методы используют `/// ` — требуется явная XML‑документация для публичного типа +- Логические ошибки: потенциальная проблема с безопасной обработкой `null` и некорректных типов + - Текущее выражение `!(bool?) v` приводит к неочевидному поведению при `v == null` или если `v` не является `bool`/`bool?` + - Желательно явно проверять тип: `if (v is bool b) return !b; if (v is bool? nb) return nb.HasValue ? !nb.Value : (object?)null;` или возвращать `Binding.DoNothing/DependencyProperty.UnsetValue` по проектным соглашениям +- Рекомендации: + - Упростить и обезопасить `Convert`/`ConvertBack`: проверить тип входного значения и явно обработать `null` + - Добавить XML‑комментарии на класс и методы + - Добавить регрессионный тест, покрывающий поведение с `null`, `true`, `false` и некорректными типами + +## MathCore.WPF/Converters/SignValue.cs +- Комментарии: отсутствуют на уровне класса; методы используют `/// ` в базовом классе — требуется явная XML‑документация для публичных свойств и класса +- Логические ошибки: общая логика выглядит корректной, явных ошибок не обнаружено + - Поведение: для `double.NaN` возвращает `double.NaN`, при `Math.Abs(v) <= Delta` возвращает `0`, иначе возвращает `Math.Sign(W * v) * K + B` (инверсное при `Inverse == true`) + - Возможный нюанс: свойства `K`, `W` могут принимать значения 0; это допустимо, но стоит документировать ожидаемые диапазоны и поведение при нулевых/отрицательных значениях + - Атрибуты `ConstructorArgument` присутствуют, но стоит проверить соответствие имен и порядок аргументов при использовании в XAML +- Рекомендации: + - Добавить XML‑документацию для класса и всех публичных свойств (K, B, W, Delta, Inverse) + - Рассмотреть добавление `ConvertBack` или документировать, что он не реализован + - Добавить тесты для пограничных случаев: NaN, ровно Delta, отрицательные/нулевые K/W, Inverse true/false + + +--- + +# Проверка следующих файлов + +Ниже отчёт по следующим трём файлам: `Addition.cs`, `AdditionMulti.cs`, `AggregateArray.cs`. + +## MathCore.WPF/Converters/Addition.cs +- Комментарии: присутствует XML `` у класса — соответствует правилам +- Логические ошибки: не обнаружено, реализация через `SimpleDoubleValueConverter` корректна +- Замечания: + - Отсутствует атрибут `ValueConversion` (для однозначности API можно добавить `[ValueConversion(typeof(double), typeof(double))]`) + - Используется параметр конструктора `P` с PascalCase — соответствует правилам проекта для параметров +- Рекомендации: + - Добавить `ValueConversion` для явного указания типов + - Добавить краткий `` в XML для использования в XAML + +## MathCore.WPF/Converters/AdditionMulti.cs +- Комментарии: отсутствуют — требуется XML‑документация +- Логические ошибки / потенциальные проблемы: + - Первое значение обрабатывается через `vv[0] is double d ? d : System.Convert.ToDouble(vv[0])`. `Convert.ToDouble` может бросать исключение для некорректных типов; для остальных элементов используется безопасный `TryConvertToDouble` + - Непоследовательное поведение при `vv == null` (возвращается `null`) и при наличии `null` элементов (возвращается `double.NaN`) — следует документировать ожидаемое поведение + - Возвращаемые значения смешивают `null` и `double.NaN` — рекомендуется выбрать единообразную стратегию (например, всегда возвращать `double.NaN` при любой недопустимой входной комбинации или `DependencyProperty.UnsetValue`) +- Рекомендации: + - Заменить `Convert.ToDouble(vv[0])` на `DoubleValueConverter.TryConvertToDouble` для единообразия и безопасности + - Добавить XML‑документацию и тесты покрывающие `null`, некорректные типы и смешанные типы + - Документировать поведение для `vv == null` и наличия `null` внутри массива + +## MathCore.WPF/Converters/AggregateArray.cs +- Комментарии: отсутствуют — требуется XML‑документация +- Логические ошибки: не обнаружено; `SelectMany(GetItems)` корректно разворачивает вложенные перечисления +- Замечания: + - Метод `GetItems` пропускает `null` элементы (yield break) — это ожидаемое поведение, но стоит документировать + - `SelectMany` возвращает ленивое перечисление; при необходимости можно материализовать в массив/список +- Рекомендации: + - Добавить XML‑документацию для класса и приватного метода `GetItems` + - Добавить тесты, покрывающие вложенные перечисления, одиночные элементы и `null` + + +--- + +# Проверка следующих файлов + +Ниже отчёт по следующим трём файлам: `And.cs`, `ArrayElement.cs`, `ArrayToStringConverter.cs`. + +## MathCore.WPF/Converters/And.cs +- Комментарии: отсутствуют — требуется XML‑документация для класса и свойства `NullDefaultValue` +- Логические ошибки / потенциальные проблемы: + - Использование `vv?.Cast().All(v => v) ?? NullDefaultValue` приведёт to `InvalidCastException`, если входной массив содержит элементы не типа `bool` или `null` + - При наличии `null` элементов `Cast()` также бросит исключение + - Поведение при `vv == null` возвращает `NullDefaultValue` — это ожидаемо, но должно быть документировано +- Рекомендации: + - Заменить `Cast()` на безопасную проверку: перебирать `vv` и приводить каждый элемент через `is bool b` или через `TryConvert` в зависимости от соглашений проекта + - Добавить XML‑документацию и тесты для сценариев: все `true`, есть `false`, есть `null`, есть некорректные типы, `vv == null` + +## MathCore.WPF/Converters/ArrayElement.cs +- Комментарии: отсутствуют — требуется XML‑документация для класса и конструктора `Index` +- Логические ошибки / потенциальные проблемы: + - Паттерн `(v, p) switch` ожидает `p` типа `int` в ряде веток; если `p` приходит как `string` или `double`, соответствие не сработает и будет возвращено `Binding.DoNothing` + - Для `IEnumerable` используется `items.Cast().ElementAtOrDefault` — это материализует перечисление итеративно; при больших потоках это может быть медленно, но функционально корректно + - Конструктор и свойство `Index` используют одно и то же имя, текущий синтаксис с primary constructor корректен, но стоит убедиться в совместимости TFM и стиля проекта +- Рекомендации: + - Добавить явное преобразование `p` в `int` с `TryParse`/`Convert.ToInt32` + обработкой ошибок, чтобы поддерживать числовые строки и другие числовые типы + - Добавить XML‑документацию и тесты: индекс в диапазоне, индекс вне диапазона, `p` с типом `int` и `string`, входные типы `Array`, `IList`, `IEnumerable`, `null` + +## MathCore.WPF/Converters/ArrayToStringConverter.cs +- Комментарии: отсутствуют — требуется XML‑документация для класса и метода `Convert` +- Логические ошибки / потенциальные проблемы: + - При `v` не `Array` возвращается `Binding.DoNothing` — поведение следует документировать + - Текущая реализация добавляет запятую после каждого элемента и затем удаляет последний символ; корректно, но можно упростить через `string.Join` и учитывать `CultureInfo` при приведении элементов к строке + - Если элементы равны `null`, в итоговой строке появится слово `""` или `""` в зависимости от `ToString` реализации — стоит задокументировать +- Рекомендации: + - Добавить XML‑документацию и тесты для пустых массивов, массивов с `null`, массивов со сложными объектами + - Рассмотреть замену на безопасную реализацию с `string.Join(',', array.Cast().Select(x => x?.ToString() ?? string.Empty))` и учётом `CultureInfo` при форматировании чисел/дат + +--- + +# Проверка следующих файлов + +Ниже отчёт по следующим трём файлам: `Arithmetic.cs`, `Average.cs`, `AverageMulti.cs`. + +## MathCore.WPF/Converters/Arithmetic.cs +- Комментарии: отсутствуют — требуется XML‑документация для класса и методов `Convert`/`ConvertBack` +- Логические ошибки / потенциальные проблемы: + - Регулярное выражение ограничено форматом `([+\-*/]{1,1})\s{0,}(\-?[\d\.]+)` — это не учитывает экспоненциальную нотацию, запятую как десятичный разделитель по локали или пробелы в числе + - Парсинг `p` использует `double.TryParse` без указания `CultureInfo` — может некорректно работать в разных локалях + - Нет явной обработки деления на ноль в `Convert` — при `op == "/"` и `p_value == 0` вернётся `double.PositiveInfinity` или `double.NegativeInfinity`, что может быть допустимо, но лучше документировать + - Атрибут `GeneratedRegex` используется условно для NET7+, это корректно +- Рекомендации: + - Уточнить регулярное выражение или поддержать более гибкий синтаксис чисел (включая экспоненциальную нотацию) + - Передавать `CultureInfo` в `double.TryParse` или использовать `double.TryParse(pattern.Groups[2].Value, NumberStyles.Float, c, out p_value)` + - Добавить XML‑документацию и тесты для всех арифметических операций, включая деление на ноль и некорректные входные строки + +## MathCore.WPF/Converters/Average.cs +- Комментарии: присутствует XML `` и параметр конструктора — соответствует правилам +- Логические ошибки: реализация корректна, использует `AverageValue` из `MathCore.Values` +- Замечания: + - Поле `_Value` инициализируется на основе `Length` конструктора — при вызове конструктора по умолчанию `Length==0`, возможно требования документировать поведение + - Свойство `Length` напрямую меняет внутреннюю длину окна — стоит подтвердить, что `AverageValue.Length` корректно обрабатывает уменьшение/увеличение окна +- Рекомендации: + - Добавить XML‑документацию для свойства `Length` (описано) + - Добавить тесты: последовательное добавление значений, NaN сбрасывает окно, корректность при Length==0 + +## MathCore.WPF/Converters/AverageMulti.cs +- Комментарии: отсутствуют — требуется XML‑документация +- Логические ошибки / потенциальные проблемы: + - Та же проблема, что и в `AdditionMulti`: использование `Convert.ToDouble(vv[0])` может бросать исключение; лучше использовать `TryConvertToDouble` + - Непоследовательное поведение при `vv == null` и `null` элементах — следует унифицировать + - Возвращаемое значение `v / vv.Length` корректно для среднего, но при `vv.Length==0` уже обработано выше; всё ещё стоит документировать +- Рекомендации: + - Использовать `DoubleValueConverter.TryConvertToDouble` для всех элементов + - Добавить XML‑документацию и тесты: пустой массив, содержит `null`, содержит некорректные типы, проверка среднего + +--- + +# Проверка следующих файлов + +Ниже отчёт по следующим трём файлам: `Bool2Visibility.cs`, `BoolToBrushConverter.cs`, `Base/DoubleToBool.cs`. + +## MathCore.WPF/Converters/Bool2Visibility.cs +- Комментарии: отсутствуют — требуется XML‑документация для класса и публичных свойств `Inverted` и `Collapsed` +- Логические ошибки / потенциальные проблемы: + - `Convert` возвращает `null` для `v == null` — более типично возвращать `DependencyProperty.UnsetValue` или `Binding.DoNothing`; следует согласовать стратегию проекта + - `Convert` и `ConvertBack` используют `throw new NotSupportedException()` для неподдерживаемых типов — лучше возвращать `Binding.DoNothing` или `DependencyProperty.UnsetValue` по соглашениям + - В `ConvertBack` для `bool` ветка `bool => v` вернёт объект `v` который не обязательно `bool?`; следует явно вернуть `(bool)v` +- Рекомендации: + - Добавить XML‑документацию и тесты для behavior: null, Visibility input, true/false, Inverted combinations + - Уточнить и унифицировать стратегию возврата при неподдерживаемых типах (`null` vs `Binding.DoNothing`) + +## MathCore.WPF/Converters/BoolToBrushConverter.cs +- Комментарии: отсутствуют — требуется XML‑документация для класса и публичных свойств `TrueColorBrush`, `FalseColorBrush`, `NullColorBrush` +- Логические ошибки / потенциальные проблемы: + - В `Convert` для `null` возвращается `Brushes.Transparent` — это может быть неожиданным; стоит документировать или изменить на возвращение `null`/`Binding.DoNothing` + - `ConvertBack` пытается создать новый `Brush` через `typeof(Brush).GetTypeInfo().Assembly.Location` — это неочевидно и может вызывать вопросы; желательно явно указать причину или упростить до`throw new NotImplementedException()` +- Рекомендации: + - Добавить XML‑документацию и тесты: все комбинации true/false/null, проверки на типы Brush + - Упростить или документировать логику в `ConvertBack` + +## MathCore.WPF/Converters/Base/DoubleToBool.cs +- Комментарии: отсутствуют — требуется XML‑документация для класса и свойств `TrueLimit`, `FalseLimit` +- Логические ошибки / потенциальные проблемы: + - В текущей реализации `Convert` для значений `v` между `TrueLimit` и `FalseLimit` (включительно) будет возвращено `true`, что может быть нежелательным; возможно, требуется изменить логику на строгие сравнения + - `TrueLimit` и `FalseLimit` могут принимать значения по умолчанию (`0`, `1`, `double.NaN`), что может приводить к неожиданному поведению; стоит уточнить в документации +- Рекомендации: + - Добавить XML‑документацию и тесты для пограничных значений, NaN, Infinity, сценариев с нулевыми пределами + - Рассмотреть возможность изменения логики `Convert` на строгие границы истинности + +--- + +# Проверка следующих файлов + +Ниже отчёт по следующим трём файлам: `Base/MultiDoubleValueValueConverter.cs`, `Base/MultiValueValueConverter.cs`, `Base/SimpleDoubleValueConverter.cs`. + +## MathCore.WPF/Converters/Base/MultiDoubleValueValueConverter.cs +- Комментарии: отсутствуют — требуется XML‑документация для класса и публичных свойств `Min` и `Max` +- Логические ошибки / потенциальные проблемы: + - В `Convert` методе при невозможности конвертации элементов `DoubleValueConverter.ConvertToDouble` может бросать исключение; следует рассмотреть использование `TryConvertToDouble` и uniform handling + - После попытки конвертации возвращаемое значение `result` может быть `double.NaN` и затем ограничиваться `Min/Max` — проверять порядок применения ограничений + - В `ConvertBack` при невозможности преобразования возвращается `null`, а при успехе `ConvertBack(...)? .Cast().ToArray()` может вернуть `null`/пустой массив — следует документировать + - Метод `ConvertBack(double v)` вызывает `base.ConvertBack(null, null, null, null);` — это похоже на ошибочный вызов и не нужен +- Рекомендации: + - Заменить `ConvertToDouble` на `TryConvertToDouble` в `Convert` и обработать ошибки единообразно + - Удалить бесполезный вызов `base.ConvertBack(null, null, null, null);` в `ConvertBack(double)` + - Добавить XML‑документацию и тесты + +## MathCore.WPF/Converters/Base/MultiValueValueConverter.cs +- Комментарии: присутствуют XML‑комментарии у публичных членов — соответствует правилам +- Логические ошибки: не обнаружено (реализация шаблонная и корректная) +- Рекомендации: + - Добавить пример использования в `` при необходимости + - Проверить стиль XML‑комментариев по проектным правилам + +## MathCore.WPF/Converters/Base/SimpleDoubleValueConverter.cs +- Комментарии: присутствуют XML‑комментарии — соответствует правилам +- Логические ошибки: не обнаружено; класс реализует шаблон для простых операций +- Рекомендации: + - Добавить тесты для нескольких реализаций (Addition, Subtraction и т.д.) + - Убедиться, что `Parameter` корректно документирован и используется в наследниках + +--- + +# Проверка следующих файлов + +Ниже отчёт по следующим трём файлам: `CSplineInterp.cs`, `Cos.cs`, `Combine.cs`. + +## MathCore.WPF/Converters/CSplineInterp.cs +- Комментарии: отсутствуют — требуется XML‑документация для класса и публичного свойства `Points` +- Логические ошибки / потенциальные проблемы: + - В `ProvideValue` используется `if(Points is null || Points.Count == 0) throw new FormatException();` — лучше использовать конкретную ошибку с описанием или возвращать `Binding.DoNothing` по соглашениям + - Конструктор `CSplineInterp()` использует посыл `this([])` — синтаксис с пустым массивом может не соответствовать целевому TFM; проверить совместимость + - Поля `_SplineTo` и `_SplineFrom` инициализируются в `ProvideValue`, но нет проверки `ProvideValue` была ли вызвана до `Convert` — `Convert` использует `_SplineTo!` с null-forgiving; нужно гарантировать, что `ProvideValue` вызывается до использования или защититься +- Рекомендации: + - Добавить XML‑документацию и тесты, покрывающие пустые значения `Points`, корректное интерполирование и `ConvertBack` + - В `ProvideValue` выбрасывать `ArgumentException` с описанием или возвращать `Binding.DoNothing` по соглашениям + - Добавить защиту в `Convert`/`ConvertBack` на случай, если `ProvideValue` не был вызван (например, ленивую инициализацию) + +## MathCore.WPF/Converters/Cos.cs +- Комментарии: отсутствуют на уровне класса и свойств — требуется XML‑документация +- Логические ошибки: не обнаружено; `Convert` корректно обрабатывает `NaN` и возвращает `Math.Cos(W * v) * K + B` +- Рекомендации: + - Добавить XML‑документацию для класса и свойств `K`, `B`, `W` + - Добавить тесты для NaN, обычных значений и `ConvertBack` + +## MathCore.WPF/Converters/Combine.cs +- Комментарии: присутствуют XML‑комментарии у класса и основных членов — соответствует правилам +- Логические ошибки / потенциальные проблемы: + - В `ConvertBack` секция с `other` повторяется дважды подряд: сначала цикл `for (var i = other.Length - 1; i >= 0; i--) if (other[i] is { } converter) v = converter.ConvertBack(v, t, p, c);` затем почти идентичный цикл с `conv` — это дублирование; вероятно, вторая секция должна быть удалена + - Конструкторы и свойства используют primary constructor синтаксис; проверить совместимость с TFM +- Рекомендации: + - Удалить дублирование в `ConvertBack` и добавить тесты, проверяющие обратную последовательность преобразований + - Добавить `` в XML по использованию в XAML + +--- + +# Проверка следующих файлов + +Ниже отчёт по следующим трём файлам: `CombineMulti.cs`, `Composite.cs`, `Custom.cs`. + +## MathCore.WPF/Converters/CombineMulti.cs +- Комментарии: отсутствуют — требуется XML‑документация для класса и свойств `First`, `Then` +- Логические ошибки / потенциальные проблемы: + - В `Convert` бросается `InvalidOperationException`, если `First` не задан — это соответствует контракту, но лучше документировать поведение и обеспечить более понятное сообщение + - В `ConvertBack` метод при `Then` вызывает `then.ConvertBack(..., v != null ? v.GetType() : typeof(object), p, c)` — второй параметр `Type[]? tt` ожидает массив типов, а передаётся одиночный `Type`, это несоответствие сигнатуре `IMultiValueConverter.ConvertBack` и может привести к ошибкам при выполнении +- Рекомендации: + - Исправить вызов `then.ConvertBack` на корректную сигнатуру или документировать ожидаемую реализацию `then` + - Добавить XML‑документацию и тесты для последовательного комбинирования конвертеров + +## MathCore.WPW/Converters/Composite.cs +- Комментарии: присутствуют частично — требуется XML‑документация для класса (есть атрибут `ContentProperty`) и публичной коллекции `Converters` +- Логические ошибки / потенциальные проблемы: + - Метод `AddChild` использует `default` ветку перед `null` веткой в `switch`, что приведёт к `ArgumentException` вместо `ArgumentNullException` для `null` значений; порядок `case null` должен идти раньше `default` + - Сообщение исключения в `AddChild` использует `$"Объект {value.GetType()} ..."` при `value == null` приведёт к `NullReferenceException` — ещё одна причина обработать `null` заранее +- Рекомендации: + - Переместить `case null` перед `default` и улучшить текст исключения + - Добавить XML‑документацию для класса и методов, а также тесты + +## MathCore.WPW/Converters/Custom.cs +- Комментарии: отсутствуют — требуется XML‑документация для класса и делегатных свойств +- Логические ошибки / потенциальные проблемы: + - Свойства `Forward`, `ForwardParam`, `Backward`, `BackwardParam` могут быть `null` — текущая логика использует `Forward is null ? ForwardParam?.Invoke(v, p) : Forward(v)` что означает, если и `Forward` и `ForwardParam` null, вернётся `null` — документировать ожидаемое поведение + - Использование `Func`-полей в XAML может быть проблематичным — нужно документировать способ назначения этих функций (в коде, не в XAML) +- Рекомендации: + - Добавить XML‑документацию и тесты для всех комбинаций заполнения делегатов + - Рассмотреть изменение сигнатур на `Func` гарантирующие не-null результаты при необходимости + +--- + +# Проверка следующих файлов + +Ниже отчёт по следующим трём файлам: `CustomMulti.cs`, `DataLengthString.cs`, `DefaultIfNaN.cs`. + +## MathCore.WPF/Converters/CustomMulti.cs +- Комментарии: отсутствуют — требуется XML‑документация для класса и делегатных свойств +- Логические ошибки / потенциальные проблемы: + - Аналогично `Custom.cs`, делегаты могут быть `null` — текущая логика вернёт `null`, если оба `Forward` и `ForwardParam` равны `null` + - Использование делегатов в XAML проблематично — задокументировать способ назначения делегатов в коде +- Рекомендации: + - Добавить XML‑документацию и тесты для всех комбинаций делегатных свойств + - Рассмотреть валидацию на уровне ProvideValue или конструкторов + +## MathCore.WPF/Converters/DataLengthString.cs +- Комментарии: отсутствуют — требуется XML‑документация для класса +- Логические ошибки / потенциальные проблемы: + - В `Convert` используется `System.Convert.ToDouble(v)` без проверки `v == null` или проверки типа — это вызовет исключение `FormatException`/`NullReferenceException` если `v` не является числом + - Тип `ValueConversion` указан как `(double, DataLength)`, но `Convert` принимает `object? v` и напрямую конвертирует — лучше использовать `DoubleValueConverter.TryConvertToDouble` для безопасности +- Рекомендации: + - Добавить проверки на `null` и использовать безопасную конвертацию + - Добавить XML‑документацию и тесты: вход `null`, строковые значения, неожиданные типы + +## MathCore.WWP/Converters/DefaultIfNaN.cs +- Комментарии: отсутствуют — требуется XML‑документация для класса и свойства `DefaultValue` +- Логические ошибки / потенциальные проблемы: + - Primary constructor синтаксис `DefaultIfNaN(double DefaultValue)` используется; убедиться в совместимости с TFM + - `Convert` возвращает `v` по умолчанию если не double — возможно лучше возвращать `Binding.DoNothing` или `DependencyProperty.UnsetValue` при некорректном типе +- Рекомендации: + - Добавить XML‑документацию и тесты для NaN, non-double, null + - Рассмотреть явную проверку типа и безопасное приведение + +--- + +# Проверка следующих файлов + +Ниже отчёт по следующим трём файлам: `FirstLastItemConverter.cs`, `GreaterThan.cs`, `GreaterThanMulti.cs`. + +## MathCore.WPW/Converters/FirstLastItemConverter.cs +- Комментарии: отсутствуют — требуется XML‑документация для класса `FirstItemConverter` +- Логические ошибки / потенциальные проблемы: + - `GetFirstValue` использует ручную работу с `IEnumerator` и корректно освобождает ресурс в `finally`; можно упростить через `foreach` и `yield return`, но поведение корректно + - В `Convert` используются pattern matching и `Binding.DoNothing` для неподдерживаемых типов — поведение следует документировать +- Рекомендации: + - Добавить XML‑документацию и тесты: пустые коллекции, массивы, IList, IEnumerable + - Рассмотреть упрощение `GetFirstValue` через `foreach` для читабельности + +## MathCore.WPW/Converters/GreaterThan.cs +- Комментарии: отсутствуют — требуется XML‑документация для класса и свойства `Value` +- Логические ошибки: не обнаружено; реализация через `DoubleToBool` корректна +- Рекомендации: + - Добавить XML‑документацию и тесты для NaN, значения равные порогу, больше/меньше порога + +## MathCore.WPW/Converters/GreaterThanMulti.cs +- Комментарии: отсутствуют — требуется XML‑документация +- Логические ошибки: не обнаружено; метод правильно проверяет, что все последующие значения меньше первого +- Рекомендации: + - Добавить XML‑документацию и тесты для различных комбинаций значений, NaN, null + +--- + +# Проверка следующих файлов + +Ниже отчёт по следующим трём файлам: `GreaterThanOrEqual.cs`, `GreaterOrEqualThanMulti.cs`, `InIntervalValue.cs`. + +## MathCore.WPW/Converters/GreaterThanOrEqual.cs +- Комментарии: отсутствуют — требуется XML‑документация для класса и свойства `Value` +- Логические ошибки: не обнаружено; реализация через `DoubleToBool` корректна +- Рекомендации: + - Добавить XML‑документацию и тесты для NaN, значения равные порогу, больше/меньше порога + +## MathCore.WPW/Converters/GreaterOrEqualThanMulti.cs +- Комментарии: отсутствуют — требуется XML‑документация +- Логические ошибки: не обнаружено; метод правильно проверяет, что все последующие значения не больше первого +- Рекомендации: + - Добавить XML‑документацию и тесты для различных комбинаций значений, NaN, null + +## MathCore.WPW/Converters/InIntervalValue.cs +- Комментарии: отсутствуют — требуется XML‑документация для класса и конструктора `Interval` +- Логические ошибки / потенциальные проблемы: + - Primary constructor синтаксис `InIntervalValue(Interval interval)` используется; убедиться в совместимости с TFM + - Свойства `Min` и `Max` используют `ConstructorArgument` с именем `Min`/`Max` но обращаются к `interval` — порядок и имена аргументов нужно проверить + - Использование `is not double.NaN and var value` в `Convert` может быть семантически неверным — `double.NaN` сравнением не работает; лучше использовать `double.IsNaN` для проверки NaN +- Рекомендации: + - Заменить `is not double.NaN` на `!double.IsNaN(...)` + - Добавить XML‑документацию и тесты на нормализацию, NaN и граничные условия + +--- + +# Проверка следующих файлов + +Ниже отчёт по следующим трём файлам: `InRange.cs`, `Interpolation.cs`, `Inverse.cs`. + +## MathCore.WPW/Converters/InRange.cs +- Комментарии: отсутствуют — требуется XML‑документация для класса и свойств `Min`, `Max`, `MinInclude`, `MaxInclude` +- Логические ошибки / потенциальные проблемы: + - Primary constructor синтаксис `InRange(Interval interval)` используется; проверить совместимость с TFM + - Свойства `Min`/`Max` используют `ConstructorArgument` с именем `Min`/`Max` но обращаются к `interval` — нужно проверить соответствие имен и аргументов + - Метод `Convert` использует `v.IsNaN()` — если это расширение, убедиться, что оно присутствует; альтернативно использовать `double.IsNaN(v)` +- Рекомендации: + - Добавить XML‑документацию и тесты + - Убедиться в наличии расширения `IsNaN()` или заменить на `double.IsNaN` для совместимости + +## MathCore.WPW/Converters/Interpolation.cs +- Комментарии: отсутствуют — требуется XML‑документация для класса и свойства `Points` +- Логические ошибки / потенциальные проблемы: + - В `Convert` используется `_Polynom!.Value(v)` с null-forgiving; если `Points` не были установлены, это приведёт к NRE. Надо либо лениво инициализировать, либо возвращать `Binding.DoNothing`/`double.NaN` + - Конструктор `PointCollection` инициализация в сеттере использует `value.Select` — если `value` пустая коллекция, возможна ошибка при создании полинома (деление на ноль). Документировать ожидаемое поведение +- Рекомендации: + - Добавить проверку `_Polynom` в `Convert` и возврат `double.NaN` или `Binding.DoNothing`, если не инициализирован + - Добавить XML‑документацию и тесты для пустых/неполных`Points` + +## MathCore.WPW/Converters/Inverse.cs +- Комментарии: отсутствуют — требуется XML‑документация для класса +- Логические ошибки / потенциальные проблемы: + - Attribute `[MarkupExtensionReturnType(typeof(double))]` вероятно неверен — должен быть `typeof(Inverse)` или `typeof(IValueConverter)` + - Деление `1 / v` не защищено от `v == 0` — вернётся ±Infinity или NaN при v==0; документировать или обрабатывать +- Рекомендации: + - Исправить `MarkupExtensionReturnType` на `typeof(Inverse)` + - Добавить XML‑документацию и тесты; рассмотреть обработку нуля или документировать поведение + +--- + +# Проверка следующих файлов + +Ниже отчёт по следующим трём файлам: `IsNull.cs`, `IsNegative.cs`, `IsPositive.cs`. + +## MathCore.WWP/Converters/IsNull.cs +- Комментарии: отсутствуют — требуется XML‑документация для класса и свойства `Inverted` +- Логические ошибки / потенциальные проблемы: + - Используется primary constructor синтаксис `IsNull(bool Inverted)`; проверить совместимость с TFM + - `Convert` возвращает `Inverted ^ (v is null)` — корректно, но возвращаемый тип метода `Convert` в `ValueConverter` ожидает `object?` — результат `bool` подойдёт +- Рекомендации: + - Добавить XML‑документацию и тесты + - Рассмотреть замену синтаксиса на обычный класс с конструктором, если совместимость вызывает сомнения + +## MathCore.WPF/Converters/IsNegative.cs +- Комментарии: отсутствуют — требуется XML‑документация для класса +- Логические ошибки / потенциальные проблемы: + - Используется `v.IsNaN()` — проверить наличие расширения. Рекомендуется `double.IsNaN(v)` для ясности + - Возвращаемый тип `bool?` корректен при `DoubleToBool` контракте +- Рекомендации: + - Заменить на `double.IsNaN(v)` если нет расширения + - Добавить XML‑документацию и тесты + +## MathCore.WPF/Converters/IsPositive.cs +- Комментарии: отсутствуют — требуется XML‑документация для класса +- Логические ошибки / потенциальные проблемы: + - В `Convert` используется `v is double.NaN` — это всегда false. Нужно использовать `double.IsNaN(v)` + - Исправить поведение для NaN: возвращать `null` при NaN +- Рекомендации: + - Исправить `v is double.NaN` на `double.IsNaN(v)` + - Добавить XML‑документацию и тесты для NaN, положительных и отрицательных значений + +--- + +# Проверка следующих файлов + +Ниже отчёт по следующим трём файлам: `JoinStringConverter.cs`, `LastItemConverter.cs`, `LessThan.cs`. + +## MathCore.WPF/Converters/JoinStringConverter.cs +- Комментарии: отсутствуют — требуется XML‑документация для класса и методов +- Логические ошибки / потенциальные проблемы: + - Используются target‑typed элементы `TargetType`/`TargetTypes` с PascalCase для параметров — в проекте параметры методов обычно PascalCase, но стоит убедиться в стиле + - В `Convert` `string.Join(separator, values)` вызовет `ToString()` для каждого элемента и объединит `null` как пустую строку; ожидаемо + - В `ConvertBack` использованы сокращённые/невалидные синтаксические конструкции и неверный вызов `Split` с массивом-сепараторов — текущее выражение не скомпилируется + - Метод `ConvertBack` должен возвращать `object[]?`; корректная реализация: `return str.Split(new[] { separator }, StringSplitOptions.None).Cast().ToArray();` +- Рекомендации: + - Исправить `ConvertBack` на корректный синтаксис и добавить защиту от `null` + - Добавить XML‑документацию и тесты + +## MathCore.WPF/Converters/LastItemConverter.cs +- Комментарии: присутствуют атрибуты `MarkupExtensionReturnType` и использование pattern matching — соответствует стилю +- Логические ошибки / потенциальные проблемы: + - Использование ручного `IEnumerator` и `finally` корректно и безопасно, но можно упростить через `foreach`/LINQ если требуется + - Pattern matching `IList and [.., var last]` использует range pattern — проверить совместимость с целевыми TFM +- Рекомендации: + - Добавить XML‑документацию и тесты: пустые коллекции, `IList`, `Array`, `IEnumerable` + +## MathCore.WPF/Converters/LessThan.cs +- Комментарии: отсутствуют — требуется XML‑документация +- Логические ошибки / потенциальные проблемы: + - В `Convert` используется `v is double.NaN` — это всегда `false`; заменить на `double.IsNaN(v)` и возвращать `null` при NaN +- Рекомендации: + - Исправить проверку NaN + - Добавить XML‑документацию и тесты + +--- + +Осталось 51 файл(ов) для обработки. + +--- + +# Продолжение отчёта — перенос в `ConvertersToRefactoring2.md` + +Для уменьшения риска потери контекста и чтобы держать файл `ConvertersToRefactoring.md` компактным, дальнейшие отчёты по файлам будут записываться в `ConvertersToRefactoring2.md`. Оригинальный файл сохранён и не меняется. + +С этого момента продолжу анализ и запись отчётов в `MathCore.WPF/Converters/ConvertersToRefactoring2.md`. diff --git a/MathCore.WPF/Converters/ConvertersToRefactoring2.md b/MathCore.WPF/Converters/ConvertersToRefactoring2.md new file mode 100644 index 0000000..28b893f --- /dev/null +++ b/MathCore.WPF/Converters/ConvertersToRefactoring2.md @@ -0,0 +1,261 @@ +--- + +# Converters to refactoring — продолжение + +Дата: 2025-12-14 + +Файл продолжения для отчётов по конвертерам. Здесь ведётся дальнейшая пофайловая проверка и рекомендации. + +--- + +# Текущее состояние + +Перенос начат — все новые записи о проверке файлов будут добавляться в этот файл. Оригинальный `ConvertersToRefactoring.md` сохранён. + +--- + +# Проверка следующих файлов (продолжение) + +Ниже отчёт по следующим трём файлам: `LessThanMulti.cs`, `LessThanOrEqual.cs`, `LessOrEqualThanMulti.cs`. + +## MathCore.WPF/Converters/LessThanMulti.cs +- Комментарии: отсутствуют — требуется XML‑документация +- Логические ошибки: не обнаружено; реализация корректно сравнивает каждое последующее значение с первым и возвращает `true`, если все > первого +- Рекомендации: + - Добавить XML‑документацию и тесты для различных комбинаций (включая NaN и null) + +## MathCore.WPF/Converters/LessThanOrEqual.cs +- Комментарии: отсутствуют — требуется XML‑документация +- Логические ошибки / потенциальные проблемы: + - В `Convert` используется `v is double.NaN` — всегда `false`; заменить на `double.IsNaN(v)` и при NaN возвращать `null` +- Рекомендации: + - Исправить проверку NaN + - Добавить XML‑документацию и тесты + +## MathCore.WPF/Converters/LessOrEqualThanMulti.cs +- Комментарии: отсутствуют — требуется XML‑документация +- Логические ошибки: не обнаружено; реализация корректна и симметрична `GreaterOrEqualThanMulti` +- Рекомендации: + - Добавить XML‑документацию и тесты + +--- + +# Проверка следующих файлов (продолжение) + +Ниже отчёт по следующим трём файлам: `Linear.cs`, `Lambda.cs`, `LambdaConverter.cs`. + +## MathCore.WPF/Converters/Linear.cs +- Комментарии: класс имеет XML `` и отдельные `` для свойств — соответствует правилам +- Логические ошибки / потенциальные проблемы: + - Primary constructor синтаксис `Linear(double K, double B)` используется; проверить совместимость с TFM + - Свойство `K` используется в `From` делении `(x - b) / k` — возможное деление на ноль, следует документировать или защищать + - `ConvertBack` и `Convert` опираются на `Inverted` флаг — поведение корректно, но задокументировать +- Рекомендации: + - Добавить защиту при `K == 0` в `From` или документировать, что при K=0 будет Infinity/Exception + - Добавить тесты и примеры использования + +## MathCore.WPF/Converters/Lambda.cs +- Комментарии: отсутствуют — требуется XML‑документация для общих типов и делегатов +- Логические ошибки / потенциальные проблемы: + - Используется primary constructor для generic типа `Lambda(...)` — проверить совместимость с TFM + - Поля `_Converter` и `_BackConverter` инициализируются через переданные делегаты, но `BackConverter` может быть `null` — обработка через `throw new NotSupportedException()` корректна + - В `Convert`/`ConvertBack` выполняется приведение `(TValue)v` / `(TResult)v` без проверки типа — может бросить `InvalidCastException` для некорректных входных значений +- Рекомендации: + - Добавить проверки типов перед приведением: `if (v is TValue value) ...` и возвращать `Binding.DoNothing`/`null` по соглашению + - Добавить XML‑документацию и тесты + +## MathCore.WPF/Converters/LambdaConverter.cs +- Комментарии: отсутствуют — требуется XML‑документация +- Логические ошибки / потенциальные проблемы: + - Primary constructor синтаксис `LambdaConverter(LambdaConverter.Converter To, ...)` — проверить совместимость с TFM + - В `ConvertBack` при отсутствии `From` бросается `NotSupportedException` с сообщением на русском — это согласуется с проектными правилами по локализованным сообщениям +- Рекомендации: + - Добавить XML‑документацию и тесты + +--- + +# Проверка следующих файлов (продолжение) + +Ниже отчёт по следующим трём файлам: `Mapper.cs`, `MaxValue.cs`. + +## MathCore.WPF/Converters/Mapper.cs +- Комментарии: отсутствуют — требуется XML‑документация для класса и всех публичных свойств +- Логические ошибки / потенциальные проблемы: + - В сеттерах свойств `_k` пересчитывается как `(_MaxScale - _MinScale) / (_MaxValue - _MinValue)` без защиты от деления на ноль (когда `_MaxValue == _MinValue`) + - При изменении любого из четырёх свойств пересчёт `_k` происходит корректно, но начальные значения приводят к делению на ноль если `_MaxValue == _MinValue` + - Нет валидации входных значений (например, Min > Max) +- Рекомендации: + - Добавить защиту при пересчёте `_k`: если `_MaxValue == _MinValue` тогда `_k = 0` или бросать `ArgumentException` + - Добавить XML‑документацию и тесты; документировать поведение при вырожденном диапазоне + +## MathCore.WPF/Converters/MaxValue.cs +- Комментарии: отсутствуют — требуется XML‑документация для класса +- Логические ошибки / потенциальные проблемы: + - Метод `Convert(object[]? vv, ...)` возвращает `null` при пустом массиве; вероятно более согласованно возвращать `Binding.DoNothing` или `null` в зависимости от проекта + - Используется `vv.Max()` без `Cast`/`Comparer`, что потребует компаратор или элементы должны быть сравнимы; для смешанных типов это вызовет исключение + - Дuplicated `ConvertBack` methods are declared twice (for IMultiValueConverter and IValueConverter) — both throw `NotSupportedException`, this is correct +- Рекомендации: + - Добавить документацию и тесты; задокументировать ожидаемые типы элементов входного массива + - Рассмотреть использование `vv.Cast().Max()` с проверками или возвращение `Binding.DoNothing` при несравнимых типах + +--- + +# Проверка следующих файлов (продолжение) + +Ниже отчёт по следующим трём файлам: `MinValue.cs`, `Mod.cs`, `Multiply.cs`. + +## MathCore.WPF/Converters/MinValue.cs +- Комментарии: отсутствуют — требует XML‑документация для класса +- Логические ошибки / потенциальные проблемы: + - `Convert(object[]? vv, ...)` возвращает `vv?.Min()` — это может привести to исключению при несравнимых типах; необходимо документировать ожидаемые типы элементов + - Возвращаемое значение для `null`/пустого ввода — `null` — нужно задокументировать +- Рекомендации: + - Добавить XML‑документацию и тесты; рассмотреть валидацию типов перед вызовом `Min()` + +## MathCore.WPF/Converters/Mod.cs +- Комментарии: отсутствуют — требуется XML‑документация +- Логические ошибки / потенциальные проблемы: + - Используется primary constructor синтаксис `Mod(double M)`; проверить совместимость с TFM + - В `Convert` используются `IsNaN()` расширения — проверить наличие расширения или заменить на `double.IsNaN(...)` + - Поведение при `M == NaN` возвращает `p ?? v` — возможно неожиданно; документировать + - Деление по модулю `(p ?? v) % M` разыменовывается при `M == 0` — оператор `%` с нулём приведёт к `DivideByZeroException`? В C# `%` с double и 0 возвращает NaN или Infinity? Нужна проверка и документация +- Рекомендации: + - Заменить проверки `IsNaN()` на `double.IsNaN` при отсутствии расширения + - Добавить защиту/документацию для `M == 0` + - Добавить XML‑документацию и тесты + +## MathCore.WPF/Converters/Multiply.cs +- Комментарии: класс имеет `` — соответствует требованиям +- Логические ошибки: не обнаружено; класс корректно использует `SimpleDoubleValueConverter` +- Рекомендации: + - Добавить тесты для поведения при K==0 и NaN + +--- + +# Проверка следующих файлов (продолжение) + +Ниже отчёт по следующим трём файлам: `MultiplyMany.cs`, `MultiValuesToCompositeCollection.cs`, `MultiValuesToEnumerable.cs`. + +## MathCore.WPF/Converters/MultiplyMany.cs +- Комментарии: отсутствуют — требуется XML‑документация +- Логические ошибки / потенциальные проблемы: + - Поведение при `vv == null` возвращает `null`, при `vv == [null]` возвращает `double.NaN` — несогласованность, следует унифицировать + - Используется `DoubleValueConverter.TryConvertToDouble` — это правильно + - Возврат `double.NaN` для некорректных элементов документировать +- Рекомендации: + - Уточнить стратегию возврата при `null` и `null` элементах + - Добавить XML‑документацию и тесты + +## MathCore.WPF/Converters/MultiValuesToCompositeCollection.cs +- Комментарии: отсутствуют — требуется XML‑документация +- Логические ошибки: не обнаружено; корректно преобразует последовательности в `CompositeCollection` +- Рекомендации: + - Добавить XML‑документацию и тесты + +## MathCore.WPF/Converters/MultiValuesToEnumerable.cs +- Комментарии: отсутствуют — требуется XML‑документация +- Логические ошибки / потенциальные проблемы: + - В `ConvertBack` используется `tt!` и `ToArray()!` — потенциальные NRE; следует предварительно проверять `tt` и `v` + - `Zip(tt!, System.Convert.ChangeType)` ожидает `tt` длину соответствующую `IEnumerable` — возможны ошибки при несовпадении длин + - Поведение `Convert` возвращает `vv` как есть — документировать ожидания +- Рекомендации: + - Добавить проверки `tt` и `v` в `ConvertBack` и возвращать `null`/`Binding.DoNothing` при несовпадении + - Добавить XML‑документацию и тесты + +--- + +# Проверка следующих файлов (продолжение) + +Ниже отчёт по следующим трём файлам: `NaNtoVisibility.cs` (файл назван NANtoVisibility.cs), `Null2Visibility.cs`, `Not.cs`. + +## MathCore.WPF/Converters/NaNtoVisibility.cs +- Комментарии: отсутствуют — требуется XML‑документация для класса и свойств `Inverted`, `Collapsed` +- Логические ошибки / потенциальные проблемы: + - Атрибут `MarkupExtensionReturnType(typeof(NaNtoVisibility))` соответствует имени класса, но имя файла начинается с `NANtoVisibility.cs` — привести к единому стилю + - `Convert` приводит `v` к `(double)v` без проверки типа — может бросить `InvalidCastException` если `v` не `double` + - Возвращает `null` для `v == null` — лучше возвращать `DependencyProperty.UnsetValue` или `Binding.DoNothing` по соглашению +- Рекомендации: + - Добавить проверку типов: `if (v is double d) ...` или использовать `DoubleValueConverter.TryConvertToDouble` + - Добавить XML‑документацию и тесты + +## MathCore.WPF/Converters/Null2Visibility.cs +- Комментарии: отсутствуют — требуется XML‑документация +- Логические ошибки: не обнаружено; логика корректно возвращает `Visibility` в зависимости от `Inverted` и `Collapsed` +- Рекомендации: + - Задокументировать поведение и добавить тесты + +## MathCore.WPF/Converters/Not.cs +- Комментарии: отсутствуют — требуется XML‑документация +- Логические ошибки / потенциальные проблемы: + - Текущее выражение `!(bool?) v` может дать непредсказуемое поведение при `v == null` или при `v` не являющемся `bool` — лучше явно проверять: `v is bool b ? !b : Binding.DoNothing` или возвращать `null` + - `ConvertBack` также использует `!(bool?)v` — аналогично +- Рекомендации: + - Исправить обработку `null`/некорректных типов + - Добавить XML‑документацию и тесты + +--- + +# Проверка следующих файлов (продолжение) + +Ниже отчёт по следующим трём файлам: `OutRange.cs`, `Or.cs`, `Points2PathGeometry.cs`. + +## MathCore.WPF/Converters/OutRange.cs +- Комментарии: отсутствуют — требуется XML‑документация для класса и свойств +- Логические ошибки / потенциальные проблемы: + - Используется `interval` из primary constructor `OutRange(Interval interval)` — проверить совместимость синтаксиса и наличие типа `Interval` + - В `Convert` используется `v.IsNaN()` — заменить на `double.IsNaN(v)` если расширение отсутствует + - Логика `IncludeLimits` устанавливает оба флага, но не обрабатывает `null` явно — текущая реализация корректна +- Рекомендации: + - Добавить защиту на случай отсутствия `interval` и документацию + - Заменить `IsNaN()` на `double.IsNaN` при необходимости + +## MathCore.WPF/Converters/Or.cs +- Комментарии: отсутствуют — требуется XML‑документация +- Логические ошибки / потенциальные проблемы: + - Используется `vv?.Cast().Any(v => v) ?? NullDefaultValue` — приведёт to `InvalidCastException` при наличии `null` или несоответствующих типов. Лучше безопасно перебирать и проверять `is bool b` +- Рекомендации: + - Заменить `Cast()` на безопасную проверку и добавить тесты + - Добавить XML‑документацию + +## MathCore.WPF/Converters/Points2PathGeometry.cs +- Комментарии: отсутствуют — требуется XML‑документация +- Логические ошибки / потенциальные проблемы: + - Используется pattern matching `Point[] and [var start, .. { Length: > 0 } tail]` и `new PathGeometry { Figures = { new(start, tail.Select(...), false) } }` — компактно, но проверить совместимость TFM + - Возвращает `null` для неподдерживаемых типов — задокументировать +- Рекомендации: + - Добавить XML‑документацию и тесты + +--- + +# Проверка следующих файлов (продолжение) + +Ниже отчёт по следующим трём файлам: `Range.cs` и набору файлов `Reflection/*` + +## MathCore.WPF/Converters/Range.cs +- Комментарии: отсутствуют — требуется XML‑документация для класса и публичных свойств +- Логические ошибки / потенциальные проблемы: + - Используется primary constructor `Range(Interval interval)`; проверить совместимость синтаксиса и наличие типа `Interval` + - Свойства `Min`/`Max` используют `ConstructorArgument` и обращаются к `interval` — проверить порядок аргументов + - `Convert` возвращает `interval.Normalize(v)` — предположительно корректно, но проверить реализацию `Normalize` на NaN/Infinity +- Рекомендации: + - Добавить XML‑документацию и тесты + - Проверить `Interval` API для корректной работы со значениями NaN/Infinity + +## MathCore.WPF/Converters/Reflection/* +- Файлы: `AssemblyCompany.cs`, `AssemblyConfiguration.cs`, `AssemblyConverter.cs`, `AssemblyCopyright.cs`, + `AssemblyDescription.cs`, `AssemblyFileVersion.cs`, `AssemblyProduct.cs`, `AssemblyTime.cs`, `AssemblyTitle.cs`, + `AssemblyTrademark.cs`, `AssemblyVersion.cs`, `GetTypeAssembly.cs` +- Комментарии: отсутствуют XML‑комментарии в большинстве файлов — требуется добавить для публичных типов +- Логические ошибки / потенциальные проблемы: + - `AssemblyConverter` использует `Converter((Assembly)v)` в `Convert` без проверки типа — привести к безопасной обработке `v is Assembly asm ? Converter(asm) : null` + - В некоторых файлах атрибут `MarkupExtensionReturnType` указывает неверный тип (например, `AssemblyConfiguration` файл использует `typeof(AssemblyCompany)`), проверить и исправить + - Используется primary constructor синтаксис для классов-наследников `AssemblyConverter(...)` — проверить совместимость синтаксиса + - `AssemblyTime` использует `a.Location` и `FileInfo` — на некоторых платформах `Assembly.Location` может быть пустой строкой для dynamic assemblies; документировать +- Рекомендации: + - Добавить XML‑документацию и тесты + - Исправить некорректные `MarkupExtensionReturnType` там, где они не соответствуют имени класса + - Заменить небезопасные приведения на безопасные проверки типов + +--- + +Осталось 0 файл(ов) для обработки. From 54da5db32a2b578d1febcb3004eafd9ddb7fad5a Mon Sep 17 00:00:00 2001 From: Infarh Date: Sun, 14 Dec 2025 17:39:48 +0300 Subject: [PATCH 24/56] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20XML-=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D0=B0=D1=80=D0=B8=D0=B5=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MathCore.WPF/Converters/Abs.cs | 5 ++- MathCore.WPF/Converters/Addition.cs | 3 ++ MathCore.WPF/Converters/AdditionMulti.cs | 7 ++- MathCore.WPF/Converters/AggregateArray.cs | 5 ++- MathCore.WPF/Converters/And.cs | 18 +++++++- MathCore.WPF/Converters/Arithmetic.cs | 30 ++++++++++--- MathCore.WPF/Converters/ArrayElement.cs | 44 ++++++++++++++----- .../Converters/ArrayToStringConverter.cs | 20 +++++---- MathCore.WPF/Converters/ArraysToPoints.cs | 3 ++ MathCore.WPF/Converters/AsyncConverter.cs | 20 +++++++++ MathCore.WPF/Converters/AverageMulti.cs | 6 ++- MathCore.WPF/Converters/Base/DoubleToBool.cs | 31 +++++++------ .../Base/MultiDoubleValueValueConverter.cs | 8 +++- .../Base/MultiValueValueConverter.cs | 6 +++ .../Base/SimpleDoubleValueConverter.cs | 7 ++- MathCore.WPF/Converters/Bool2Visibility.cs | 11 +++-- .../Converters/BoolToBrushConverter.cs | 6 +++ MathCore.WPF/Converters/CSplineInterp.cs | 31 ++++++++++--- .../Converters/ColorBrushConverter.cs | 3 ++ MathCore.WPF/Converters/Combine.cs | 6 +-- MathCore.WPF/Converters/CombineMulti.cs | 21 +++++++-- MathCore.WPF/Converters/Composite.cs | 10 +++-- .../Converters/ConvertersToRefactoring2.md | 14 ++++++ MathCore.WPF/Converters/Cos.cs | 6 ++- MathCore.WPF/Converters/Ctg.cs | 6 ++- MathCore.WPF/Converters/Custom.cs | 20 ++++++--- MathCore.WPF/Converters/CustomMulti.cs | 20 ++++++--- MathCore.WPF/Converters/DataLengthString.cs | 9 ++-- MathCore.WPF/Converters/DefaultIfNaN.cs | 14 +++++- MathCore.WPF/Converters/Deviation.cs | 3 +- MathCore.WPF/Converters/Divide.cs | 1 + MathCore.WPF/Converters/DivideMulti.cs | 3 ++ MathCore.WPF/Converters/ExConverter.cs | 8 +++- MathCore.WPF/Converters/ExpConverter.cs | 12 +++-- .../Converters/FirstLastItemConverter.cs | 3 ++ MathCore.WPF/Converters/GetType.cs | 4 +- .../Converters/GreaterOrEqualThanMulti.cs | 14 ++++++ MathCore.WPF/Converters/GreaterThan.cs | 7 ++- MathCore.WPF/Converters/GreaterThanMulti.cs | 11 +++++ MathCore.WPF/Converters/GreaterThanOrEqual.cs | 5 ++- MathCore.WPF/Converters/IO/FilePathToName.cs | 5 ++- MathCore.WPF/Converters/InIntervalValue.cs | 9 +++- MathCore.WPF/Converters/InRange.cs | 5 ++- MathCore.WPF/Converters/Interpolation.cs | 5 ++- MathCore.WPF/Converters/Inverse.cs | 10 ++++- MathCore.WPF/Converters/IsNaN.cs | 5 ++- MathCore.WPF/Converters/IsNegative.cs | 5 ++- MathCore.WPF/Converters/IsNull.cs | 2 + MathCore.WPF/Converters/IsPositive.cs | 5 ++- .../Converters/JoinStringConverter.cs | 16 +++++-- MathCore.WPF/Converters/Lambda.cs | 26 +++++++---- MathCore.WPF/Converters/LambdaConverter.cs | 6 +++ .../Converters/LessOrEqualThanMulti.cs | 11 +++++ MathCore.WPF/Converters/LessThan.cs | 5 ++- MathCore.WPF/Converters/LessThanMulti.cs | 11 +++++ MathCore.WPF/Converters/LessThanOrEqual.cs | 5 ++- MathCore.WPF/Converters/Linear.cs | 3 +- MathCore.WPF/Converters/Mapper.cs | 21 ++++++--- MathCore.WPF/Converters/MaxValue.cs | 34 +++++++++++++- MathCore.WPF/Converters/MinValue.cs | 30 ++++++++++++- MathCore.WPF/Converters/Mod.cs | 17 ++++--- .../MultiValuesToCompositeCollection.cs | 2 + .../Converters/MultiValuesToEnumerable.cs | 24 +++++++++- MathCore.WPF/Converters/Multiply.cs | 1 + MathCore.WPF/Converters/MultiplyMany.cs | 10 +++++ MathCore.WPF/Converters/NANtoVisibility.cs | 24 +++++++--- MathCore.WPF/Converters/Not.cs | 19 ++++++-- MathCore.WPF/Converters/Or.cs | 19 +++++++- MathCore.WPF/Converters/OutRange.cs | 5 ++- .../Converters/Points2PathGeometry.cs | 9 ++-- MathCore.WPF/Converters/Range.cs | 4 +- .../Converters/Reflection/AssemblyCompany.cs | 8 +++- .../Reflection/AssemblyConfiguration.cs | 10 +++-- .../Reflection/AssemblyConverter.cs | 9 ++-- .../Reflection/AssemblyCopyright.cs | 10 +++-- .../Reflection/AssemblyDescription.cs | 8 +++- .../Reflection/AssemblyFileVersion.cs | 8 +++- .../Converters/Reflection/AssemblyProduct.cs | 8 +++- .../Converters/Reflection/AssemblyTime.cs | 13 +++++- .../Converters/Reflection/AssemblyTitle.cs | 8 +++- .../Reflection/AssemblyTrademark.cs | 8 +++- .../Converters/Reflection/AssemblyVersion.cs | 8 +++- .../Converters/Reflection/GetTypeAssembly.cs | 5 ++- MathCore.WPF/Converters/Round.cs | 21 ++++++--- MathCore.WPF/Converters/RoundAdaptive.cs | 20 +++++++-- MathCore.WPF/Converters/SecondsToTimeSpan.cs | 9 +++- MathCore.WPF/Converters/Sign.cs | 6 ++- MathCore.WPF/Converters/SignValue.cs | 27 +++++++++--- MathCore.WPF/Converters/Sin.cs | 6 ++- MathCore.WPF/Converters/dB.cs | 13 +++--- 90 files changed, 800 insertions(+), 219 deletions(-) diff --git a/MathCore.WPF/Converters/Abs.cs b/MathCore.WPF/Converters/Abs.cs index 459dc9c..648fcd0 100644 --- a/MathCore.WPF/Converters/Abs.cs +++ b/MathCore.WPF/Converters/Abs.cs @@ -7,13 +7,14 @@ namespace MathCore.WPF.Converters; +/// Возвращает абсолютное значение числа [ValueConversion(typeof(double), typeof(double))] [MarkupExtensionReturnType(typeof(Abs))] public class Abs : DoubleValueConverter { - /// + /// Возвращает абсолютное значение входного числа protected override double Convert(double v, double? p = null) => Math.Abs(v); - /// + /// Обратное преобразование возвращает значение как есть protected override double ConvertBack(double v, double? p = null) => v; } \ No newline at end of file diff --git a/MathCore.WPF/Converters/Addition.cs b/MathCore.WPF/Converters/Addition.cs index f90d742..8461df2 100644 --- a/MathCore.WPF/Converters/Addition.cs +++ b/MathCore.WPF/Converters/Addition.cs @@ -1,4 +1,5 @@ using System.Windows.Markup; +using System.Windows.Data; using MathCore.WPF.Converters.Base; @@ -7,6 +8,8 @@ namespace MathCore.WPF.Converters; /// Преобразователь сложения значения с вещественным числом +/// Добавочное значение, которое прибавляется к входному значению +[ValueConversion(typeof(double), typeof(double))] [MarkupExtensionReturnType(typeof(Addition))] public class Addition(double P) : SimpleDoubleValueConverter(P, (v, p) => v + p, (r, p) => r - p) { diff --git a/MathCore.WPF/Converters/AdditionMulti.cs b/MathCore.WPF/Converters/AdditionMulti.cs index 6810e0c..819a478 100644 --- a/MathCore.WPF/Converters/AdditionMulti.cs +++ b/MathCore.WPF/Converters/AdditionMulti.cs @@ -1,5 +1,6 @@ using System.Globalization; using System.Windows.Markup; +using System.Windows.Data; using MathCore.WPF.Converters.Base; @@ -7,9 +8,12 @@ namespace MathCore.WPF.Converters; +/// Суммирует последовательность числовых значений [MarkupExtensionReturnType(typeof(AdditionMulti))] public class AdditionMulti : MultiValueValueConverter { + /// Преобразует массив значений, суммируя их последовательно + /// Сумма значений; null если вход null; double.NaN если один из элементов не может быть преобразован protected override object? Convert(object?[]? vv, Type? t, object? p, CultureInfo? c) { switch (vv) @@ -20,7 +24,8 @@ public class AdditionMulti : MultiValueValueConverter return double.NaN; } - var v = vv[0] is double d ? d : System.Convert.ToDouble(vv[0]); + if (!DoubleValueConverter.TryConvertToDouble(vv[0], c, out var v)) + return double.NaN; for (var i = 1; i < vv.Length; i++) { diff --git a/MathCore.WPF/Converters/AggregateArray.cs b/MathCore.WPF/Converters/AggregateArray.cs index b42e40a..b44d2ad 100644 --- a/MathCore.WPF/Converters/AggregateArray.cs +++ b/MathCore.WPF/Converters/AggregateArray.cs @@ -1,5 +1,6 @@ using System.Collections; using System.Globalization; +using System.Linq; using System.Windows.Markup; using MathCore.WPF.Converters.Base; @@ -8,12 +9,14 @@ namespace MathCore.WPF.Converters; +/// Разворачивает вложенные перечисления в одну последовательность [MarkupExtensionReturnType(typeof(AggregateArray))] public class AggregateArray : MultiValueValueConverter { - /// + /// Преобразует массив значений в одну плоскую последовательность, разворачивая вложенныеenumerations protected override object? Convert(object[]? vv, Type? t, object? p, CultureInfo? c) => vv?.SelectMany(GetItems); + /// Возвращает элементы, если вход — перечисление, иначе возвращает сам элемент; пропускает null private static IEnumerable GetItems(object? Item) { switch (Item) diff --git a/MathCore.WPF/Converters/And.cs b/MathCore.WPF/Converters/And.cs index 2ea4f5e..6aa0aba 100644 --- a/MathCore.WPF/Converters/And.cs +++ b/MathCore.WPF/Converters/And.cs @@ -1,4 +1,5 @@ using System.Globalization; +using System.Windows.Data; using System.Windows.Markup; using MathCore.WPF.Converters.Base; @@ -8,11 +9,24 @@ namespace MathCore.WPF.Converters; +/// Проверяет, что все элементы входного массива являются true [MarkupExtensionReturnType(typeof(And))] public class And : MultiValueValueConverter { + /// Возвращаемое значение при null входе public bool NullDefaultValue { get; set; } - /// - protected override object Convert(object[]? vv, Type? t, object? p, CultureInfo? c) => vv?.Cast().All(v => v) ?? NullDefaultValue; + /// Возвращает true если все элементы истинны, иначе false; при наличии несоответствующих типов возвращает Binding.DoNothing + protected override object Convert(object[]? vv, Type? t, object? p, CultureInfo? c) + { + if (vv is null) return NullDefaultValue; + + foreach (var item in vv) + { + if (item is not bool b) return Binding.DoNothing; + if (!b) return false; + } + + return true; + } } \ No newline at end of file diff --git a/MathCore.WPF/Converters/Arithmetic.cs b/MathCore.WPF/Converters/Arithmetic.cs index 1ff9d00..112aa3f 100644 --- a/MathCore.WPF/Converters/Arithmetic.cs +++ b/MathCore.WPF/Converters/Arithmetic.cs @@ -1,6 +1,7 @@ using System.Globalization; using System.Text.RegularExpressions; using System.Windows.Markup; +using System.Windows.Data; using MathCore.WPF.Converters.Base; @@ -8,6 +9,8 @@ namespace MathCore.WPF.Converters; +/// Выполняет простое арифметическое преобразование значения по строковому шаблону +/// Шаблон параметра должен быть в формате "{op}{value}", например "+2.5" [MarkupExtensionReturnType(typeof(Arithmetic))] #if NET7_0_OR_GREATER public partial class Arithmetic : ValueConverter @@ -24,16 +27,22 @@ public class Arithmetic : ValueConverter private readonly Regex _Pattern = new(__ArithmeticParseExpression, RegexOptions.Compiled); #endif + /// Преобразует входное значение с использованием операции, заданной в параметре + /// Входное значение + /// Тип целевого значения + /// Параметр операции, например "+2.5" + /// Культура для парсинга чисел + /// Результат арифметической операции или Binding.DoNothing при некорректных входных данных protected override object? Convert(object? v, Type t, object? p, CultureInfo c) { - if (v is not double value || p is not string { Length: > 0 } p_str) return null; + if (v is not double value || p is not string { Length: > 0 } p_str) return Binding.DoNothing; var pattern = _Pattern.Match(p_str); - if (pattern.Groups.Count != 3) return null; + if (pattern.Groups.Count != 3) return Binding.DoNothing; var op = pattern.Groups[1].Value.Trim(); p_str = pattern.Groups[2].Value; - if (!double.TryParse(p_str, out var p_value)) return null; + if (!double.TryParse(p_str, NumberStyles.Float | NumberStyles.AllowThousands, c, out var p_value)) return Binding.DoNothing; return op switch { @@ -45,17 +54,24 @@ public class Arithmetic : ValueConverter }; } + /// Обратное преобразование для арифметической операции + /// Входное значение + /// Тип целевого значения + /// Параметр операции + /// Культура для парсинга чисел + /// Обратный результат или Binding.DoNothing при некорректных входных данных protected override object? ConvertBack(object? v, Type? t, object? p, CultureInfo? c) { - if (v is not double d || p is not string { Length: > 0 } p_str) return null; + if (v is not double d || p is not string { Length: > 0 } p_str) return Binding.DoNothing; var pattern = _Pattern.Match(p_str); - if (pattern.Groups.Count != 3) return null; + if (pattern.Groups.Count != 3) return Binding.DoNothing; var op = pattern.Groups[1].Value.Trim(); p_str = pattern.Groups[2].Value; - return !double.TryParse(p_str, out var p_value) - ? null + var culture = c ?? CultureInfo.InvariantCulture; + return !double.TryParse(p_str, NumberStyles.Float | NumberStyles.AllowThousands, culture, out var p_value) + ? Binding.DoNothing : op switch { "+" => (d - p_value), diff --git a/MathCore.WPF/Converters/ArrayElement.cs b/MathCore.WPF/Converters/ArrayElement.cs index 0395c1c..108534f 100644 --- a/MathCore.WPF/Converters/ArrayElement.cs +++ b/MathCore.WPF/Converters/ArrayElement.cs @@ -1,5 +1,6 @@ using System.Collections; using System.Globalization; +using System.Linq; using System.Windows.Data; using System.Windows.Markup; @@ -10,23 +11,46 @@ namespace MathCore.WPF.Converters; +/// Возвращает элемент коллекции по указанному индексу [MarkupExtensionReturnType(typeof(ArrayElement))] [ValueConversion(typeof(IEnumerable), typeof(object))] public class ArrayElement(int Index) : ValueConverter { public ArrayElement() : this(0) { } + /// Индекс элемента для возврата public int Index { get; set; } = Index; - /// - protected override object? Convert(object? v, Type? t, object? p, CultureInfo? c) => (v, p) switch + /// Пытается разрешить индекс из параметра: поддерживает int, строку и числовые типы + private static bool TryResolveIndex(object? p, int defaultIndex, out int index) { - (Array array, int index) => index < array.Length ? array.GetValue(index) : default, - (Array array, _) => Index < array.Length ? array.GetValue(Index) : default, - (IList list, int index) => index < list.Count ? list[index] : default, - (IList list, _) => Index < list.Count ? list[Index] : default, - (IEnumerable items, int index) => items.Cast().ElementAtOrDefault(index), - (IEnumerable items, _) => items.Cast().ElementAtOrDefault(Index), - _ => Binding.DoNothing - }; + index = defaultIndex; + if (p is null) return true; + if (p is int i) { index = i; return true; } + if (p is string s && int.TryParse(s, out i)) { index = i; return true; } + try + { + index = System.Convert.ToInt32(p); + return true; + } + catch + { + return false; + } + } + + /// Возвращает элемент коллекции по индексу (поддерживает Array, IList и IEnumerable) + protected override object? Convert(object? v, Type? t, object? p, CultureInfo? c) + { + if (!TryResolveIndex(p, Index, out var idx)) return Binding.DoNothing; + if (idx < 0) return Binding.DoNothing; + + return v switch + { + Array array when idx < array.Length => array.GetValue(idx), + IList list when idx < list.Count => list[idx], + IEnumerable items => items.Cast().ElementAtOrDefault(idx), + _ => Binding.DoNothing + }; + } } \ No newline at end of file diff --git a/MathCore.WPF/Converters/ArrayToStringConverter.cs b/MathCore.WPF/Converters/ArrayToStringConverter.cs index def6397..24e9701 100644 --- a/MathCore.WPF/Converters/ArrayToStringConverter.cs +++ b/MathCore.WPF/Converters/ArrayToStringConverter.cs @@ -1,5 +1,5 @@ using System.Globalization; -using System.Text; +using System.Linq; using System.Windows.Data; using System.Windows.Markup; @@ -7,21 +7,23 @@ namespace MathCore.WPF.Converters; +/// Преобразует массив в строку [MarkupExtensionReturnType(typeof(ArrayToStringConverter))] public class ArrayToStringConverter : ValueConverter { + /// Преобразует массив в строку, элементы разделены запятой + /// Массив для преобразования + /// Тип целевого значения + /// Параметр преобразования (не используется) + /// Культура + /// Строковое представление массива или Binding.DoNothing для неподдерживаемых входов + /// Использует запятую в качестве разделителя protected override object? Convert(object? v, Type t, object? p, CultureInfo c) { if (v is not Array array) return Binding.DoNothing; - var result = new StringBuilder(); - for (var i = 0; i < array.Length; i++) - result.Append(array.GetValue(i)).Append(','); - - if (result.Length > 0) - result.Length--; - - return result.ToString(); + var items = array.Cast().Select(x => x?.ToString() ?? string.Empty).ToArray(); + return string.Join(",", items); } } diff --git a/MathCore.WPF/Converters/ArraysToPoints.cs b/MathCore.WPF/Converters/ArraysToPoints.cs index 594b082..e69f900 100644 --- a/MathCore.WPF/Converters/ArraysToPoints.cs +++ b/MathCore.WPF/Converters/ArraysToPoints.cs @@ -3,6 +3,7 @@ using System.Windows; using System.Windows.Markup; using System.Windows.Media; +using System.Linq; using MathCore.WPF.Converters.Base; @@ -10,9 +11,11 @@ namespace MathCore.WPF.Converters; +/// Преобразует две последовательности координат X и Y в коллекцию точек [MarkupExtensionReturnType(typeof(ArraysToPoints))] public class ArraysToPoints : MultiValueValueConverter { + /// Ожидает два перечисления одинаковой длины; выбрасывает ArgumentException при несоответствующих входах protected override object Convert(object[]? vv, Type? t, object? p, CultureInfo? c) { if (vv is not [IEnumerable e1, IEnumerable e2]) diff --git a/MathCore.WPF/Converters/AsyncConverter.cs b/MathCore.WPF/Converters/AsyncConverter.cs index e90ec86..1160153 100644 --- a/MathCore.WPF/Converters/AsyncConverter.cs +++ b/MathCore.WPF/Converters/AsyncConverter.cs @@ -2,15 +2,21 @@ using System.Globalization; using System.Windows.Data; using System.Windows.Markup; +using System.Threading; +using System.Threading.Tasks; +using System; namespace MathCore.WPF.Converters; +/// Преобразует задачу в наблюдаемый объект для привязки результатов асинхронных операций [ValueConversion(typeof(Task), typeof(AsyncConverterTaskCompletionNotifier))] [MarkupExtensionReturnType(typeof(AsyncConverter))] public class AsyncConverter : MarkupExtension, IValueConverter { + /// Плейсхолдер возвращаемого значения пока задача выполняется public object? RunningPlaceholder { get; set; } + /// Преобразует Task{T} в объект-обёртку с уведомлениями об изменениях public object? Convert(object? v, Type t, object? p, CultureInfo c) => v switch { Task task => new AsyncConverterTaskCompletionNotifier(task, RunningPlaceholder), @@ -18,17 +24,22 @@ public class AsyncConverter : MarkupExtension, IValueConverter _ => null }; + /// Обратное преобразование не поддерживается public object? ConvertBack(object? v, Type t, object? p, CultureInfo c) => throw new NotSupportedException(); + /// public override object ProvideValue(IServiceProvider sp) => this; } +/// Обёртка для наблюдения за завершением Task{T} и оповещения UI public sealed class AsyncConverterTaskCompletionNotifier : INotifyPropertyChanged { private readonly object? _RunningPlaceholder; + /// Исходная задача public Task Task { get; } + /// Результат задачи или плейсхолдер, если задача ещё не завершена public object? Result { get @@ -47,22 +58,31 @@ public object? Result } } + /// Статус задачи public TaskStatus Status => Task.Status; + /// Возвращает true если задача завершена public bool IsCompleted => Task.IsCompleted; + /// Возвращает true если задача успешно завершена public bool IsSuccessfullyCompleted => Task.Status == TaskStatus.RanToCompletion; + /// Возвращает true если задача отменена public bool IsCanceled => Task.IsCanceled; + /// Возвращает true если задача завершилась с ошибкой public bool IsFaulted => Task.IsFaulted; + /// Исключение, связанное с задачей public AggregateException? Exception => Task.Exception; + /// Внутреннее исключение, если есть public Exception? InnerException => Exception?.InnerException; + /// Текст ошибки, если есть public string? ErrorMessage => InnerException?.Message; + /// Создаёт обёртку для наблюдения за задачей public AsyncConverterTaskCompletionNotifier(Task task, object? RunningPlaceholder) { _RunningPlaceholder = RunningPlaceholder; diff --git a/MathCore.WPF/Converters/AverageMulti.cs b/MathCore.WPF/Converters/AverageMulti.cs index 0ac9b75..c2052a1 100644 --- a/MathCore.WPF/Converters/AverageMulti.cs +++ b/MathCore.WPF/Converters/AverageMulti.cs @@ -8,9 +8,12 @@ namespace MathCore.WPF.Converters; +/// Вычисляет среднее значение последовательности чисел [MarkupExtensionReturnType(typeof(AverageMulti))] public class AverageMulti() : MultiValueValueConverter { + /// Вычисляет среднее значение последовательности чисел + /// Среднее арифметическое элементов массива; Binding.DoNothing для некорректных входных данных protected override object? Convert(object?[]? vv, Type? t, object? p, CultureInfo? c) { switch (vv) @@ -21,7 +24,8 @@ public class AverageMulti() : MultiValueValueConverter return double.NaN; } - var v = vv[0] is double d ? d : System.Convert.ToDouble(vv[0]); + if (!DoubleValueConverter.TryConvertToDouble(vv[0], c, out var v)) + return double.NaN; for (var i = 1; i < vv.Length; i++) { diff --git a/MathCore.WPF/Converters/Base/DoubleToBool.cs b/MathCore.WPF/Converters/Base/DoubleToBool.cs index 0a553f0..3e907c2 100644 --- a/MathCore.WPF/Converters/Base/DoubleToBool.cs +++ b/MathCore.WPF/Converters/Base/DoubleToBool.cs @@ -5,6 +5,7 @@ namespace MathCore.WPF.Converters.Base; +/// Базовый класс для конвертеров double -> bool [ValueConversion(typeof(double?), typeof(bool?))] public abstract class DoubleToBool : ValueConverter { @@ -18,21 +19,25 @@ protected DoubleToBool(Func? to = null, Func? from _ConvertBack = from ?? ConvertBack; } + /// Преобразует число в логическое значение protected virtual bool? Convert(double v) => throw new NotImplementedException("Не определён метод прямого преобразования величины"); + /// Обратное преобразование логического значения в число protected virtual double ConvertBack(bool? v) => throw new NotSupportedException("Обратное преобразование не поддерживается"); - /// - protected override object? Convert(object? v, Type? t, object? p, CultureInfo? c) => - DoubleValueConverter.TryConvertToDouble(p, c, out var P) - ? _Convert(P) - : v is null - ? null - : DoubleValueConverter.TryConvertToDouble(v, c, out var V) ? _Convert(V) : V; - - /// - protected override object? ConvertBack(object? v, Type? t, object? p, CultureInfo? c) => - v is null - ? null - : _ConvertBack((bool)v); + /// Проверяет параметр p на число и использует его, иначе использует входное значение + protected override object? Convert(object? v, Type? t, object? p, CultureInfo? c) + { + // параметр имеет приоритет если он числовой + if (DoubleValueConverter.TryConvertToDouble(p, c, out var P)) + return _Convert(P); + + if (v is null) return null; + + return DoubleValueConverter.TryConvertToDouble(v, c, out var V) ? _Convert(V) : Binding.DoNothing; + } + + /// Обратное преобразование с безопасной проверкой типов + protected override object? ConvertBack(object? v, Type? t, object? p, CultureInfo? c) => + v is null ? null : (v is bool b ? _ConvertBack(b) : Binding.DoNothing); } \ No newline at end of file diff --git a/MathCore.WPF/Converters/Base/MultiDoubleValueValueConverter.cs b/MathCore.WPF/Converters/Base/MultiDoubleValueValueConverter.cs index 7515831..1559e4c 100644 --- a/MathCore.WPF/Converters/Base/MultiDoubleValueValueConverter.cs +++ b/MathCore.WPF/Converters/Base/MultiDoubleValueValueConverter.cs @@ -2,10 +2,13 @@ namespace MathCore.WPF.Converters.Base; +/// Базовый конвертер для операций над массивом double с ограничением Min/Max public abstract class MultiDoubleValueValueConverter : MultiValueValueConverter { + /// Минимальное ограничение результата public double? Min { get; set; } + /// Максимальное ограничение результата public double? Max { get; set; } /// @@ -35,11 +38,14 @@ protected override object Convert(object[]? vv, Type? t, object? p, CultureInfo? ? null : ConvertBack(DoubleValueConverter.ConvertToDouble(v, c))?.Cast().ToArray(); + /// Выполняет вычисление результата по массиву double + /// Массив входных значений или null + /// Числовой результат вычисления protected abstract double Convert(double[]? vv); + /// Обратное преобразование значения в массив double, по умолчанию не реализовано protected virtual double[]? ConvertBack(double v) { - base.ConvertBack(null, null, null, null); return null; } } \ No newline at end of file diff --git a/MathCore.WPF/Converters/Base/MultiValueValueConverter.cs b/MathCore.WPF/Converters/Base/MultiValueValueConverter.cs index bdfbc31..313c807 100644 --- a/MathCore.WPF/Converters/Base/MultiValueValueConverter.cs +++ b/MathCore.WPF/Converters/Base/MultiValueValueConverter.cs @@ -17,6 +17,12 @@ public abstract class MultiValueValueConverter : MarkupExtension, IMultiValueCon /// Параметр преобразования /// Сведения о культуре /// Преобразованное значение + /// + /// + /// var converter = new MyMultiConverter(); + /// var result = converter.Convert(new object[] { 1, 2 }, typeof(object), null, CultureInfo.InvariantCulture); + /// + /// protected abstract object? Convert(object[]? vv, Type? t, object? p, CultureInfo? c); /// Обратное преобразование значения diff --git a/MathCore.WPF/Converters/Base/SimpleDoubleValueConverter.cs b/MathCore.WPF/Converters/Base/SimpleDoubleValueConverter.cs index 6428744..33a92cd 100644 --- a/MathCore.WPF/Converters/Base/SimpleDoubleValueConverter.cs +++ b/MathCore.WPF/Converters/Base/SimpleDoubleValueConverter.cs @@ -1,12 +1,11 @@ - -// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable MemberCanBePrivate.Global // ReSharper disable AutoPropertyCanBeMadeGetOnly.Global // ReSharper disable MemberCanBeProtected.Global // ReSharper disable VirtualMemberNeverOverridden.Global namespace MathCore.WPF.Converters.Base; -/// Простой математический конвертер для бинарных операций с константой (либо с параметром) +/// Простой математический конвертер для бинарных операций с константой или параметром public abstract class SimpleDoubleValueConverter : DoubleValueConverter { /// Метод преобразования значения @@ -21,7 +20,7 @@ public abstract class SimpleDoubleValueConverter : DoubleValueConverter /// Метод обратного преобразования private readonly Conversion _From; - /// Параметр преобразования + /// Параметр преобразования, используемый если параметр вызова отсутствует public double Parameter { get; set; } protected SimpleDoubleValueConverter(double Parameter, Conversion? to = null, Conversion? from = null) diff --git a/MathCore.WPF/Converters/Bool2Visibility.cs b/MathCore.WPF/Converters/Bool2Visibility.cs index df43d5e..1695013 100644 --- a/MathCore.WPF/Converters/Bool2Visibility.cs +++ b/MathCore.WPF/Converters/Bool2Visibility.cs @@ -11,16 +11,20 @@ namespace MathCore.WPF.Converters; +/// Преобразует булево значение в Visibility [ValueConversion(typeof(bool?), typeof(Visibility))] [MarkupExtensionReturnType(typeof(Bool2Visibility))] public class Bool2Visibility : ValueConverter { + /// Инвертировать результат преобразования public bool Inverted { get; set; } + /// Использовать Collapsed вместо Hidden public bool Collapsed { get; set; } private Visibility Hidden => Collapsed ? Visibility.Collapsed : Visibility.Hidden; + /// Преобразует булево значение в Visibility; возвращает null для null входа protected override object? Convert(object? v, Type? t, object? p, CultureInfo? c) => v switch { @@ -28,17 +32,18 @@ public class Bool2Visibility : ValueConverter Visibility => v, true => !Inverted ? Visibility.Visible : Hidden, false => Inverted ? Visibility.Visible : Hidden, - _ => throw new NotSupportedException() + _ => Binding.DoNothing }; + /// Обратное преобразование Visibility в bool protected override object? ConvertBack(object? v, Type? t, object? p, CultureInfo? c) => v switch { null => null, - bool => v, + bool b => b, Visibility.Visible => !Inverted, Visibility.Hidden => Inverted, Visibility.Collapsed => Inverted, - _ => throw new NotSupportedException() + _ => Binding.DoNothing }; } \ No newline at end of file diff --git a/MathCore.WPF/Converters/BoolToBrushConverter.cs b/MathCore.WPF/Converters/BoolToBrushConverter.cs index c2d9df0..24289f4 100644 --- a/MathCore.WPF/Converters/BoolToBrushConverter.cs +++ b/MathCore.WPF/Converters/BoolToBrushConverter.cs @@ -7,16 +7,21 @@ namespace MathCore.WPF.Converters; +/// Преобразует булево значение в Brush [MarkupExtensionReturnType(typeof(BoolToBrushConverter))] [ValueConversion(typeof(bool?), typeof(Brush))] public class BoolToBrushConverter : ValueConverter { + /// Brush для true public Brush TrueColorBrush { get; set; } = new SolidColorBrush(Colors.Green); + /// Brush для false public Brush FalseColorBrush { get; set; } = new SolidColorBrush(Colors.Orange); + /// Brush для null public Brush NullColorBrush { get; set; } = new SolidColorBrush(Colors.Transparent); + /// Преобразует булево значение в соответствующий Brush protected override object? Convert(object? v, Type? t, object? p, CultureInfo? c) => v switch { null => NullColorBrush, @@ -25,5 +30,6 @@ public class BoolToBrushConverter : ValueConverter _ => Binding.DoNothing }; + /// Обратное преобразование не реализовано и возвращает Binding.DoNothing protected override object? ConvertBack(object? v, Type? t, object? p, CultureInfo? c) => Binding.DoNothing; } \ No newline at end of file diff --git a/MathCore.WPF/Converters/CSplineInterp.cs b/MathCore.WPF/Converters/CSplineInterp.cs index bbfe67b..3f0cf40 100644 --- a/MathCore.WPF/Converters/CSplineInterp.cs +++ b/MathCore.WPF/Converters/CSplineInterp.cs @@ -1,5 +1,6 @@ using System.Windows.Markup; using System.Windows.Media; +using System.Linq; using MathCore.WPF.Converters.Base; @@ -7,10 +8,12 @@ namespace MathCore.WPF.Converters; -// ReSharper disable once IdentifierTypo +/// Интерполяция значений кубическим сплайном по заданной коллекции точек +/// Коллекция контрольных точек [MarkupExtensionReturnType(typeof(CSplineInterp))] public class CSplineInterp(PointCollection points) : DoubleValueConverter { + /// Инициализация интерполяции кубическим сплайном по коллекции точек public CSplineInterp() : this([]) { } private MathCore.Interpolation.CubicSpline? _SplineTo; @@ -20,11 +23,13 @@ public CSplineInterp() : this([]) { } private double _MaxX; private double _MaxY; + /// Коллекция точек для интерполяции public PointCollection Points { get; set; } = points; + /// Инициализация сплайнов; вызывает ошибку при пустой или отсутствующей коллекции public override object ProvideValue(IServiceProvider sp) { - if(Points is null || Points.Count == 0) throw new FormatException(); + if (Points is null || Points.Count == 0) throw new ArgumentException("Points must contain at least one point", nameof(Points)); var x = Points.Select(p => p.X).ToArray(); var y = Points.Select(p => p.Y).ToArray(); @@ -36,9 +41,23 @@ public override object ProvideValue(IServiceProvider sp) return base.ProvideValue(sp); } - /// - protected override double Convert(double v, double? p = null) => _SplineTo!.Value(Math.Max(Math.Min(_MaxX, v), _MinX)); + private void EnsureInitialized() + { + if (_SplineTo is null || _SplineFrom is null) + throw new InvalidOperationException("Spline not initialized; call ProvideValue before using the converter"); + } - /// - protected override double ConvertBack(double v, double? p = null) => _SplineFrom!.Value(Math.Max(Math.Min(_MaxY, v), _MinY)); + /// Вычисляет значение сплайна в заданной точке с ограничением по диапазону + protected override double Convert(double v, double? p = null) + { + EnsureInitialized(); + return _SplineTo!.Value(Math.Max(Math.Min(_MaxX, v), _MinX)); + } + + /// Обратное преобразование через обратный сплайн + protected override double ConvertBack(double v, double? p = null) + { + EnsureInitialized(); + return _SplineFrom!.Value(Math.Max(Math.Min(_MaxY, v), _MinY)); + } } \ No newline at end of file diff --git a/MathCore.WPF/Converters/ColorBrushConverter.cs b/MathCore.WPF/Converters/ColorBrushConverter.cs index fbb5c7c..78973c1 100644 --- a/MathCore.WPF/Converters/ColorBrushConverter.cs +++ b/MathCore.WPF/Converters/ColorBrushConverter.cs @@ -7,11 +7,13 @@ namespace MathCore.WPF.Converters; +/// Преобразует Color в SolidColorBrush с сохранением кеша и заморозкой кистей [MarkupExtensionReturnType(typeof(ColorBrushConverter))] public class ColorBrushConverter : ValueConverter { private static readonly ConcurrentDictionary __Brushes = new(); + /// Преобразует Color в SolidColorBrush protected override object? Convert(object? v, Type t, object? p, CultureInfo c) => v is Color color ? __Brushes.GetOrAdd(color, brush_color => @@ -22,6 +24,7 @@ v is Color color }) : null; + /// Преобразует SolidColorBrush обратно в Color protected override object? ConvertBack(object? v, Type t, object? p, CultureInfo c) => v is SolidColorBrush { Color: var color } ? color diff --git a/MathCore.WPF/Converters/Combine.cs b/MathCore.WPF/Converters/Combine.cs index c72e448..6e3264a 100644 --- a/MathCore.WPF/Converters/Combine.cs +++ b/MathCore.WPF/Converters/Combine.cs @@ -1,6 +1,7 @@ using System.Globalization; using System.Windows.Data; using System.Windows.Markup; +using System.Linq; using MathCore.WPF.Converters.Base; @@ -63,11 +64,6 @@ public Combine(IValueConverter First, IValueConverter Then) : this(First, Then, if (other[i] is { } converter) v = converter.ConvertBack(v, t, p, c); - if (other is { Length: > 0 }) - for (var i = other.Length - 1; i >= 0; i--) - if (other[i] is { } conv) - v = conv.ConvertBack(v, t, p, c); - if (Then != null) v = Then.ConvertBack(v, t, p, c); if (First != null) v = First.ConvertBack(v, t, p, c); diff --git a/MathCore.WPF/Converters/CombineMulti.cs b/MathCore.WPF/Converters/CombineMulti.cs index 59ab859..2c15fb3 100644 --- a/MathCore.WPF/Converters/CombineMulti.cs +++ b/MathCore.WPF/Converters/CombineMulti.cs @@ -9,31 +9,46 @@ namespace MathCore.WPF.Converters; +/// Комбинирует многозначный конвертер с одиночным конвертером [MarkupExtensionReturnType(typeof(CombineMulti))] public class CombineMulti(IMultiValueConverter First, IValueConverter Then) : MultiValueValueConverter { + /// Комбинирует многозначный конвертер с одиночным конвертером public CombineMulti() : this(null, null) { } + /// Комбинирует многозначный конвертер с одиночным конвертером public CombineMulti(IMultiValueConverter First) : this(First, null) { } + /// Первичный многозначный конвертер [ConstructorArgument(nameof(First))] public IMultiValueConverter? First { get; set; } = First; + /// Последующий одиночный конвертер [ConstructorArgument(nameof(Then))] public IValueConverter? Then { get; set; } = Then; + /// protected override object? Convert(object[]? vv, Type? t, object? p, CultureInfo? c) { - var result = (First ?? throw new InvalidOperationException("Не задан первичный конвертер значений")).Convert(vv, t, p, c); + var primary = First ?? throw new InvalidOperationException("Не задан первичный конвертер значений"); + var result = primary.Convert(vv, t, p, c); return Then is { } then ? then.Convert(result, t, p, c) : result; } + /// protected override object[]? ConvertBack(object? v, Type[]? tt, object? p, CultureInfo? c) { + var result = v; + if (Then is { } then) - v = then.ConvertBack(v, v != null ? v.GetType() : typeof(object), p, c); - return (First ?? throw new InvalidOperationException("Не задан первичный конвертер значений")).ConvertBack(v, tt, p, c); + { + // Для одиночного конвертера передаём тип целевого значения как Type (в случае null используем typeof(object)) + var targetType = result != null ? result.GetType() : typeof(object); + result = then.ConvertBack(result, targetType, p, c); + } + + return (First ?? throw new InvalidOperationException("Не задан первичный конвертер значений")).ConvertBack(result, tt, p, c); } } \ No newline at end of file diff --git a/MathCore.WPF/Converters/Composite.cs b/MathCore.WPF/Converters/Composite.cs index b9ddec9..c4d7ba6 100644 --- a/MathCore.WPF/Converters/Composite.cs +++ b/MathCore.WPF/Converters/Composite.cs @@ -1,6 +1,7 @@ using System.Globalization; using System.Windows.Data; using System.Windows.Markup; +using System.Linq; using MathCore.WPF.Converters.Base; @@ -13,6 +14,7 @@ namespace MathCore.WPF.Converters; [ContentProperty("Converters")] [MarkupExtensionReturnType(typeof(Composite))] +/// Последовательно применяет набор вложенных конвертеров public class Composite : ValueConverter, IAddChild { private readonly List _Converters = []; @@ -22,10 +24,10 @@ public class Composite : ValueConverter, IAddChild #region IValueConverter - /// + /// Применяет последовательно все внутренние конвертеры protected override object? Convert(object? v, Type? t, object? p, CultureInfo? c) => _Converters.Aggregate(v, (V, C) => C.Convert(V, t, p, c)); - /// + /// Обратное преобразование применяет конвертеры в обратном порядке protected override object? ConvertBack(object? v, Type? t, object? p, CultureInfo? c) { for (var i = _Converters.Count - 1; i >= 0; i--) @@ -35,17 +37,19 @@ public class Composite : ValueConverter, IAddChild #endregion + /// Добавляет дочерний объект-конвертер в коллекцию public void AddChild(object value) { switch (value) { - default: throw new ArgumentException($"Объект {value.GetType()} не реализует интерфейс {typeof(IValueConverter)}"); case null: throw new ArgumentNullException(nameof(value)); case IValueConverter converter: _Converters.Add(converter); break; + default: throw new ArgumentException($"Объект {value.GetType()} не реализует интерфейс {typeof(IValueConverter)}"); } } + /// Добавление текста в коллекцию не поддерживается public void AddText(string text) => throw new NotSupportedException(); } \ No newline at end of file diff --git a/MathCore.WPF/Converters/ConvertersToRefactoring2.md b/MathCore.WPF/Converters/ConvertersToRefactoring2.md index 28b893f..19fb329 100644 --- a/MathCore.WPF/Converters/ConvertersToRefactoring2.md +++ b/MathCore.WPF/Converters/ConvertersToRefactoring2.md @@ -259,3 +259,17 @@ --- Осталось 0 файл(ов) для обработки. + +--- + +# Прогресс: добавлены XML‑комментарии + +Ниже обновление по изменениям: добавлены XML‑комментарии и внесены минимальные защитные правки для трёх конвертеров. + +- MathCore.WPF/Converters/NANtoVisibility.cs — добавлена XML‑документация для класса и свойств `Inverted`, `Collapsed`; добавлена проверка типа входного значения и возвращение `Binding.DoNothing` для неподходящих типов +- MathCore.WPF/Converters/Not.cs — добавлена XML‑документация; Convert/ConvertBack теперь безопасно обрабатывают `null` и несоответствующие типы, возвращая `Binding.DoNothing` +- MathCore.WPF/Converters/Mapper.cs — добавлена XML‑документация для класса и всех публичных свойств; добавлена защита при пересчёте коэффициента `_k` (деление на ноль заменено на `_k = 0`) и ConvertBack возвращает `double.NaN` при `_k == 0` + +Статус: эти пункты в ConvertersToRefactoring2.md можно считать выполненными при последующих проходах проверки. + +--- diff --git a/MathCore.WPF/Converters/Cos.cs b/MathCore.WPF/Converters/Cos.cs index 3562be9..bb12194 100644 --- a/MathCore.WPF/Converters/Cos.cs +++ b/MathCore.WPF/Converters/Cos.cs @@ -5,17 +5,21 @@ namespace MathCore.WPF.Converters; +/// Преобразует значение по функции косинуса с масштабом и смещением [ValueConversion(typeof(double), typeof(double))] [MarkupExtensionReturnType(typeof(Cos))] public class Cos : DoubleValueConverter { + /// Множитель выходного значения public double K { get; set; } = 1; + /// Аддитивное смещение public double B { get; set; } = 0; + /// Частота (коэффициент перед аргументом функции) public double W { get; set; } = Consts.pi2; - /// + /// Возвращает NaN для NaN входа, иначе Math.Cos(W * v) * K + B protected override double Convert(double v, double? p = null) => double.IsNaN(v) ? v : Math.Cos(W * v) * K + B; /// diff --git a/MathCore.WPF/Converters/Ctg.cs b/MathCore.WPF/Converters/Ctg.cs index 155c47a..bec803d 100644 --- a/MathCore.WPF/Converters/Ctg.cs +++ b/MathCore.WPF/Converters/Ctg.cs @@ -5,17 +5,21 @@ namespace MathCore.WPF.Converters; +/// Преобразует значение по функции котангенса [ValueConversion(typeof(double), typeof(double))] [MarkupExtensionReturnType(typeof(Ctg))] public class Ctg : DoubleValueConverter { + /// Множитель выходного значения public double K { get; set; } = 1; + /// Аддитивное смещение public double B { get; set; } = 0; + /// Коэффициент перед аргументом функции public double W { get; set; } = Consts.pi2; - /// + /// Возвращает NaN для NaN входа, иначе K / Tan(W * v) + B protected override double Convert(double v, double? p = null) => double.IsNaN(v) ? v : K / Math.Tan(W * v) + B; /// diff --git a/MathCore.WPF/Converters/Custom.cs b/MathCore.WPF/Converters/Custom.cs index 5fa3623..b1dc6ad 100644 --- a/MathCore.WPF/Converters/Custom.cs +++ b/MathCore.WPF/Converters/Custom.cs @@ -1,4 +1,5 @@ using System.Globalization; +using System.Windows.Data; using System.Windows.Markup; using MathCore.WPF.Converters.Base; @@ -9,24 +10,31 @@ namespace MathCore.WPF.Converters; +/// Позволяет задать пользовательские делегаты для Convert/ConvertBack [MarkupExtensionReturnType(typeof(Custom))] public class Custom : ValueConverter { + /// Функция прямого преобразования без параметра public Func? Forward { get; set; } + /// Функция прямого преобразования с параметром public Func? ForwardParam { get; set; } + /// Функция обратного преобразования без параметра public Func? Backward { get; set; } + /// Функция обратного преобразования с параметром public Func? BackwardParam { get; set; } - protected override object? Convert(object? v, Type? t, object? p, CultureInfo? c) => + /// + protected override object? Convert(object? v, Type? t, object? p, CultureInfo? c) => Forward is null - ? ForwardParam?.Invoke(v, p) - : Forward(v); + ? ForwardParam?.Invoke(v, p) + : Forward(v) ?? Binding.DoNothing; - protected override object? ConvertBack(object? v, Type? t, object? p, CultureInfo? c) => + /// + protected override object? ConvertBack(object? v, Type? t, object? p, CultureInfo? c) => Backward is null - ? BackwardParam?.Invoke(v, p) - : Backward(v); + ? BackwardParam?.Invoke(v, p) + : Backward(v) ?? Binding.DoNothing; } \ No newline at end of file diff --git a/MathCore.WPF/Converters/CustomMulti.cs b/MathCore.WPF/Converters/CustomMulti.cs index e302e1b..48f4eae 100644 --- a/MathCore.WPF/Converters/CustomMulti.cs +++ b/MathCore.WPF/Converters/CustomMulti.cs @@ -1,4 +1,5 @@ using System.Globalization; +using System.Windows.Data; using System.Windows.Markup; using MathCore.WPF.Converters.Base; @@ -8,24 +9,31 @@ namespace MathCore.WPF.Converters; +/// Позволяет задать пользовательские делегаты для многозначных Convert/ConvertBack [MarkupExtensionReturnType(typeof(CustomMulti))] public class CustomMulti : MultiValueValueConverter { + /// Делегат прямого преобразования для массива входных значений public Func? Forward { get; set; } + /// Делегат прямого преобразования с параметром public Func? ForwardParam { get; set; } + /// Делегат обратного преобразования без параметра public Func? Backward { get; set; } + /// Делегат обратного преобразования с параметром public Func? BackwardParam { get; set; } - protected override object? Convert(object[]? vv, Type? t, object? p, CultureInfo? c) => - Forward is null - ? ForwardParam?.Invoke(vv, p) + /// + protected override object? Convert(object[]? vv, Type? t, object? p, CultureInfo? c) => + Forward is null + ? ForwardParam?.Invoke(vv, p) : Forward(vv); - protected override object[]? ConvertBack(object? v, Type[]? tt, object? p, CultureInfo? c) => - Backward is null - ? BackwardParam?.Invoke(v, p) + /// + protected override object[]? ConvertBack(object? v, Type[]? tt, object? p, CultureInfo? c) => + Backward is null + ? BackwardParam?.Invoke(v, p) : Backward(v); } \ No newline at end of file diff --git a/MathCore.WPF/Converters/DataLengthString.cs b/MathCore.WPF/Converters/DataLengthString.cs index 80da84c..04331cb 100644 --- a/MathCore.WPF/Converters/DataLengthString.cs +++ b/MathCore.WPF/Converters/DataLengthString.cs @@ -9,12 +9,15 @@ namespace MathCore.WPF.Converters; +/// Преобразует числовое значение в объект DataLength [ValueConversion(typeof(double), typeof(DataLength))] [MarkupExtensionReturnType(typeof(DataLengthString))] // ReSharper disable once UnusedMember.Global public sealed class DataLengthString : ValueConverter { - /// - protected override object Convert(object? v, Type? t, object? p, CultureInfo? c) => - new DataLength(System.Convert.ToDouble(v), 1024d); + /// Преобразует числовое значение (в байтах) в DataLength с основанием 1024 + protected override object Convert(object? v, Type? t, object? p, CultureInfo? c) => + DoubleValueConverter.TryConvertToDouble(v, c, out var value) + ? new DataLength(value, 1024d) + : Binding.DoNothing; } \ No newline at end of file diff --git a/MathCore.WPF/Converters/DefaultIfNaN.cs b/MathCore.WPF/Converters/DefaultIfNaN.cs index 0ab6f0f..fed04e3 100644 --- a/MathCore.WPF/Converters/DefaultIfNaN.cs +++ b/MathCore.WPF/Converters/DefaultIfNaN.cs @@ -1,10 +1,12 @@ using System.Globalization; +using System.Windows.Data; using System.Windows.Markup; using MathCore.WPF.Converters.Base; namespace MathCore.WPF.Converters; +/// Возвращает значение по умолчанию, если входное значение является NaN [MarkupExtensionReturnType(typeof(DefaultIfNaN))] public class DefaultIfNaN(double DefaultValue) : ValueConverter { @@ -13,9 +15,19 @@ public DefaultIfNaN() : this(default) { } [ConstructorArgument(nameof(DefaultValue))] public double DefaultValue { get; set; } = DefaultValue; + /// + /// Возвращает DefaultValue если входное значение NaN + /// + /// Входное значение + /// Тип целевого свойства + /// Параметры привязки + /// Информация о культуре + /// + /// ? : + /// protected override object? Convert(object? v, Type t, object? p, CultureInfo c) => v switch { double d => double.IsNaN(d) ? DefaultValue : d, - _ => v + _ => Binding.DoNothing }; } \ No newline at end of file diff --git a/MathCore.WPF/Converters/Deviation.cs b/MathCore.WPF/Converters/Deviation.cs index fa3b5dd..12eb148 100644 --- a/MathCore.WPF/Converters/Deviation.cs +++ b/MathCore.WPF/Converters/Deviation.cs @@ -5,7 +5,7 @@ namespace MathCore.WPF.Converters; -/// +/// Вычисляет разницу между текущим и предыдущим значением [ValueConversion(typeof(double), typeof(double))] // ReSharper disable once UnusedType.Global [MarkupExtensionReturnType(typeof(Deviation))] @@ -13,6 +13,7 @@ public class Deviation : DoubleValueConverter { private double _LastValue = double.NaN; + /// Возвращает разницу между текущим и предыдущим значением protected override double Convert(double v, double? p = null) { var dev = v - _LastValue; diff --git a/MathCore.WPF/Converters/Divide.cs b/MathCore.WPF/Converters/Divide.cs index e32f4ad..34bf81f 100644 --- a/MathCore.WPF/Converters/Divide.cs +++ b/MathCore.WPF/Converters/Divide.cs @@ -5,6 +5,7 @@ namespace MathCore.WPF.Converters; /// Преобразователь деления значения на вещественное число +/// При K == 0 обратное преобразование может привести к делению на ноль [MarkupExtensionReturnType(typeof(Divide))] // ReSharper disable once UnusedType.Global public class Divide(double K) : SimpleDoubleValueConverter(K, (v, k) => v / k, (r, k) => r * k) diff --git a/MathCore.WPF/Converters/DivideMulti.cs b/MathCore.WPF/Converters/DivideMulti.cs index 035f449..58e2956 100644 --- a/MathCore.WPF/Converters/DivideMulti.cs +++ b/MathCore.WPF/Converters/DivideMulti.cs @@ -5,9 +5,12 @@ namespace MathCore.WPF.Converters; +/// Выполняет последовательное деление элементов массива [MarkupExtensionReturnType(typeof(DivideMulti))] public class DivideMulti : MultiValueValueConverter { + /// Выполняет последовательное деление элементов массива + /// Возвращает NaN при некорректных входах; при делении на ноль возвращает Infinity в зависимости от знака protected override object? Convert(object?[]? vv, Type? t, object? p, CultureInfo? c) { switch (vv) diff --git a/MathCore.WPF/Converters/ExConverter.cs b/MathCore.WPF/Converters/ExConverter.cs index 5aa669a..a02daa8 100644 --- a/MathCore.WPF/Converters/ExConverter.cs +++ b/MathCore.WPF/Converters/ExConverter.cs @@ -1,4 +1,5 @@ using System.Globalization; +using System.Linq; using System.Windows.Data; using System.Windows.Markup; @@ -8,19 +9,22 @@ namespace MathCore.WPF.Converters; +/// Комбинирует последовательность вложенных конвертеров в один [MarkupExtensionReturnType(typeof(ExConverter))] public class ExConverter : ValueConverter { + /// Внешний преобразователь: выполняет последовательное применение вложенных конвертеров public IValueConverter? From { get; set; } + /// Внешний преобразователь: применяется после получения значения public IValueConverter? To { get; set; } /// - protected override object? Convert(object? v, Type? t, object? p, CultureInfo? c) => + protected override object? Convert(object? v, Type? t, object? p, CultureInfo? c) => GetConverters().Aggregate(v, (value, converter) => To?.Convert(converter.Convert(value, t, p, c), t, p, c)); /// - protected override object? ConvertBack(object? v, Type? t, object? p, CultureInfo? c) => + protected override object? ConvertBack(object? v, Type? t, object? p, CultureInfo? c) => GetConverters().Reverse().Aggregate(v, (value, converter) => converter.ConvertBack(To?.Convert(value, t, p, c), t, p, c)); private IEnumerable GetConverters() diff --git a/MathCore.WPF/Converters/ExpConverter.cs b/MathCore.WPF/Converters/ExpConverter.cs index 9533e81..44bfaa2 100644 --- a/MathCore.WPF/Converters/ExpConverter.cs +++ b/MathCore.WPF/Converters/ExpConverter.cs @@ -1,10 +1,12 @@ using System.Collections.Concurrent; +using System.Globalization; using MathCore.MathParser; using MathCore.WPF.Converters.Base; namespace MathCore.WPF.Converters; +/// Вычисляет значение выражения, скомпилированного в делегат public class Expr : SimpleDoubleValueConverter { private static readonly ConcurrentDictionary<(string Expr, string ArgName, string ParameterName), Func> __Converters = new(); @@ -23,12 +25,16 @@ private static Func GetConverter((string Expr, string Ar private Func? _Converter; + /// Значение параметра по умолчанию public double ParameterDefault { get; set; } = double.NaN; + /// Имя аргумента в выражении public string ArgumentName { get; set; } = "x"; + /// Имя параметра в выражении public string ParameterName { get; set; } = "p"; + /// Строковое выражение для компиляции public string Expression { get => _Expression; @@ -45,8 +51,8 @@ public Expr() { } public Expr(string Expression) => this.Expression = Expression; - /// - protected override double Convert(double v, double? p = null) => _Converter is { } converter - ? converter(v, p ?? ParameterDefault) + /// Преобразует входное значение с использованием скомпилированного выражения или вызывает базовое поведение при отсутствии выражения + protected override double Convert(double v, double? p = null) => _Converter is { } converter + ? converter(v, p ?? ParameterDefault) : base.Convert(v, p); } diff --git a/MathCore.WPF/Converters/FirstLastItemConverter.cs b/MathCore.WPF/Converters/FirstLastItemConverter.cs index 14b6dc3..94063e8 100644 --- a/MathCore.WPF/Converters/FirstLastItemConverter.cs +++ b/MathCore.WPF/Converters/FirstLastItemConverter.cs @@ -7,9 +7,11 @@ namespace MathCore.WPF.Converters; +/// Преобразователь, возвращающий первый элемент коллекции [MarkupExtensionReturnType(typeof(FirstItemConverter))] public class FirstItemConverter : ValueConverter { + /// Возвращает первый элемент перечисления или null private static object? GetFirstValue(IEnumerable items) { var enumerator = items.GetEnumerator(); @@ -23,6 +25,7 @@ public class FirstItemConverter : ValueConverter } } + /// Преобразует входную коллекцию, возвращая первый элемент protected override object? Convert(object? v, Type t, object? p, CultureInfo c) => v switch { diff --git a/MathCore.WPF/Converters/GetType.cs b/MathCore.WPF/Converters/GetType.cs index 38b22e7..08b1a1a 100644 --- a/MathCore.WPF/Converters/GetType.cs +++ b/MathCore.WPF/Converters/GetType.cs @@ -8,9 +8,11 @@ namespace MathCore.WPF.Converters; +/// Возвращает System.Type переданного объекта [ValueConversion(typeof(object), typeof(Type))] [MarkupExtensionReturnType(typeof(GetType))] public class GetType : ValueConverter { - protected override object? Convert(object? v, Type? t, object? p, CultureInfo? c) => v?.GetType(); + /// Возвращает System.Type для переданного объекта или Binding.DoNothing для null + protected override object? Convert(object? v, Type? t, object? p, CultureInfo? c) => v is null ? Binding.DoNothing : v.GetType(); } \ No newline at end of file diff --git a/MathCore.WPF/Converters/GreaterOrEqualThanMulti.cs b/MathCore.WPF/Converters/GreaterOrEqualThanMulti.cs index 5a2bf38..50ac1ef 100644 --- a/MathCore.WPF/Converters/GreaterOrEqualThanMulti.cs +++ b/MathCore.WPF/Converters/GreaterOrEqualThanMulti.cs @@ -6,9 +6,23 @@ namespace MathCore.WPF.Converters; +/// Проверяет, что все последующие значения не больше первого [MarkupExtensionReturnType(typeof(GreaterThanMulti))] public class GreaterOrEqualThanMulti : MultiValueValueConverter { + /// + /// Преобразует массив объектов в булево значение, указывающее + /// на то, что все последующие значения не больше первого. + /// + /// Массив значений для проверки. + /// Тип, в который требуется преобразовать значения. + /// Дополнительный параметр, который может быть использован при преобразовании. + /// Культура, которая может быть использована при преобразовании. + /// + /// Возвращает , если все последующие значения не больше первого; + /// иначе - . Если входные данные недопустимы, + /// возвращает . + /// protected override object? Convert(object[]? vv, Type? t, object? p, CultureInfo? c) { if (vv is not { Length: > 1 }) diff --git a/MathCore.WPF/Converters/GreaterThan.cs b/MathCore.WPF/Converters/GreaterThan.cs index caff35e..7c35132 100644 --- a/MathCore.WPF/Converters/GreaterThan.cs +++ b/MathCore.WPF/Converters/GreaterThan.cs @@ -8,13 +8,18 @@ namespace MathCore.WPF.Converters; +/// Проверяет что значение больше заданного порога [MarkupExtensionReturnType(typeof(GreaterThan))] [ValueConversion(typeof(double), typeof(bool?))] public class GreaterThan(double value) : DoubleToBool { public GreaterThan() : this(double.NegativeInfinity) { } + /// Пороговое значение public double Value { get; set; } = value; - protected override bool? Convert(double v) => v.IsNaN() ? null : v > Value; + /// + /// Возвращает null при NaN входе, иначе true если v > Value + /// + protected override bool? Convert(double v) => double.IsNaN(v) ? null : v > Value; } \ No newline at end of file diff --git a/MathCore.WPF/Converters/GreaterThanMulti.cs b/MathCore.WPF/Converters/GreaterThanMulti.cs index f15eebc..1c454d8 100644 --- a/MathCore.WPF/Converters/GreaterThanMulti.cs +++ b/MathCore.WPF/Converters/GreaterThanMulti.cs @@ -6,9 +6,20 @@ namespace MathCore.WPF.Converters; +/// Проверяет, что все последующие значения меньше первого [MarkupExtensionReturnType(typeof(GreaterThanMulti))] public class GreaterThanMulti : MultiValueValueConverter { + /// Проверяет, что все последующие значения меньше первого + /// Массив значений, которые требуется проверить + /// Тип, в который требуется произвести преобразование + /// Дополнительные параметры преобразования + /// Культура, используемая при преобразовании + /// + /// Возвращает , если все последующие значения меньше первого; + /// возвращает , если хотя бы одно значение больше или равно первому; + /// возвращает , если входные данные некорректны. + /// protected override object? Convert(object[]? vv, Type? t, object? p, CultureInfo? c) { if (vv is not { Length: > 1 }) diff --git a/MathCore.WPF/Converters/GreaterThanOrEqual.cs b/MathCore.WPF/Converters/GreaterThanOrEqual.cs index 4391a63..c38ef5a 100644 --- a/MathCore.WPF/Converters/GreaterThanOrEqual.cs +++ b/MathCore.WPF/Converters/GreaterThanOrEqual.cs @@ -8,13 +8,16 @@ namespace MathCore.WPF.Converters; +/// Проверяет что значение больше или равно заданному порогу [MarkupExtensionReturnType(typeof(GreaterThanOrEqual))] [ValueConversion(typeof(double), typeof(bool?))] public class GreaterThanOrEqual(double value) : DoubleToBool { public GreaterThanOrEqual() : this(double.NegativeInfinity) { } + /// Пороговое значение public double Value { get; set; } = value; - protected override bool? Convert(double v) => v.IsNaN() ? null : v >= Value; + /// Возвращает null для NaN, иначе true если v >= Value + protected override bool? Convert(double v) => double.IsNaN(v) ? null : v >= Value; } \ No newline at end of file diff --git a/MathCore.WPF/Converters/IO/FilePathToName.cs b/MathCore.WPF/Converters/IO/FilePathToName.cs index 7a04d46..863420e 100644 --- a/MathCore.WPF/Converters/IO/FilePathToName.cs +++ b/MathCore.WPF/Converters/IO/FilePathToName.cs @@ -9,11 +9,12 @@ namespace MathCore.WPF.Converters.IO; +/// Возвращает имя файла из полного пути [MarkupExtensionReturnType(typeof(FilePathToName))] [ValueConversion(typeof(string), typeof(string))] public class FilePathToName : ValueConverter { - /// + /// Возвращает имя файла из полного пути или Binding.DoNothing при неподдерживаемом типе protected override object? Convert(object? v, Type t, object? p, CultureInfo c) => - v is string str ? Path.GetFileName(str) : null; + v is string str ? Path.GetFileName(str) : Binding.DoNothing; } \ No newline at end of file diff --git a/MathCore.WPF/Converters/InIntervalValue.cs b/MathCore.WPF/Converters/InIntervalValue.cs index fd1f287..2835532 100644 --- a/MathCore.WPF/Converters/InIntervalValue.cs +++ b/MathCore.WPF/Converters/InIntervalValue.cs @@ -8,6 +8,7 @@ namespace MathCore.WPF.Converters; +/// Нормализует значение в заданном интервале [MarkupExtensionReturnType(typeof(InIntervalValue))] [ValueConversion(typeof(double), typeof(double))] public class InIntervalValue(Interval interval) : DoubleValueConverter @@ -28,6 +29,10 @@ public InIntervalValue(double Min, double Max) : this(new(Math.Min(Min, Max), Ma public bool MaxInclude { get => interval.MaxInclude; set => interval = interval.IncludeMax(value); } - /// - protected override double Convert(double v, double? p = null) => (p ?? v) is not double.NaN and var value ? interval.Normalize(value) : double.NaN; + /// Нормализует значение в заданном интервале, возвращая NaN для NaN входа + protected override double Convert(double v, double? p = null) + { + var value = p ?? v; + return double.IsNaN(value) ? double.NaN : interval.Normalize(value); + } } \ No newline at end of file diff --git a/MathCore.WPF/Converters/InRange.cs b/MathCore.WPF/Converters/InRange.cs index bb36663..965c860 100644 --- a/MathCore.WPF/Converters/InRange.cs +++ b/MathCore.WPF/Converters/InRange.cs @@ -8,6 +8,7 @@ namespace MathCore.WPF.Converters; +/// Проверяет, что значение входит в указанный интервал [MarkupExtensionReturnType(typeof(InRange))] [ValueConversion(typeof(double), typeof(bool?))] public class InRange(Interval interval) : DoubleToBool @@ -28,6 +29,6 @@ public InRange(double min, double max) : this(new(Math.Min(min, max), Math.Max(m public bool MaxInclude { get => interval.MaxInclude; set => interval = interval.IncludeMax(value); } - /// - protected override bool? Convert(double v) => v.IsNaN() ? null : interval.Check(v); + /// Возвращает null для NaN входа, иначе true если значение в интервале + protected override bool? Convert(double v) => double.IsNaN(v) ? null : interval.Check(v); } \ No newline at end of file diff --git a/MathCore.WPF/Converters/Interpolation.cs b/MathCore.WPF/Converters/Interpolation.cs index f296b78..b27fe09 100644 --- a/MathCore.WPF/Converters/Interpolation.cs +++ b/MathCore.WPF/Converters/Interpolation.cs @@ -9,6 +9,7 @@ namespace MathCore.WPF.Converters; +/// Интерполирует значение по коллекции контрольных точек [ValueConversion(typeof(double), typeof(double))] [MarkupExtensionReturnType(typeof(Interpolation))] public class Interpolation : DoubleValueConverter @@ -16,6 +17,7 @@ public class Interpolation : DoubleValueConverter private Polynom? _Polynom; private PointCollection? _Points; + /// Коллекция контрольных точек public PointCollection? Points { get => _Points; @@ -32,5 +34,6 @@ public PointCollection? Points } } - protected override double Convert(double v, double? p = null) => _Polynom!.Value(v); + /// Вычисляет значение полинома или возвращает Binding.DoNothing, если полином не инициализирован + protected override double Convert(double v, double? p = null) => _Polynom is null ? throw new InvalidOperationException("Polynom not initialized; set Points before using converter") : _Polynom.Value(v); } \ No newline at end of file diff --git a/MathCore.WPF/Converters/Inverse.cs b/MathCore.WPF/Converters/Inverse.cs index c07c31b..491ad7c 100644 --- a/MathCore.WPF/Converters/Inverse.cs +++ b/MathCore.WPF/Converters/Inverse.cs @@ -4,8 +4,14 @@ namespace MathCore.WPF.Converters; -[MarkupExtensionReturnType(typeof(double))] +/// Преобразует число в обратное значение (1/x) или параметр/x +[MarkupExtensionReturnType(typeof(Inverse))] public class Inverse : SimpleDoubleValueConverter { - protected override double Convert(double v, double? p = null) => p is { } k ? k / v : 1 / v; + /// Возвращает обратное значение: параметр / v или 1 / v + protected override double Convert(double v, double? p = null) + { + if (v == 0) return double.NaN; + return p is double k ? k / v : 1 / v; + } } \ No newline at end of file diff --git a/MathCore.WPF/Converters/IsNaN.cs b/MathCore.WPF/Converters/IsNaN.cs index 4c27e46..9992ef1 100644 --- a/MathCore.WPF/Converters/IsNaN.cs +++ b/MathCore.WPF/Converters/IsNaN.cs @@ -8,6 +8,7 @@ namespace MathCore.WPF.Converters; +/// Проверяет, является ли значение NaN [MarkupExtensionReturnType(typeof(IsNaN))] [ValueConversion(typeof(double), typeof(bool?))] public class IsNaN(bool Inverted) : DoubleToBool @@ -17,6 +18,6 @@ public IsNaN() : this(false) { } [ConstructorArgument(nameof(Inverted))] public bool Inverted { get; set; } = Inverted; - /// - protected override bool? Convert(double v) => Inverted ^ v.IsNaN(); + /// Возвращает логическое значение, показывающее является ли вход NaN (с учётом инверсии) + protected override bool? Convert(double v) => Inverted ^ double.IsNaN(v); } \ No newline at end of file diff --git a/MathCore.WPF/Converters/IsNegative.cs b/MathCore.WPF/Converters/IsNegative.cs index 3f5b4a5..03a713d 100644 --- a/MathCore.WPF/Converters/IsNegative.cs +++ b/MathCore.WPF/Converters/IsNegative.cs @@ -7,10 +7,11 @@ namespace MathCore.WPF.Converters; +/// Проверяет, является ли число отрицательным [MarkupExtensionReturnType(typeof(IsNegative))] [ValueConversion(typeof(double), typeof(bool?))] public class IsNegative : DoubleToBool { - /// - protected override bool? Convert(double v) => v.IsNaN() ? null : v < 0; + /// Возвращает null для NaN, иначе true если значение отрицательно + protected override bool? Convert(double v) => double.IsNaN(v) ? null : v < 0; } \ No newline at end of file diff --git a/MathCore.WPF/Converters/IsNull.cs b/MathCore.WPF/Converters/IsNull.cs index 3d86bff..04be460 100644 --- a/MathCore.WPF/Converters/IsNull.cs +++ b/MathCore.WPF/Converters/IsNull.cs @@ -5,6 +5,7 @@ namespace MathCore.WPF.Converters; +/// Проверяет, является ли значение null [MarkupExtensionReturnType(typeof(IsNull))] public class IsNull(bool Inverted) : ValueConverter { @@ -13,5 +14,6 @@ public IsNull() : this(false) { } [ConstructorArgument(nameof(Inverted))] public bool Inverted { get; set; } = Inverted; + /// Возвращает true если значение null (с учётом Inverted) protected override object? Convert(object? v, Type t, object? p, CultureInfo c) => Inverted ^ (v is null); } \ No newline at end of file diff --git a/MathCore.WPF/Converters/IsPositive.cs b/MathCore.WPF/Converters/IsPositive.cs index 26d04c1..f0aec54 100644 --- a/MathCore.WPF/Converters/IsPositive.cs +++ b/MathCore.WPF/Converters/IsPositive.cs @@ -7,10 +7,11 @@ namespace MathCore.WPF.Converters; +/// Проверяет, является ли число положительным [MarkupExtensionReturnType(typeof(IsPositive))] [ValueConversion(typeof(double), typeof(bool?))] public class IsPositive : DoubleToBool { - /// - protected override bool? Convert(double v) => v is double.NaN ? null : v > 0; + /// Возвращает null для NaN, иначе true если значение положительно + protected override bool? Convert(double v) => double.IsNaN(v) ? null : v > 0; } \ No newline at end of file diff --git a/MathCore.WPF/Converters/JoinStringConverter.cs b/MathCore.WPF/Converters/JoinStringConverter.cs index 296095e..da7f97f 100644 --- a/MathCore.WPF/Converters/JoinStringConverter.cs +++ b/MathCore.WPF/Converters/JoinStringConverter.cs @@ -1,22 +1,30 @@ using System.Globalization; +using System.Linq; using System.Windows.Data; +using System.Windows.Markup; namespace MathCore.WPF.Converters; +/// Объединяет массив значений в строку public class JoinStringConverter : IMultiValueConverter { + /// Преобразует массив значений в строку, используя параметр как разделитель public object Convert(object[]? values, Type TargetType, object? parameter, CultureInfo culture) { var separator = parameter as string ?? " "; - return string.Join(separator, values); + if (values is null) return Binding.DoNothing; + + var items = values.Select(v => v?.ToString() ?? string.Empty); + return string.Join(separator, items); } + /// Разбивает строку по разделителю и возвращает массив объектов public object[]? ConvertBack(object? value, Type[] TargetTypes, object? parameter, CultureInfo culture) { - if (value is not string { } str) return null; + if (value is not string str) return null; var separator = parameter as string ?? " "; - - return [.. str.Split([separator], StringSplitOptions.None).Cast()]; + var parts = str.Split(new[] { separator }, StringSplitOptions.None); + return parts.Cast().ToArray(); } } \ No newline at end of file diff --git a/MathCore.WPF/Converters/Lambda.cs b/MathCore.WPF/Converters/Lambda.cs index e4bf326..de210e1 100644 --- a/MathCore.WPF/Converters/Lambda.cs +++ b/MathCore.WPF/Converters/Lambda.cs @@ -1,4 +1,5 @@ using System.Globalization; +using System.Windows.Data; using System.Windows.Markup; using MathCore.WPF.Converters.Base; @@ -7,32 +8,39 @@ namespace MathCore.WPF.Converters; +/// Lambda конвертер с типизированными делегатами [MarkupExtensionReturnType(typeof(Lambda<,>))] public class Lambda(Lambda.Converter Converter, Lambda.ConverterBack? BackConverter = null) : ValueConverter { + /// Именованный делегат преобразования + public delegate TResult Converter(TValue Value, Type? TargetValueType, object? Parameter, CultureInfo? Culture); + + /// Именованный делегат обратного преобразования + public delegate TValue ConverterBack(TResult Value, Type? SourceValueType, object? Parameter, CultureInfo? Culture); + public Lambda( Func Converter, Func? BackConverter = null) : this((v, _, _, _) => Converter(v), BackConverter is null ? null : ((v, _, _, _) => BackConverter(v))) { } - public delegate TResult Converter(TValue Value, Type? TargetValueType, object? Parameter, CultureInfo? Culture); - - public delegate TValue ConverterBack(TResult Value, Type? SourceValueType, object? Parameter, CultureInfo? Culture); - private readonly Converter _Converter = Converter; private readonly ConverterBack _BackConverter = BackConverter ?? ((_, _, _, _) => throw new NotSupportedException()); /// - protected override object? Convert(object? v, Type? t, object? p, CultureInfo? c) => - v is null - ? null - : _Converter((TValue)v, t, p, c); + protected override object? Convert(object? v, Type? t, object? p, CultureInfo? c) => + v is null + ? null + : v is TValue value + ? _Converter(value, t, p, c) + : Binding.DoNothing; /// protected override object? ConvertBack(object? v, Type? t, object? p, CultureInfo? c) => v is null ? null - : _BackConverter((TResult) v, t, p, c); + : v is TResult result + ? _BackConverter(result, t, p, c) + : Binding.DoNothing; } \ No newline at end of file diff --git a/MathCore.WPF/Converters/LambdaConverter.cs b/MathCore.WPF/Converters/LambdaConverter.cs index 090cc34..4cab649 100644 --- a/MathCore.WPF/Converters/LambdaConverter.cs +++ b/MathCore.WPF/Converters/LambdaConverter.cs @@ -5,14 +5,20 @@ namespace MathCore.WPF.Converters; +/// Универсальный Lambda конвертер, использующий делегаты для преобразования [MarkupExtensionReturnType(typeof(LambdaConverter))] public class LambdaConverter(LambdaConverter.Converter To, LambdaConverter.ConverterBack? From = null) : ValueConverter { + /// Делегат преобразования public delegate object? Converter(object? Value, Type? TargetValueType, object? Parameter, CultureInfo? Culture); + /// Делегат обратного преобразования public delegate object? ConverterBack(object? Value, Type? SourceValueType, object? Parameter, CultureInfo? Culture); + /// protected override object? Convert(object? v, Type? t, object? p, CultureInfo? c) => To(v, t, p, c); + /// Выполняет обратное преобразование, если доступен делегат + /// Если обратное преобразование не поддерживается protected override object? ConvertBack(object? v, Type? t, object? p, CultureInfo? c) => (From ?? throw new NotSupportedException("Обратное преобразование не поддерживается")).Invoke(v, t, p, c); } \ No newline at end of file diff --git a/MathCore.WPF/Converters/LessOrEqualThanMulti.cs b/MathCore.WPF/Converters/LessOrEqualThanMulti.cs index bef9f63..3b4452c 100644 --- a/MathCore.WPF/Converters/LessOrEqualThanMulti.cs +++ b/MathCore.WPF/Converters/LessOrEqualThanMulti.cs @@ -6,9 +6,20 @@ namespace MathCore.WPF.Converters; +/// Проверяет что все последующие значения больше либо равны первому значению [MarkupExtensionReturnType(typeof(LessOrEqualThanMulti))] public class LessOrEqualThanMulti : MultiValueValueConverter { + /// Преобразует массив значений в логическое значение + /// Входные значения, первый элемент используется как эталон + /// Тип целевого значения + /// Параметр преобразования + /// Культура + /// + /// true если все последующие значения больше или равны первому; + /// false если найдено значение строго меньше первого; + /// Binding.DoNothing при некорректных входных данных + /// protected override object? Convert(object[]? vv, Type? t, object? p, CultureInfo? c) { if (vv is not { Length: > 1 }) diff --git a/MathCore.WPF/Converters/LessThan.cs b/MathCore.WPF/Converters/LessThan.cs index 32d9a9d..d5a8862 100644 --- a/MathCore.WPF/Converters/LessThan.cs +++ b/MathCore.WPF/Converters/LessThan.cs @@ -7,13 +7,16 @@ namespace MathCore.WPF.Converters; +/// Проверяет что число меньше заданного порога [MarkupExtensionReturnType(typeof(LessThan))] [ValueConversion(typeof(double), typeof(bool?))] public class LessThan(double value) : DoubleToBool { public LessThan() : this(double.PositiveInfinity) { } + /// Пороговое значение public double Value { get; set; } = value; - protected override bool? Convert(double v) => v is double.NaN ? null : v < Value; + /// Возвращает null при NaN, иначе true если v < Value + protected override bool? Convert(double v) => double.IsNaN(v) ? null : v < Value; } \ No newline at end of file diff --git a/MathCore.WPF/Converters/LessThanMulti.cs b/MathCore.WPF/Converters/LessThanMulti.cs index b357756..95e0f5a 100644 --- a/MathCore.WPF/Converters/LessThanMulti.cs +++ b/MathCore.WPF/Converters/LessThanMulti.cs @@ -6,9 +6,20 @@ namespace MathCore.WPF.Converters; +/// Проверяет что все последующие значения больше первого значения [MarkupExtensionReturnType(typeof(LessThanMulti))] public class LessThanMulti : MultiValueValueConverter { + /// Преобразует массив значений в логическое значение + /// Входные значения, первый элемент используется как эталон + /// Тип целевого значения + /// Параметр преобразования + /// Культура + /// + /// true если все последующие значения строго больше первого; + /// false если найдено значение меньше или равное первому; + /// Binding.DoNothing при некорректных входных данных + /// protected override object? Convert(object[]? vv, Type? t, object? p, CultureInfo? c) { if (vv is not { Length: > 1 }) diff --git a/MathCore.WPF/Converters/LessThanOrEqual.cs b/MathCore.WPF/Converters/LessThanOrEqual.cs index 70a2b59..c9f756a 100644 --- a/MathCore.WPF/Converters/LessThanOrEqual.cs +++ b/MathCore.WPF/Converters/LessThanOrEqual.cs @@ -7,13 +7,16 @@ namespace MathCore.WPF.Converters; +/// Преобразователь сравнения значения с порогом (меньше или равно) [MarkupExtensionReturnType(typeof(LessThanOrEqual))] [ValueConversion(typeof(double), typeof(bool?))] public class LessThanOrEqual(double value) : DoubleToBool { public LessThanOrEqual() : this(double.PositiveInfinity) { } + /// Пороговое значение public double Value { get; set; } = value; - protected override bool? Convert(double v) => v is double.NaN ? null : v <= Value; + /// Преобразует входное значение в логическое, возвращая null для NaN + protected override bool? Convert(double v) => double.IsNaN(v) ? null : v <= Value; } \ No newline at end of file diff --git a/MathCore.WPF/Converters/Linear.cs b/MathCore.WPF/Converters/Linear.cs index f217dde..42889ae 100644 --- a/MathCore.WPF/Converters/Linear.cs +++ b/MathCore.WPF/Converters/Linear.cs @@ -8,6 +8,7 @@ namespace MathCore.WPF.Converters; /// Линейный конвертер вещественных величин по формуле result = K*value + B +/// При K == 0 обратное преобразование вернёт NaN [ValueConversion(typeof(double), typeof(double))] [MarkupExtensionReturnType(typeof(Linear))] public class Linear(double K, double B) : DoubleValueConverter @@ -28,7 +29,7 @@ public Linear(double K) : this(K, 0) { } public bool Inverted { get; set; } private static double To(double x, double k, double b) => k * x + b; - private static double From(double x, double k, double b) => (x - b) / k; + private static double From(double x, double k, double b) => k == 0 ? double.NaN : (x - b) / k; /// protected override double Convert(double v, double? p = null) => diff --git a/MathCore.WPF/Converters/Mapper.cs b/MathCore.WPF/Converters/Mapper.cs index a951744..8515ba3 100644 --- a/MathCore.WPF/Converters/Mapper.cs +++ b/MathCore.WPF/Converters/Mapper.cs @@ -5,6 +5,7 @@ namespace MathCore.WPF.Converters; +/// Масштабирует значение из одного диапазона в другой [ValueConversion(typeof(double), typeof(double))] [MarkupExtensionReturnType(typeof(Mapper))] public class Mapper() : DoubleValueConverter @@ -13,52 +14,62 @@ public class Mapper() : DoubleValueConverter private double _MinScale; + /// Минимальное значение масштаба public double MinScale { get => _MinScale; set { _MinScale = value; - _k = (_MaxScale - _MinScale) / (_MaxValue - _MinValue); + RecalculateK(); } } private double _MaxScale = 1; + /// Максимальное значение масштаба public double MaxScale { get => _MaxScale; set { _MaxScale = value; - _k = (_MaxScale - _MinScale) / (_MaxValue - _MinValue); + RecalculateK(); } } private double _MinValue; + /// Минимальное значение исходного диапазона public double MinValue { get => _MinValue; set { _MinValue = value; - _k = (_MaxScale - _MinScale) / (_MaxValue - _MinValue); + RecalculateK(); } } private double _MaxValue = 1; + /// Максимальное значение исходного диапазона public double MaxValue { get => _MaxValue; set { _MaxValue = value; - _k = (_MaxScale - _MinScale) / (_MaxValue - _MinValue); + RecalculateK(); } } + private void RecalculateK() + { + var denom = (_MaxValue - _MinValue); + _k = denom == 0 ? 0 : (_MaxScale - _MinScale) / denom; + } + /// protected override double Convert(double v, double? p = null) { @@ -68,5 +79,5 @@ protected override double Convert(double v, double? p = null) } /// - protected override double ConvertBack(double x, double? p = null) => (x - _MinScale) / _k + _MinValue; + protected override double ConvertBack(double x, double? p = null) => _k == 0 ? double.NaN : (x - _MinScale) / _k + _MinValue; } \ No newline at end of file diff --git a/MathCore.WPF/Converters/MaxValue.cs b/MathCore.WPF/Converters/MaxValue.cs index 67c73d9..01b7e86 100644 --- a/MathCore.WPF/Converters/MaxValue.cs +++ b/MathCore.WPF/Converters/MaxValue.cs @@ -1,5 +1,6 @@ using System.Collections; using System.Globalization; +using System.Linq; using System.Windows.Data; using System.Windows.Markup; @@ -7,17 +8,46 @@ namespace MathCore.WPF.Converters; +/// Возвращает максимальное значение из набора входных значений [ValueConversion(typeof(IEnumerable), typeof(object))] [MarkupExtensionReturnType(typeof(MaxValue))] public class MaxValue : MarkupExtension, IMultiValueConverter, IValueConverter { + /// public override object ProvideValue(IServiceProvider sp) => this; - public object? Convert(object[]? vv, Type? t, object? p, CultureInfo? c) => vv is not { Length: > 0 } ? null : vv.Max(); + /// Возвращает максимальное значение из массива значений или Binding.DoNothing при некорректных входах + public object? Convert(object[]? vv, Type? t, object? p, CultureInfo? c) + { + if (vv is not { Length: > 0 }) return Binding.DoNothing; + try + { + return vv.Max(); + } + catch + { + return Binding.DoNothing; + } + } + + /// public object[] ConvertBack(object? v, Type[]? tt, object? p, CultureInfo? c) => throw new NotSupportedException(); - public object? Convert(object? v, Type? t, object? p, CultureInfo? c) => (v as IEnumerable)?.Cast().Max(); + /// Возвращает максимальный элемент перечисления или Binding.DoNothing + public object? Convert(object? v, Type? t, object? p, CultureInfo? c) + { + if (v is not IEnumerable enumerable) return Binding.DoNothing; + try + { + return enumerable.Cast().Max(); + } + catch + { + return Binding.DoNothing; + } + } + /// public object? ConvertBack(object? v, Type? t, object? p, CultureInfo? c) => throw new NotSupportedException(); } \ No newline at end of file diff --git a/MathCore.WPF/Converters/MinValue.cs b/MathCore.WPF/Converters/MinValue.cs index f788c14..a28c6ac 100644 --- a/MathCore.WPF/Converters/MinValue.cs +++ b/MathCore.WPF/Converters/MinValue.cs @@ -1,5 +1,6 @@ using System.Collections; using System.Globalization; +using System.Linq; using System.Windows.Data; using System.Windows.Markup; @@ -7,17 +8,42 @@ namespace MathCore.WPF.Converters; +/// Возвращает минимальное значение из набора входных значений [ValueConversion(typeof(IEnumerable), typeof(object))] [MarkupExtensionReturnType(typeof(MinValue))] public class MinValue : MarkupExtension, IMultiValueConverter, IValueConverter { public override object ProvideValue(IServiceProvider sp) => this; - public object? Convert(object[]? vv, Type? t, object? p, CultureInfo? c) => vv?.Min(); + /// Возвращает минимальное значение из массива или Binding.DoNothing + public object? Convert(object[]? vv, Type? t, object? p, CultureInfo? c) + { + if (vv is not { Length: > 0 }) return Binding.DoNothing; + try + { + return vv.Min(); + } + catch + { + return Binding.DoNothing; + } + } public object[]? ConvertBack(object? v, Type[]? tt, object? p, CultureInfo? c) => throw new NotSupportedException(); - public object? Convert(object? v, Type? t, object? p, CultureInfo? c) => (v as IEnumerable)?.Cast().Min(); + /// Возвращает минимальный элемент перечисления или Binding.DoNothing + public object? Convert(object? v, Type? t, object? p, CultureInfo? c) + { + if (v is not IEnumerable enumerable) return Binding.DoNothing; + try + { + return enumerable.Cast().Min(); + } + catch + { + return Binding.DoNothing; + } + } public object? ConvertBack(object? v, Type? t, object? p, CultureInfo? c) => throw new NotSupportedException(); } \ No newline at end of file diff --git a/MathCore.WPF/Converters/Mod.cs b/MathCore.WPF/Converters/Mod.cs index 6d210e6..41dc0bf 100644 --- a/MathCore.WPF/Converters/Mod.cs +++ b/MathCore.WPF/Converters/Mod.cs @@ -8,6 +8,7 @@ namespace MathCore.WPF.Converters; +/// Вычисляет остаток от деления значения на заданный модуль [ValueConversion(typeof(double), typeof(double))] [MarkupExtensionReturnType(typeof(Mod))] public class Mod(double M) : DoubleValueConverter @@ -16,11 +17,13 @@ public Mod() : this(double.NaN) { } public double M { get; set; } = M; - /// - protected override double Convert(double v, double? p = null) => - (p ?? v).IsNaN() - ? double.NaN - : M.IsNaN() - ? p ?? v - : (p ?? v) % M; + /// Вычисляет остаток от деления значения на M или возвращает NaN при некорректных входных данных + protected override double Convert(double v, double? p = null) + { + var value = p ?? v; + if (double.IsNaN(value)) return double.NaN; + if (double.IsNaN(M)) return value; + if (M == 0) return double.NaN; + return value % M; + } } \ No newline at end of file diff --git a/MathCore.WPF/Converters/MultiValuesToCompositeCollection.cs b/MathCore.WPF/Converters/MultiValuesToCompositeCollection.cs index ad1cde2..6f19c96 100644 --- a/MathCore.WPF/Converters/MultiValuesToCompositeCollection.cs +++ b/MathCore.WPF/Converters/MultiValuesToCompositeCollection.cs @@ -9,9 +9,11 @@ namespace MathCore.WPF.Converters; +/// Преобразует входные значения в CompositeCollection [MarkupExtensionReturnType(typeof(MultiValuesToCompositeCollection))] public class MultiValuesToCompositeCollection : MultiValueValueConverter { + /// Создаёт CompositeCollection, где последовательности оборачиваются в CollectionContainer protected override object? Convert(object[]? vv, Type? t, object? p, CultureInfo? c) { if (vv is null) return null; diff --git a/MathCore.WPF/Converters/MultiValuesToEnumerable.cs b/MathCore.WPF/Converters/MultiValuesToEnumerable.cs index bfee755..9be323a 100644 --- a/MathCore.WPF/Converters/MultiValuesToEnumerable.cs +++ b/MathCore.WPF/Converters/MultiValuesToEnumerable.cs @@ -1,5 +1,7 @@ using System.Collections; using System.Globalization; +using System.Linq; +using System.Windows.Data; using System.Windows.Markup; using MathCore.WPF.Converters.Base; @@ -8,11 +10,29 @@ namespace MathCore.WPF.Converters; +/// Возвращает входные значения как IEnumerable и восстанавливает массив при ConvertBack [MarkupExtensionReturnType(typeof(MultiValuesToEnumerable))] public class MultiValuesToEnumerable : MultiValueValueConverter { + /// Возвращает входной массив как есть protected override object? Convert(object[]? vv, Type? t, object? p, CultureInfo? c) => vv; - protected override object[]? ConvertBack(object? v, Type[]? tt, object? p, CultureInfo? c) => - (v as IEnumerable)?.Cast().Zip(tt!, System.Convert.ChangeType).ToArray()!; + /// Пытается восстановить массив значений из IEnumerable, используя указанные типы + protected override object[]? ConvertBack(object? v, Type[]? tt, object? p, CultureInfo? c) + { + if (v is not IEnumerable enumerable || tt is null) return null; + + var values = enumerable.Cast().ToArray(); + if (values.Length != tt.Length) return null; + + try + { + var result = values.Zip(tt, (val, type) => System.Convert.ChangeType(val, type)).ToArray(); + return result; + } + catch + { + return null; + } + } } \ No newline at end of file diff --git a/MathCore.WPF/Converters/Multiply.cs b/MathCore.WPF/Converters/Multiply.cs index 0346c2f..b1d0b1e 100644 --- a/MathCore.WPF/Converters/Multiply.cs +++ b/MathCore.WPF/Converters/Multiply.cs @@ -10,5 +10,6 @@ namespace MathCore.WPF.Converters; [MarkupExtensionReturnType(typeof(Multiply))] public class Multiply(double K) : SimpleDoubleValueConverter(K, (v, k) => v * k, (r, k) => r / k) { + /// Создать преобразователь, умножающий на 1 public Multiply() : this(1) { } } \ No newline at end of file diff --git a/MathCore.WPF/Converters/MultiplyMany.cs b/MathCore.WPF/Converters/MultiplyMany.cs index 9ed32c3..e0e601f 100644 --- a/MathCore.WPF/Converters/MultiplyMany.cs +++ b/MathCore.WPF/Converters/MultiplyMany.cs @@ -1,13 +1,23 @@ using System.Globalization; using System.Windows.Markup; +using System.Windows.Data; using MathCore.WPF.Converters.Base; namespace MathCore.WPF.Converters; +/// Умножает последовательность числовых значений [MarkupExtensionReturnType(typeof(MultiplyMany))] public class MultiplyMany : MultiValueValueConverter { + /// Преобразует массив значений, перемножая их последовательно + /// Входные значения + /// Тип целевого значения + /// Параметр + /// Культура + /// + /// Произведение значений; null если вход null; double.NaN если первый элемент null или один из элементов не может быть конвертирован + /// protected override object? Convert(object?[]? vv, Type? t, object? p, CultureInfo? c) { switch (vv) diff --git a/MathCore.WPF/Converters/NANtoVisibility.cs b/MathCore.WPF/Converters/NANtoVisibility.cs index c18648d..92ee01f 100644 --- a/MathCore.WPF/Converters/NANtoVisibility.cs +++ b/MathCore.WPF/Converters/NANtoVisibility.cs @@ -12,21 +12,31 @@ namespace MathCore.WPF.Converters; +/// Преобразует NaN в Visibility [MarkupExtensionReturnType(typeof(NaNtoVisibility))] [ValueConversion(typeof(double), typeof(Visibility))] public class NaNtoVisibility : ValueConverter { + /// Инвертировать результат преобразования public bool Inverted { get; set; } + /// Использовать Visibility.Collapsed вместо Visibility.Hidden public bool Collapsed { get; set; } private Visibility Hidden => Collapsed ? Visibility.Collapsed : Visibility.Hidden; - /// - protected override object? Convert(object? v, Type? t, object? p, CultureInfo? c) => - v is null - ? null - : Inverted - ? !double.IsNaN((double)v) ? Hidden : Visibility.Visible - : double.IsNaN((double)v) ? Hidden : Visibility.Visible; + /// Преобразует NaN в Visibility + /// Входное значение + /// Тип целевого значения + /// Параметр преобразования + /// Культура + /// Visibility.Visible если значение не NaN (с учётом флага Inverted), иначе Hidden; возвращает null для null входа + protected override object? Convert(object? v, Type? t, object? p, CultureInfo? c) => + v is null + ? null + : v is double d + ? Inverted + ? !double.IsNaN(d) ? Hidden : Visibility.Visible + : double.IsNaN(d) ? Hidden : Visibility.Visible + : Binding.DoNothing; } \ No newline at end of file diff --git a/MathCore.WPF/Converters/Not.cs b/MathCore.WPF/Converters/Not.cs index d25d483..ff11947 100644 --- a/MathCore.WPF/Converters/Not.cs +++ b/MathCore.WPF/Converters/Not.cs @@ -8,13 +8,24 @@ namespace MathCore.WPF.Converters; +/// Инвертирует логическое значение [MarkupExtensionReturnType(typeof(Not))] [ValueConversion(typeof(bool), typeof(bool))] public class Not : ValueConverter { - /// - protected override object? Convert(object? v, Type? t, object? p, CultureInfo? c) => !(bool?) v; + /// Инвертирует логическое значение + /// Входное значение + /// Тип целевого значения + /// Параметр преобразования + /// Культура + /// Инвертированное булево значение или Binding.DoNothing при некорректном входе + protected override object? Convert(object? v, Type? t, object? p, CultureInfo? c) => v is bool b ? !b : Binding.DoNothing; - /// - protected override object? ConvertBack(object? v, Type? t, object? p, CultureInfo? c) => !(bool?)v; + /// Инвертирует логическое значение при обратном преобразовании + /// Входное значение + /// Тип исходного значения + /// Параметр преобразования + /// Культура + /// Инвертированное булево значение или Binding.DoNothing при некорректном входе + protected override object? ConvertBack(object? v, Type? t, object? p, CultureInfo? c) => v is bool b ? !b : Binding.DoNothing; } \ No newline at end of file diff --git a/MathCore.WPF/Converters/Or.cs b/MathCore.WPF/Converters/Or.cs index 7c0b9d6..d565315 100644 --- a/MathCore.WPF/Converters/Or.cs +++ b/MathCore.WPF/Converters/Or.cs @@ -1,5 +1,7 @@ using System.Globalization; +using System.Linq; using System.Windows.Markup; +using System.Windows.Data; using MathCore.WPF.Converters.Base; @@ -7,10 +9,25 @@ namespace MathCore.WPF.Converters; +/// Проверяет, что хотя бы одно из входных значений истинно [MarkupExtensionReturnType(typeof(Or))] public class Or : MultiValueValueConverter { + /// Значение по умолчанию при null входе public bool NullDefaultValue { get; set; } - protected override object? Convert(object[]? vv, Type? t, object? p, CultureInfo? c) => vv?.Cast().Any(v => v) ?? NullDefaultValue; + /// Возвращает true если хотя бы одно значение истинно, Binding.DoNothing при несоответствующих типах + protected override object? Convert(object[]? vv, Type? t, object? p, CultureInfo? c) + { + if (vv is null) return NullDefaultValue; + + var anyTrue = false; + foreach (var item in vv) + { + if (item is not bool b) return Binding.DoNothing; + if (b) { anyTrue = true; break; } + } + + return anyTrue; + } } \ No newline at end of file diff --git a/MathCore.WPF/Converters/OutRange.cs b/MathCore.WPF/Converters/OutRange.cs index 0c5f47e..efb251c 100644 --- a/MathCore.WPF/Converters/OutRange.cs +++ b/MathCore.WPF/Converters/OutRange.cs @@ -9,6 +9,7 @@ namespace MathCore.WPF.Converters; +/// Проверяет, что значение находится вне указанного интервала [MarkupExtensionReturnType(typeof(OutRange))] [ValueConversion(typeof(double), typeof(bool?))] public class OutRange(Interval interval) : DoubleToBool @@ -48,6 +49,6 @@ public bool? IncludeLimits } } - /// - protected override bool? Convert(double v) => v.IsNaN() ? null : !interval.Check(v); + /// Возвращает null при NaN, иначе true если значение вне интервала + protected override bool? Convert(double v) => double.IsNaN(v) ? null : !interval.Check(v); } \ No newline at end of file diff --git a/MathCore.WPF/Converters/Points2PathGeometry.cs b/MathCore.WPF/Converters/Points2PathGeometry.cs index 21fa119..58be678 100644 --- a/MathCore.WPF/Converters/Points2PathGeometry.cs +++ b/MathCore.WPF/Converters/Points2PathGeometry.cs @@ -2,6 +2,7 @@ using System.Windows.Data; using System.Windows.Markup; using System.Windows.Media; +using System.Linq; using MathCore.WPF.Converters.Base; @@ -9,19 +10,17 @@ namespace MathCore.WPF.Converters; +/// Преобразует массив точек в PathGeometry [ValueConversion(typeof(Point[]), typeof(PathGeometry))] [MarkupExtensionReturnType(typeof(Points2PathGeometry))] public class Points2PathGeometry : ValueConverter { - #region IValueConverter Members - + /// Преобразует массив точек в PathGeometry, соединяя точки последовательными отрезками protected override object? Convert(object? v, Type? t, object? p, System.Globalization.CultureInfo? c) => v is Point[] and [var start, .. { Length: > 0 } tail] ? new PathGeometry { - Figures = { new(start, tail.Select(p => new LineSegment(p, true)), false) } + Figures = { new(start, tail.Select(pt => new LineSegment(pt, true)), false) } } : null; - - #endregion } \ No newline at end of file diff --git a/MathCore.WPF/Converters/Range.cs b/MathCore.WPF/Converters/Range.cs index c7d833f..332aadd 100644 --- a/MathCore.WPF/Converters/Range.cs +++ b/MathCore.WPF/Converters/Range.cs @@ -7,9 +7,11 @@ namespace MathCore.WPF.Converters; +/// Создаёт нормализатор значений в пределах заданного интервала [MarkupExtensionReturnType(typeof(Range))] public class Range(Interval interval) : DoubleValueConverter { + /// Создаёт нормализатор значений в пределах заданного интервала public Range() : this(double.NegativeInfinity, double.PositiveInfinity) { } public Range(double MinMax) : this(new(-MinMax, MinMax)) { } @@ -26,6 +28,6 @@ public Range(double Min, double Max) : this(new(Math.Min(Min, Max), Math.Max(Min public bool MaxInclude { get => interval.MaxInclude; set => interval = interval.IncludeMax(value); } - /// + /// Нормализует значение в пределах интервала protected override double Convert(double v, double? p = null) => interval.Normalize(v); } \ No newline at end of file diff --git a/MathCore.WPF/Converters/Reflection/AssemblyCompany.cs b/MathCore.WPF/Converters/Reflection/AssemblyCompany.cs index 58cbea9..04ca352 100644 --- a/MathCore.WPF/Converters/Reflection/AssemblyCompany.cs +++ b/MathCore.WPF/Converters/Reflection/AssemblyCompany.cs @@ -1,8 +1,12 @@ using System.Reflection; using System.Windows.Markup; -// ReSharper disable UnusedType.Global namespace MathCore.WPF.Converters.Reflection; +/// Возвращает компанию авторов сборки [MarkupExtensionReturnType(typeof(AssemblyCompany))] -public class AssemblyCompany() : AssemblyConverter(Attribute(a => a.Company)); \ No newline at end of file +public class AssemblyCompany : AssemblyConverter +{ + /// Инициализирует конвертер, извлекающий значение AssemblyCompanyAttribute.Company + public AssemblyCompany() : base(Attribute(a => a.Company)) { } +} \ No newline at end of file diff --git a/MathCore.WPF/Converters/Reflection/AssemblyConfiguration.cs b/MathCore.WPF/Converters/Reflection/AssemblyConfiguration.cs index 2b5f4eb..3da86f6 100644 --- a/MathCore.WPF/Converters/Reflection/AssemblyConfiguration.cs +++ b/MathCore.WPF/Converters/Reflection/AssemblyConfiguration.cs @@ -1,8 +1,12 @@ using System.Reflection; using System.Windows.Markup; -// ReSharper disable UnusedType.Global namespace MathCore.WPF.Converters.Reflection; -[MarkupExtensionReturnType(typeof(AssemblyCompany))] -public class AssemblyConfiguration() : AssemblyConverter(Attribute(a => a.Configuration)); \ No newline at end of file +/// Возвращает конфигурацию сборки +[MarkupExtensionReturnType(typeof(AssemblyConfiguration))] +public class AssemblyConfiguration : AssemblyConverter +{ + /// Возвращает конфигурацию сборки + public AssemblyConfiguration() : base(Attribute(a => a.Configuration)) { } +} \ No newline at end of file diff --git a/MathCore.WPF/Converters/Reflection/AssemblyConverter.cs b/MathCore.WPF/Converters/Reflection/AssemblyConverter.cs index 4ac4279..e12deb4 100644 --- a/MathCore.WPF/Converters/Reflection/AssemblyConverter.cs +++ b/MathCore.WPF/Converters/Reflection/AssemblyConverter.cs @@ -1,20 +1,23 @@ using System.Globalization; using System.Reflection; +using System.Linq; using System.Windows.Data; +using System.Windows.Markup; using MathCore.WPF.Converters.Base; namespace MathCore.WPF.Converters.Reflection; -[ValueConversion(typeof(Assembly), typeof(object))] +/// Базовый конвертер для извлечения значения из атрибутов сборки public abstract class AssemblyConverter(Func Converter) : ValueConverter { + /// Возвращает функцию получения значения из атрибута сборки protected static Func Attribute(Func Converter) where T : Attribute => asm => { var a = asm.GetCustomAttributes(typeof(T), false).OfType().FirstOrDefault(); return a is null ? null : Converter(a); }; - /// - protected override object? Convert(object? v, Type t, object? p, CultureInfo c) => v is null ? null : Converter((Assembly)v); + /// Преобразует объект Assembly с помощью переданного Converter; возвращает Binding.DoNothing для неподходящих входов + protected override object? Convert(object? v, Type t, object? p, CultureInfo c) => v is Assembly asm ? Converter(asm) : Binding.DoNothing; } \ No newline at end of file diff --git a/MathCore.WPF/Converters/Reflection/AssemblyCopyright.cs b/MathCore.WPF/Converters/Reflection/AssemblyCopyright.cs index 9dd6a1c..f5c638b 100644 --- a/MathCore.WPF/Converters/Reflection/AssemblyCopyright.cs +++ b/MathCore.WPF/Converters/Reflection/AssemblyCopyright.cs @@ -1,8 +1,12 @@ using System.Reflection; using System.Windows.Markup; -// ReSharper disable UnusedType.Global namespace MathCore.WPF.Converters.Reflection; -[MarkupExtensionReturnType(typeof(AssemblyCompany))] -public class AssemblyCopyright() : AssemblyConverter(Attribute(a => a.Copyright)); \ No newline at end of file +/// Возвращает данные об авторских правах сборки +[MarkupExtensionReturnType(typeof(AssemblyCopyright))] +public class AssemblyCopyright : AssemblyConverter +{ + /// Возвращает данные об авторских правах сборки + public AssemblyCopyright() : base(Attribute(a => a.Copyright)) { } +} \ No newline at end of file diff --git a/MathCore.WPF/Converters/Reflection/AssemblyDescription.cs b/MathCore.WPF/Converters/Reflection/AssemblyDescription.cs index a28b465..cb72a42 100644 --- a/MathCore.WPF/Converters/Reflection/AssemblyDescription.cs +++ b/MathCore.WPF/Converters/Reflection/AssemblyDescription.cs @@ -1,8 +1,12 @@ using System.Reflection; using System.Windows.Markup; -// ReSharper disable UnusedType.Global namespace MathCore.WPF.Converters.Reflection; +/// Возвращает описание сборки [MarkupExtensionReturnType(typeof(AssemblyDescription))] -public class AssemblyDescription() : AssemblyConverter(Attribute(a => a.Description)); \ No newline at end of file +public class AssemblyDescription : AssemblyConverter +{ + /// Возвращает описание сборки + public AssemblyDescription() : base(Attribute(a => a.Description)) { } +} \ No newline at end of file diff --git a/MathCore.WPF/Converters/Reflection/AssemblyFileVersion.cs b/MathCore.WPF/Converters/Reflection/AssemblyFileVersion.cs index 318fc66..c605081 100644 --- a/MathCore.WPF/Converters/Reflection/AssemblyFileVersion.cs +++ b/MathCore.WPF/Converters/Reflection/AssemblyFileVersion.cs @@ -1,8 +1,12 @@ using System.Reflection; using System.Windows.Markup; -// ReSharper disable UnusedType.Global namespace MathCore.WPF.Converters.Reflection; +/// Возвращает версию файла сборки [MarkupExtensionReturnType(typeof(AssemblyFileVersion))] -public class AssemblyFileVersion() : AssemblyConverter(Attribute(a => a.Version)); \ No newline at end of file +public class AssemblyFileVersion : AssemblyConverter +{ + /// Возвращает версию файла сборки + public AssemblyFileVersion() : base(Attribute(a => a.Version)) { } +} \ No newline at end of file diff --git a/MathCore.WPF/Converters/Reflection/AssemblyProduct.cs b/MathCore.WPF/Converters/Reflection/AssemblyProduct.cs index 6a4ab26..23a1cf9 100644 --- a/MathCore.WPF/Converters/Reflection/AssemblyProduct.cs +++ b/MathCore.WPF/Converters/Reflection/AssemblyProduct.cs @@ -1,8 +1,12 @@ using System.Reflection; using System.Windows.Markup; -// ReSharper disable UnusedType.Global namespace MathCore.WPF.Converters.Reflection; +/// Возвращает продукт сборки [MarkupExtensionReturnType(typeof(AssemblyProduct))] -public class AssemblyProduct() : AssemblyConverter(Attribute(a => a.Product)); \ No newline at end of file +public class AssemblyProduct : AssemblyConverter +{ + /// Возвращает продукт сборки + public AssemblyProduct() : base(Attribute(a => a.Product)) { } +} \ No newline at end of file diff --git a/MathCore.WPF/Converters/Reflection/AssemblyTime.cs b/MathCore.WPF/Converters/Reflection/AssemblyTime.cs index 490d96a..9b9fd80 100644 --- a/MathCore.WPF/Converters/Reflection/AssemblyTime.cs +++ b/MathCore.WPF/Converters/Reflection/AssemblyTime.cs @@ -1,8 +1,17 @@ using System.IO; using System.Windows.Markup; -// ReSharper disable UnusedType.Global namespace MathCore.WPF.Converters.Reflection; +/// Возвращает время создания файла сборки [MarkupExtensionReturnType(typeof(AssemblyTime))] -public class AssemblyTime() : AssemblyConverter(a => new FileInfo(a.Location).CreationTime); \ No newline at end of file +public class AssemblyTime : AssemblyConverter +{ + /// Возвращает время создания файла сборки + public AssemblyTime() : base(a => + { + var location = a.Location; + return string.IsNullOrEmpty(location) ? null : (object?)new FileInfo(location).CreationTime; + }) + { } +} \ No newline at end of file diff --git a/MathCore.WPF/Converters/Reflection/AssemblyTitle.cs b/MathCore.WPF/Converters/Reflection/AssemblyTitle.cs index 100d721..f7a7187 100644 --- a/MathCore.WPF/Converters/Reflection/AssemblyTitle.cs +++ b/MathCore.WPF/Converters/Reflection/AssemblyTitle.cs @@ -1,8 +1,12 @@ using System.Reflection; using System.Windows.Markup; -// ReSharper disable UnusedType.Global namespace MathCore.WPF.Converters.Reflection; +/// Возвращает заголовок сборки [MarkupExtensionReturnType(typeof(AssemblyTitle))] -public class AssemblyTitle() : AssemblyConverter(Attribute(a => a.Title)); \ No newline at end of file +public class AssemblyTitle : AssemblyConverter +{ + /// Возвращает заголовок сборки + public AssemblyTitle() : base(Attribute(a => a.Title)) { } +} \ No newline at end of file diff --git a/MathCore.WPF/Converters/Reflection/AssemblyTrademark.cs b/MathCore.WPF/Converters/Reflection/AssemblyTrademark.cs index b9c6da1..a21a946 100644 --- a/MathCore.WPF/Converters/Reflection/AssemblyTrademark.cs +++ b/MathCore.WPF/Converters/Reflection/AssemblyTrademark.cs @@ -1,8 +1,12 @@ using System.Reflection; using System.Windows.Markup; -// ReSharper disable UnusedType.Global namespace MathCore.WPF.Converters.Reflection; +/// Возвращает торговую марку сборки [MarkupExtensionReturnType(typeof(AssemblyTrademark))] -public class AssemblyTrademark() : AssemblyConverter(Attribute(a => a.Trademark)); \ No newline at end of file +public class AssemblyTrademark : AssemblyConverter +{ + /// Возвращает торговую марку сборки + public AssemblyTrademark() : base(Attribute(a => a.Trademark)) { } +} \ No newline at end of file diff --git a/MathCore.WPF/Converters/Reflection/AssemblyVersion.cs b/MathCore.WPF/Converters/Reflection/AssemblyVersion.cs index 8cc6a89..a0c03f7 100644 --- a/MathCore.WPF/Converters/Reflection/AssemblyVersion.cs +++ b/MathCore.WPF/Converters/Reflection/AssemblyVersion.cs @@ -1,7 +1,11 @@ using System.Windows.Markup; -// ReSharper disable UnusedType.Global namespace MathCore.WPF.Converters.Reflection; +/// Возвращает версию сборки [MarkupExtensionReturnType(typeof(AssemblyVersion))] -public class AssemblyVersion() : AssemblyConverter(a => a.GetName().Version); \ No newline at end of file +public class AssemblyVersion : AssemblyConverter +{ + /// Возвращает версию сборки + public AssemblyVersion() : base(a => a.GetName().Version) { } +} \ No newline at end of file diff --git a/MathCore.WPF/Converters/Reflection/GetTypeAssembly.cs b/MathCore.WPF/Converters/Reflection/GetTypeAssembly.cs index 068b611..22fc72d 100644 --- a/MathCore.WPF/Converters/Reflection/GetTypeAssembly.cs +++ b/MathCore.WPF/Converters/Reflection/GetTypeAssembly.cs @@ -9,10 +9,11 @@ namespace MathCore.WPF.Converters.Reflection; +/// Возвращает сборку, в которой определён указанный тип [MarkupExtensionReturnType(typeof(GetTypeAssembly))] [ValueConversion(typeof(Type), typeof(Assembly))] public class GetTypeAssembly : ValueConverter { - /// - protected override object? Convert(object? v, Type t, object? p, CultureInfo c) => ((Type)v).Assembly; + /// Возвращает сборку, в которой определён указаный тип + protected override object? Convert(object? v, Type t, object? p, CultureInfo c) => v is Type ty ? ty.Assembly : Binding.DoNothing; } \ No newline at end of file diff --git a/MathCore.WPF/Converters/Round.cs b/MathCore.WPF/Converters/Round.cs index 1c376a6..f54ffd2 100644 --- a/MathCore.WPF/Converters/Round.cs +++ b/MathCore.WPF/Converters/Round.cs @@ -10,10 +10,12 @@ namespace MathCore.WPF.Converters; +/// Округляет вещественное число с заданным количеством знаков [ValueConversion(typeof(double), typeof(double))] [MarkupExtensionReturnType(typeof(Round))] public class Round(int Digits, MidpointRounding Rounding) : DoubleValueConverter { + /// Множитель для масштабирования перед округлением public double K { get; set; } = 1; public Round() : this(0) { } @@ -26,11 +28,18 @@ public Round(int Digits) : this(Digits, default) { } [ConstructorArgument(nameof(Rounding))] public MidpointRounding Rounding { get; set; } = Rounding; - /// - protected override double Convert(double v, double? p = null) => Digits > 0 - ? Math.Round(v * K, Digits) / K - : Math.Round(v * K) / K; - - /// + /// Преобразует значение, округляя его + /// Входное значение + /// Параметр преобразования, приоритетнее входного значения + /// Округлённое значение или NaN при NaN входе + protected override double Convert(double v, double? p = null) + { + if (double.IsNaN(v)) return v; + return Digits >= 0 + ? Math.Round(v * K, Digits, Rounding) / K + : Math.Round(v * K, Rounding) / K; + } + + /// Обратное преобразование просто возвращает значение как есть protected override double ConvertBack(double v, double? p = null) => v; } \ No newline at end of file diff --git a/MathCore.WPF/Converters/RoundAdaptive.cs b/MathCore.WPF/Converters/RoundAdaptive.cs index dc0b535..4d56c34 100644 --- a/MathCore.WPF/Converters/RoundAdaptive.cs +++ b/MathCore.WPF/Converters/RoundAdaptive.cs @@ -11,6 +11,7 @@ namespace MathCore.WPF.Converters; +/// Адаптивное округление числа, выбирающее количество знаков по значению [ValueConversion(typeof(double), typeof(double))] [MarkupExtensionReturnType(typeof(RoundAdaptive))] public class RoundAdaptive(int Digits, MidpointRounding Rounding) : DoubleValueConverter @@ -26,9 +27,20 @@ public RoundAdaptive(int Digits) : this(Digits, default) { } [ConstructorArgument(nameof(Rounding))] public MidpointRounding Rounding { get; set; } = Rounding; - /// - protected override double Convert(double v, double? p = null) => v.RoundAdaptive(Digits); - - /// + /// Преобразует значение с адаптивным округлением + protected override double Convert(double v, double? p = null) + { + if (double.IsNaN(v)) return v; + try + { + return v.RoundAdaptive(Digits); + } + catch + { + return Math.Round(v, Digits, Rounding); + } + } + + /// Обратное преобразование не изменяет значение protected override double ConvertBack(double v, double? p = null) => v; } \ No newline at end of file diff --git a/MathCore.WPF/Converters/SecondsToTimeSpan.cs b/MathCore.WPF/Converters/SecondsToTimeSpan.cs index a10a682..21aa58b 100644 --- a/MathCore.WPF/Converters/SecondsToTimeSpan.cs +++ b/MathCore.WPF/Converters/SecondsToTimeSpan.cs @@ -6,6 +6,7 @@ namespace MathCore.WPF.Converters; +/// Преобразует значение в секунды в TimeSpan и обратно [MarkupExtensionReturnType(typeof(SecondsToTimeSpan))] [ValueConversion(typeof(double), typeof(TimeSpan))] [ValueConversion(typeof(float), typeof(TimeSpan))] @@ -16,8 +17,10 @@ namespace MathCore.WPF.Converters; [ValueConversion(typeof(TimeSpan), typeof(double))] public class SecondsToTimeSpan : ValueConverter { + /// Преобразует числовое значение в TimeSpan или TimeSpan в число секунд protected override object? Convert(object? v, Type t, object? p, CultureInfo c) => v switch { + null => Binding.DoNothing, float x => TimeSpan.FromSeconds(x), double x => TimeSpan.FromSeconds(x), long x => TimeSpan.FromSeconds(x), @@ -25,11 +28,13 @@ public class SecondsToTimeSpan : ValueConverter short x => TimeSpan.FromSeconds(x), byte x => TimeSpan.FromSeconds(x), TimeSpan time => time.TotalSeconds, - _ => throw new InvalidOperationException() + _ => Binding.DoNothing }; + /// Обратное преобразование, идентично Convert protected override object? ConvertBack(object? v, Type t, object? p, CultureInfo c) => v switch { + null => Binding.DoNothing, float x => TimeSpan.FromSeconds(x), double x => TimeSpan.FromSeconds(x), long x => TimeSpan.FromSeconds(x), @@ -37,6 +42,6 @@ public class SecondsToTimeSpan : ValueConverter short x => TimeSpan.FromSeconds(x), byte x => TimeSpan.FromSeconds(x), TimeSpan time => time.TotalSeconds, - _ => throw new InvalidOperationException() + _ => Binding.DoNothing }; } diff --git a/MathCore.WPF/Converters/Sign.cs b/MathCore.WPF/Converters/Sign.cs index 2c26aec..14e430e 100644 --- a/MathCore.WPF/Converters/Sign.cs +++ b/MathCore.WPF/Converters/Sign.cs @@ -7,17 +7,21 @@ namespace MathCore.WPF.Converters; +/// Возвращает знак числа с коэффициентами масштабирования и смещения [ValueConversion(typeof(double), typeof(double))] [MarkupExtensionReturnType(typeof(Sign))] public class Sign : DoubleValueConverter { + /// Масштабный коэффициент для результата public double K { get; set; } = 1; + /// Аддитивное смещение результата public double B { get; set; } = 0; + /// Вес на входе перед вычислением знака public double W { get; set; } = 1; - /// + /// Возвращает NaN для NaN входа, иначе знак входного значения, масштабированный и со смещением protected override double Convert(double v, double? p = null) => double.IsNaN(v) ? v : Math.Sign(W * v) * K + B; /// diff --git a/MathCore.WPF/Converters/SignValue.cs b/MathCore.WPF/Converters/SignValue.cs index 3dd5dfd..2a2df84 100644 --- a/MathCore.WPF/Converters/SignValue.cs +++ b/MathCore.WPF/Converters/SignValue.cs @@ -12,34 +12,49 @@ namespace MathCore.WPF.Converters; +/// Возвращает дискретизированное представление знака значения с порогом [ValueConversion(typeof(double), typeof(double))] [MarkupExtensionReturnType(typeof(SignValue))] public class SignValue : DoubleValueConverter { + /// Масштабный коэффициент результата public double K { get; set; } = 1; + /// Аддитивное смещение результата public double B { get; set; } = 0; + /// Вес применяемый к входному значению перед вычислением знака public double W { get; set; } = 1; + /// Пороговая дельта, при |v| <= Delta возвращается 0 [ConstructorArgument(nameof(Delta))] public double Delta { get; set; } + /// Инвертировать знак результата [ConstructorArgument(nameof(Inverse))] public bool Inverse { get; set; } + /// Инициализация SignValue по умолчанию public SignValue() { } + /// Инициализация SignValue с порогом + /// Пороговая дельта public SignValue(double Delta) => this.Delta = Delta; + /// Инициализация SignValue с флагом инверсии + /// Флаг инверсии public SignValue(bool Inverse) => this.Inverse = Inverse; - protected override double Convert(double v, double? p = null) => - double.IsNaN(v) - ? double.NaN - : Math.Abs(v) <= Delta - ? 0 - : Inverse + /// Преобразует значение, возвращая 0 внутри дельты, NaN для NaN и знак вне дельты + /// Входное значение + /// Параметр преобразования + /// Результат преобразования + protected override double Convert(double v, double? p = null) => + double.IsNaN(v) + ? double.NaN + : Math.Abs(v) <= Delta + ? 0 + : Inverse ? -Math.Sign(W * v) * K + B : Math.Sign(W * v) * K + B; } \ No newline at end of file diff --git a/MathCore.WPF/Converters/Sin.cs b/MathCore.WPF/Converters/Sin.cs index 62cc4fa..6391038 100644 --- a/MathCore.WPF/Converters/Sin.cs +++ b/MathCore.WPF/Converters/Sin.cs @@ -5,17 +5,21 @@ namespace MathCore.WPF.Converters; +/// Преобразует значение по функции синуса с масштабом и смещением [ValueConversion(typeof(double), typeof(double))] [MarkupExtensionReturnType(typeof(Sin))] public class Sin : DoubleValueConverter { + /// Множитель выходного значения public double K { get; set; } = 1; + /// Аддитивное смещение public double B { get; set; } = 0; + /// Частота (коэффициент перед аргументом функции) public double W { get; set; } = Consts.pi2; - /// + /// Преобразует входное значение, возвращает NaN если вход NaN protected override double Convert(double v, double? p = null) => double.IsNaN(v) ? v : Math.Sin(W * v) * K + B; /// diff --git a/MathCore.WPF/Converters/dB.cs b/MathCore.WPF/Converters/dB.cs index a7aaa81..788ffe7 100644 --- a/MathCore.WPF/Converters/dB.cs +++ b/MathCore.WPF/Converters/dB.cs @@ -8,22 +8,25 @@ namespace MathCore.WPF.Converters; +/// Преобразует уровень сигнала между логарифмическим дБ и линейным масштабом [MarkupExtensionReturnType(typeof(dB))] [System.Diagnostics.CodeAnalysis.SuppressMessage("Стиль", "IDE1006:Стили именования", Justification = "<Ожидание>")] // ReSharper disable once InconsistentNaming public class dB : DoubleValueConverter { + /// Использовать формулу для мощности (10*log10) вместо амплитудной (20*log10) public bool ByPower { get; set; } + /// Инвертировать преобразование public bool Invert { get; set; } - /// + /// Преобразует значение в дБ или обратно в зависимости от флага Invert protected override double Convert(double v, double? p = null) => Invert - ? double.IsNaN(v) ? v : ByPower ? Math.Pow(10, v / 10) : Math.Pow(10, v / 20) - : double.IsNaN(v) ? v : ByPower ? 10 * Math.Log10(v) : 20 * Math.Log10(v); + ? (double.IsNaN(v) ? v : (ByPower ? Math.Pow(10, v / 10) : Math.Pow(10, v / 20))) + : (double.IsNaN(v) ? v : (ByPower ? 10 * Math.Log10(v) : 20 * Math.Log10(v))); /// protected override double ConvertBack(double v, double? p = null) => Invert - ? double.IsNaN(v) ? v : ByPower ? 10 * Math.Log10(v) : 20 * Math.Log10(v) - : double.IsNaN(v) ? v : ByPower ? Math.Pow(10, v / 10) : Math.Pow(10, v / 20); + ? (double.IsNaN(v) ? v : (ByPower ? 10 * Math.Log10(v) : 20 * Math.Log10(v))) + : (double.IsNaN(v) ? v : (ByPower ? Math.Pow(10, v / 10) : Math.Pow(10, v / 20))); } \ No newline at end of file From 80c41a28fa57236795d94b2337974f5e82f26e39 Mon Sep 17 00:00:00 2001 From: Infarh Date: Sun, 14 Dec 2025 17:45:50 +0300 Subject: [PATCH 25/56] =?UTF-8?q?=D0=9F=D0=BE=D0=B4=D0=B3=D0=BE=D1=82?= =?UTF-8?q?=D0=BE=D0=B2=D0=BA=D0=B0=20=D0=BA=20=D1=81=D0=BB=D0=B5=D0=B4?= =?UTF-8?q?=D1=83=D1=8E=D1=89=D0=B5=D0=BC=D1=83=20=D1=8D=D1=82=D0=B0=D0=BF?= =?UTF-8?q?=D1=83=20=D1=80=D0=B5=D1=84=D0=B0=D1=82=D0=BE=D1=80=D0=B8=D0=BD?= =?UTF-8?q?=D0=B3=D0=B0=20=D0=BA=D0=BE=D0=BD=D0=B2=D0=B5=D1=80=D1=82=D0=B5?= =?UTF-8?q?=D1=80=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Converters/ConvertersToRefactoring.md | 514 +----------------- .../Converters/ConvertersToRefactoring2.md | 330 +++-------- 2 files changed, 85 insertions(+), 759 deletions(-) diff --git a/MathCore.WPF/Converters/ConvertersToRefactoring.md b/MathCore.WPF/Converters/ConvertersToRefactoring.md index 5513fca..89446f3 100644 --- a/MathCore.WPF/Converters/ConvertersToRefactoring.md +++ b/MathCore.WPF/Converters/ConvertersToRefactoring.md @@ -2,7 +2,9 @@ Дата: 2025-12-14 -Список файлов конвертеров и связанных типов в каталоге `Converters` и его подкаталогах (первичный экспорт) +Статус: Добавление XML‑комментариев на уровне классов завершено; список конвертеров разделён между двумя файлами + +Ниже — алфавитный список конвертеров (первая половина) в каталоге `Converters` и его подкаталогах - MathCore.WPF/Converters/Abs.cs - MathCore.WPF/Converters/Addition.cs @@ -12,31 +14,38 @@ - MathCore.WPF/Converters/ArrayElement.cs - MathCore.WPF/Converters/ArrayToStringConverter.cs - MathCore.WPF/Converters/Arithmetic.cs +- MathCore.WPF/Converters/AsyncConverter.cs - MathCore.WPF/Converters/Average.cs - MathCore.WPF/Converters/AverageMulti.cs -- MathCore.WPF/Converters/Bool2Visibility.cs -- MathCore.WPF/Converters/BoolToBrushConverter.cs - MathCore.WPF/Converters/Base/DoubleToBool.cs +- MathCore.WPF/Converters/Base/DoubleValueConverter.cs - MathCore.WPF/Converters/Base/MultiDoubleValueValueConverter.cs - MathCore.WPF/Converters/Base/MultiValueValueConverter.cs - MathCore.WPF/Converters/Base/SimpleDoubleValueConverter.cs - MathCore.WPF/Converters/Base/ValueConverter.cs -- MathCore.WPF/Converters/CSplineInterp.cs -- MathCore.WPF/Converters/Cos.cs +- MathCore.WPF/Converters/Ctg.cs +- MathCore.WPF/Converters/ColorBrushConverter.cs - MathCore.WPF/Converters/Combine.cs - MathCore.WPF/Converters/CombineMulti.cs - MathCore.WPF/Converters/Composite.cs +- MathCore.WPF/Converters/Cos.cs +- MathCore.WPF/Converters/CSplineInterp.cs - MathCore.WPF/Converters/Custom.cs - MathCore.WPF/Converters/CustomMulti.cs - MathCore.WPF/Converters/DataLengthString.cs -- MathCore.WPF/Converters/DefaultIfNaN.cs - MathCore.WPF/Converters/dB.cs +- MathCore.WPF/Converters/DefaultIfNaN.cs +- MathCore.WPF/Converters/Deviation.cs - MathCore.WPF/Converters/Divide.cs - MathCore.WPF/Converters/DivideMulti.cs -- MathCore.WPF/Converters/Deviation.cs -- MathCore.WPF/Converters/ExpConverter.cs - MathCore.WPF/Converters/ExConverter.cs +- MathCore.WPF/Converters/ExpConverter.cs +- MathCore.WPF/Converters/EnumEqual.cs - MathCore.WPF/Converters/FirstLastItemConverter.cs +- MathCore.WPF/Converters/GetType.cs +- MathCore.WPF/Converters/Reflection/GetTypeAssembly.cs +- MathCore.WPF/Converters/IO/FilePathToName.cs +- MathCore.WPF/Converters/IO/StringToFileInfo.cs - MathCore.WPF/Converters/GreaterThan.cs - MathCore.WPF/Converters/GreaterThanMulti.cs - MathCore.WPF/Converters/GreaterThanOrEqual.cs @@ -45,11 +54,9 @@ - MathCore.WPF/Converters/InRange.cs - MathCore.WPF/Converters/Interpolation.cs - MathCore.WPF/Converters/Inverse.cs -- MathCore.WPF/Converters/IO/FilePathToName.cs -- MathCore.WPF/Converters/IO/StringToFileInfo.cs - MathCore.WPF/Converters/IsNaN.cs -- MathCore.WPF/Converters/IsNull.cs - MathCore.WPF/Converters/IsNegative.cs +- MathCore.WPF/Converters/IsNull.cs - MathCore.WPF/Converters/IsPositive.cs - MathCore.WPF/Converters/JoinStringConverter.cs - MathCore.WPF/Converters/LastItemConverter.cs @@ -107,489 +114,8 @@ - MathCore.WPF/Converters/Truncate.cs - MathCore.WPF/Converters/Trunc.cs - MathCore.WPF/Converters/ValuesToPoint.cs -- MathCore.WPF/Converters/LastItemConverter.cs -- MathCore.WPF/Converters/FirstLastItemConverter.cs -- MathCore.WPF/Converters/GreaterThanMulti.cs -- MathCore.WPF/Converters/LessThanMulti.cs - - -Примечание: это первичный экспорт, файл будет дополняться после ручного обзора каждого файла и проверки публичных членов - ---- - -# Проверка первых файлов - -Ниже отчёт по первым трём файлам, проверенным последовательно: `Abs.cs`, `Not.cs`, `SignValue.cs`. - -## MathCore.WPF/Converters/Abs.cs -- Комментарии: отсутствуют на уровне класса; методы используют `/// ` — требуется явная XML‑документация для публичного типа и свойств/конструкторов -- Логические ошибки: не обнаружено (реализация `Convert` возвращает `Math.Abs(v)`, `ConvertBack` возвращает исходное значение) -- Рекомендации: - - Добавить XML `` для класса и краткие комментарии для методов (русский язык по правилам проекта) - - Рассмотреть добавление проверки входных значений и примера использования в `` - -## MathCore.WPF/Converters/Not.cs -- Комментарии: отсутствуют на уровне класса; методы используют `/// ` — требуется явная XML‑документация для публичного типа -- Логические ошибки: потенциальная проблема с безопасной обработкой `null` и некорректных типов - - Текущее выражение `!(bool?) v` приводит к неочевидному поведению при `v == null` или если `v` не является `bool`/`bool?` - - Желательно явно проверять тип: `if (v is bool b) return !b; if (v is bool? nb) return nb.HasValue ? !nb.Value : (object?)null;` или возвращать `Binding.DoNothing/DependencyProperty.UnsetValue` по проектным соглашениям -- Рекомендации: - - Упростить и обезопасить `Convert`/`ConvertBack`: проверить тип входного значения и явно обработать `null` - - Добавить XML‑комментарии на класс и методы - - Добавить регрессионный тест, покрывающий поведение с `null`, `true`, `false` и некорректными типами - -## MathCore.WPF/Converters/SignValue.cs -- Комментарии: отсутствуют на уровне класса; методы используют `/// ` в базовом классе — требуется явная XML‑документация для публичных свойств и класса -- Логические ошибки: общая логика выглядит корректной, явных ошибок не обнаружено - - Поведение: для `double.NaN` возвращает `double.NaN`, при `Math.Abs(v) <= Delta` возвращает `0`, иначе возвращает `Math.Sign(W * v) * K + B` (инверсное при `Inverse == true`) - - Возможный нюанс: свойства `K`, `W` могут принимать значения 0; это допустимо, но стоит документировать ожидаемые диапазоны и поведение при нулевых/отрицательных значениях - - Атрибуты `ConstructorArgument` присутствуют, но стоит проверить соответствие имен и порядок аргументов при использовании в XAML -- Рекомендации: - - Добавить XML‑документацию для класса и всех публичных свойств (K, B, W, Delta, Inverse) - - Рассмотреть добавление `ConvertBack` или документировать, что он не реализован - - Добавить тесты для пограничных случаев: NaN, ровно Delta, отрицательные/нулевые K/W, Inverse true/false - - ---- - -# Проверка следующих файлов - -Ниже отчёт по следующим трём файлам: `Addition.cs`, `AdditionMulti.cs`, `AggregateArray.cs`. - -## MathCore.WPF/Converters/Addition.cs -- Комментарии: присутствует XML `` у класса — соответствует правилам -- Логические ошибки: не обнаружено, реализация через `SimpleDoubleValueConverter` корректна -- Замечания: - - Отсутствует атрибут `ValueConversion` (для однозначности API можно добавить `[ValueConversion(typeof(double), typeof(double))]`) - - Используется параметр конструктора `P` с PascalCase — соответствует правилам проекта для параметров -- Рекомендации: - - Добавить `ValueConversion` для явного указания типов - - Добавить краткий `` в XML для использования в XAML - -## MathCore.WPF/Converters/AdditionMulti.cs -- Комментарии: отсутствуют — требуется XML‑документация -- Логические ошибки / потенциальные проблемы: - - Первое значение обрабатывается через `vv[0] is double d ? d : System.Convert.ToDouble(vv[0])`. `Convert.ToDouble` может бросать исключение для некорректных типов; для остальных элементов используется безопасный `TryConvertToDouble` - - Непоследовательное поведение при `vv == null` (возвращается `null`) и при наличии `null` элементов (возвращается `double.NaN`) — следует документировать ожидаемое поведение - - Возвращаемые значения смешивают `null` и `double.NaN` — рекомендуется выбрать единообразную стратегию (например, всегда возвращать `double.NaN` при любой недопустимой входной комбинации или `DependencyProperty.UnsetValue`) -- Рекомендации: - - Заменить `Convert.ToDouble(vv[0])` на `DoubleValueConverter.TryConvertToDouble` для единообразия и безопасности - - Добавить XML‑документацию и тесты покрывающие `null`, некорректные типы и смешанные типы - - Документировать поведение для `vv == null` и наличия `null` внутри массива - -## MathCore.WPF/Converters/AggregateArray.cs -- Комментарии: отсутствуют — требуется XML‑документация -- Логические ошибки: не обнаружено; `SelectMany(GetItems)` корректно разворачивает вложенные перечисления -- Замечания: - - Метод `GetItems` пропускает `null` элементы (yield break) — это ожидаемое поведение, но стоит документировать - - `SelectMany` возвращает ленивое перечисление; при необходимости можно материализовать в массив/список -- Рекомендации: - - Добавить XML‑документацию для класса и приватного метода `GetItems` - - Добавить тесты, покрывающие вложенные перечисления, одиночные элементы и `null` - - ---- - -# Проверка следующих файлов - -Ниже отчёт по следующим трём файлам: `And.cs`, `ArrayElement.cs`, `ArrayToStringConverter.cs`. - -## MathCore.WPF/Converters/And.cs -- Комментарии: отсутствуют — требуется XML‑документация для класса и свойства `NullDefaultValue` -- Логические ошибки / потенциальные проблемы: - - Использование `vv?.Cast().All(v => v) ?? NullDefaultValue` приведёт to `InvalidCastException`, если входной массив содержит элементы не типа `bool` или `null` - - При наличии `null` элементов `Cast()` также бросит исключение - - Поведение при `vv == null` возвращает `NullDefaultValue` — это ожидаемо, но должно быть документировано -- Рекомендации: - - Заменить `Cast()` на безопасную проверку: перебирать `vv` и приводить каждый элемент через `is bool b` или через `TryConvert` в зависимости от соглашений проекта - - Добавить XML‑документацию и тесты для сценариев: все `true`, есть `false`, есть `null`, есть некорректные типы, `vv == null` - -## MathCore.WPF/Converters/ArrayElement.cs -- Комментарии: отсутствуют — требуется XML‑документация для класса и конструктора `Index` -- Логические ошибки / потенциальные проблемы: - - Паттерн `(v, p) switch` ожидает `p` типа `int` в ряде веток; если `p` приходит как `string` или `double`, соответствие не сработает и будет возвращено `Binding.DoNothing` - - Для `IEnumerable` используется `items.Cast().ElementAtOrDefault` — это материализует перечисление итеративно; при больших потоках это может быть медленно, но функционально корректно - - Конструктор и свойство `Index` используют одно и то же имя, текущий синтаксис с primary constructor корректен, но стоит убедиться в совместимости TFM и стиля проекта -- Рекомендации: - - Добавить явное преобразование `p` в `int` с `TryParse`/`Convert.ToInt32` + обработкой ошибок, чтобы поддерживать числовые строки и другие числовые типы - - Добавить XML‑документацию и тесты: индекс в диапазоне, индекс вне диапазона, `p` с типом `int` и `string`, входные типы `Array`, `IList`, `IEnumerable`, `null` - -## MathCore.WPF/Converters/ArrayToStringConverter.cs -- Комментарии: отсутствуют — требуется XML‑документация для класса и метода `Convert` -- Логические ошибки / потенциальные проблемы: - - При `v` не `Array` возвращается `Binding.DoNothing` — поведение следует документировать - - Текущая реализация добавляет запятую после каждого элемента и затем удаляет последний символ; корректно, но можно упростить через `string.Join` и учитывать `CultureInfo` при приведении элементов к строке - - Если элементы равны `null`, в итоговой строке появится слово `""` или `""` в зависимости от `ToString` реализации — стоит задокументировать -- Рекомендации: - - Добавить XML‑документацию и тесты для пустых массивов, массивов с `null`, массивов со сложными объектами - - Рассмотреть замену на безопасную реализацию с `string.Join(',', array.Cast().Select(x => x?.ToString() ?? string.Empty))` и учётом `CultureInfo` при форматировании чисел/дат - ---- - -# Проверка следующих файлов - -Ниже отчёт по следующим трём файлам: `Arithmetic.cs`, `Average.cs`, `AverageMulti.cs`. - -## MathCore.WPF/Converters/Arithmetic.cs -- Комментарии: отсутствуют — требуется XML‑документация для класса и методов `Convert`/`ConvertBack` -- Логические ошибки / потенциальные проблемы: - - Регулярное выражение ограничено форматом `([+\-*/]{1,1})\s{0,}(\-?[\d\.]+)` — это не учитывает экспоненциальную нотацию, запятую как десятичный разделитель по локали или пробелы в числе - - Парсинг `p` использует `double.TryParse` без указания `CultureInfo` — может некорректно работать в разных локалях - - Нет явной обработки деления на ноль в `Convert` — при `op == "/"` и `p_value == 0` вернётся `double.PositiveInfinity` или `double.NegativeInfinity`, что может быть допустимо, но лучше документировать - - Атрибут `GeneratedRegex` используется условно для NET7+, это корректно -- Рекомендации: - - Уточнить регулярное выражение или поддержать более гибкий синтаксис чисел (включая экспоненциальную нотацию) - - Передавать `CultureInfo` в `double.TryParse` или использовать `double.TryParse(pattern.Groups[2].Value, NumberStyles.Float, c, out p_value)` - - Добавить XML‑документацию и тесты для всех арифметических операций, включая деление на ноль и некорректные входные строки - -## MathCore.WPF/Converters/Average.cs -- Комментарии: присутствует XML `` и параметр конструктора — соответствует правилам -- Логические ошибки: реализация корректна, использует `AverageValue` из `MathCore.Values` -- Замечания: - - Поле `_Value` инициализируется на основе `Length` конструктора — при вызове конструктора по умолчанию `Length==0`, возможно требования документировать поведение - - Свойство `Length` напрямую меняет внутреннюю длину окна — стоит подтвердить, что `AverageValue.Length` корректно обрабатывает уменьшение/увеличение окна -- Рекомендации: - - Добавить XML‑документацию для свойства `Length` (описано) - - Добавить тесты: последовательное добавление значений, NaN сбрасывает окно, корректность при Length==0 - -## MathCore.WPF/Converters/AverageMulti.cs -- Комментарии: отсутствуют — требуется XML‑документация -- Логические ошибки / потенциальные проблемы: - - Та же проблема, что и в `AdditionMulti`: использование `Convert.ToDouble(vv[0])` может бросать исключение; лучше использовать `TryConvertToDouble` - - Непоследовательное поведение при `vv == null` и `null` элементах — следует унифицировать - - Возвращаемое значение `v / vv.Length` корректно для среднего, но при `vv.Length==0` уже обработано выше; всё ещё стоит документировать -- Рекомендации: - - Использовать `DoubleValueConverter.TryConvertToDouble` для всех элементов - - Добавить XML‑документацию и тесты: пустой массив, содержит `null`, содержит некорректные типы, проверка среднего - ---- - -# Проверка следующих файлов - -Ниже отчёт по следующим трём файлам: `Bool2Visibility.cs`, `BoolToBrushConverter.cs`, `Base/DoubleToBool.cs`. - -## MathCore.WPF/Converters/Bool2Visibility.cs -- Комментарии: отсутствуют — требуется XML‑документация для класса и публичных свойств `Inverted` и `Collapsed` -- Логические ошибки / потенциальные проблемы: - - `Convert` возвращает `null` для `v == null` — более типично возвращать `DependencyProperty.UnsetValue` или `Binding.DoNothing`; следует согласовать стратегию проекта - - `Convert` и `ConvertBack` используют `throw new NotSupportedException()` для неподдерживаемых типов — лучше возвращать `Binding.DoNothing` или `DependencyProperty.UnsetValue` по соглашениям - - В `ConvertBack` для `bool` ветка `bool => v` вернёт объект `v` который не обязательно `bool?`; следует явно вернуть `(bool)v` -- Рекомендации: - - Добавить XML‑документацию и тесты для behavior: null, Visibility input, true/false, Inverted combinations - - Уточнить и унифицировать стратегию возврата при неподдерживаемых типах (`null` vs `Binding.DoNothing`) - -## MathCore.WPF/Converters/BoolToBrushConverter.cs -- Комментарии: отсутствуют — требуется XML‑документация для класса и публичных свойств `TrueColorBrush`, `FalseColorBrush`, `NullColorBrush` -- Логические ошибки / потенциальные проблемы: - - В `Convert` для `null` возвращается `Brushes.Transparent` — это может быть неожиданным; стоит документировать или изменить на возвращение `null`/`Binding.DoNothing` - - `ConvertBack` пытается создать новый `Brush` через `typeof(Brush).GetTypeInfo().Assembly.Location` — это неочевидно и может вызывать вопросы; желательно явно указать причину или упростить до`throw new NotImplementedException()` -- Рекомендации: - - Добавить XML‑документацию и тесты: все комбинации true/false/null, проверки на типы Brush - - Упростить или документировать логику в `ConvertBack` - -## MathCore.WPF/Converters/Base/DoubleToBool.cs -- Комментарии: отсутствуют — требуется XML‑документация для класса и свойств `TrueLimit`, `FalseLimit` -- Логические ошибки / потенциальные проблемы: - - В текущей реализации `Convert` для значений `v` между `TrueLimit` и `FalseLimit` (включительно) будет возвращено `true`, что может быть нежелательным; возможно, требуется изменить логику на строгие сравнения - - `TrueLimit` и `FalseLimit` могут принимать значения по умолчанию (`0`, `1`, `double.NaN`), что может приводить к неожиданному поведению; стоит уточнить в документации -- Рекомендации: - - Добавить XML‑документацию и тесты для пограничных значений, NaN, Infinity, сценариев с нулевыми пределами - - Рассмотреть возможность изменения логики `Convert` на строгие границы истинности - ---- - -# Проверка следующих файлов - -Ниже отчёт по следующим трём файлам: `Base/MultiDoubleValueValueConverter.cs`, `Base/MultiValueValueConverter.cs`, `Base/SimpleDoubleValueConverter.cs`. - -## MathCore.WPF/Converters/Base/MultiDoubleValueValueConverter.cs -- Комментарии: отсутствуют — требуется XML‑документация для класса и публичных свойств `Min` и `Max` -- Логические ошибки / потенциальные проблемы: - - В `Convert` методе при невозможности конвертации элементов `DoubleValueConverter.ConvertToDouble` может бросать исключение; следует рассмотреть использование `TryConvertToDouble` и uniform handling - - После попытки конвертации возвращаемое значение `result` может быть `double.NaN` и затем ограничиваться `Min/Max` — проверять порядок применения ограничений - - В `ConvertBack` при невозможности преобразования возвращается `null`, а при успехе `ConvertBack(...)? .Cast().ToArray()` может вернуть `null`/пустой массив — следует документировать - - Метод `ConvertBack(double v)` вызывает `base.ConvertBack(null, null, null, null);` — это похоже на ошибочный вызов и не нужен -- Рекомендации: - - Заменить `ConvertToDouble` на `TryConvertToDouble` в `Convert` и обработать ошибки единообразно - - Удалить бесполезный вызов `base.ConvertBack(null, null, null, null);` в `ConvertBack(double)` - - Добавить XML‑документацию и тесты - -## MathCore.WPF/Converters/Base/MultiValueValueConverter.cs -- Комментарии: присутствуют XML‑комментарии у публичных членов — соответствует правилам -- Логические ошибки: не обнаружено (реализация шаблонная и корректная) -- Рекомендации: - - Добавить пример использования в `` при необходимости - - Проверить стиль XML‑комментариев по проектным правилам - -## MathCore.WPF/Converters/Base/SimpleDoubleValueConverter.cs -- Комментарии: присутствуют XML‑комментарии — соответствует правилам -- Логические ошибки: не обнаружено; класс реализует шаблон для простых операций -- Рекомендации: - - Добавить тесты для нескольких реализаций (Addition, Subtraction и т.д.) - - Убедиться, что `Parameter` корректно документирован и используется в наследниках - ---- - -# Проверка следующих файлов - -Ниже отчёт по следующим трём файлам: `CSplineInterp.cs`, `Cos.cs`, `Combine.cs`. - -## MathCore.WPF/Converters/CSplineInterp.cs -- Комментарии: отсутствуют — требуется XML‑документация для класса и публичного свойства `Points` -- Логические ошибки / потенциальные проблемы: - - В `ProvideValue` используется `if(Points is null || Points.Count == 0) throw new FormatException();` — лучше использовать конкретную ошибку с описанием или возвращать `Binding.DoNothing` по соглашениям - - Конструктор `CSplineInterp()` использует посыл `this([])` — синтаксис с пустым массивом может не соответствовать целевому TFM; проверить совместимость - - Поля `_SplineTo` и `_SplineFrom` инициализируются в `ProvideValue`, но нет проверки `ProvideValue` была ли вызвана до `Convert` — `Convert` использует `_SplineTo!` с null-forgiving; нужно гарантировать, что `ProvideValue` вызывается до использования или защититься -- Рекомендации: - - Добавить XML‑документацию и тесты, покрывающие пустые значения `Points`, корректное интерполирование и `ConvertBack` - - В `ProvideValue` выбрасывать `ArgumentException` с описанием или возвращать `Binding.DoNothing` по соглашениям - - Добавить защиту в `Convert`/`ConvertBack` на случай, если `ProvideValue` не был вызван (например, ленивую инициализацию) - -## MathCore.WPF/Converters/Cos.cs -- Комментарии: отсутствуют на уровне класса и свойств — требуется XML‑документация -- Логические ошибки: не обнаружено; `Convert` корректно обрабатывает `NaN` и возвращает `Math.Cos(W * v) * K + B` -- Рекомендации: - - Добавить XML‑документацию для класса и свойств `K`, `B`, `W` - - Добавить тесты для NaN, обычных значений и `ConvertBack` - -## MathCore.WPF/Converters/Combine.cs -- Комментарии: присутствуют XML‑комментарии у класса и основных членов — соответствует правилам -- Логические ошибки / потенциальные проблемы: - - В `ConvertBack` секция с `other` повторяется дважды подряд: сначала цикл `for (var i = other.Length - 1; i >= 0; i--) if (other[i] is { } converter) v = converter.ConvertBack(v, t, p, c);` затем почти идентичный цикл с `conv` — это дублирование; вероятно, вторая секция должна быть удалена - - Конструкторы и свойства используют primary constructor синтаксис; проверить совместимость с TFM -- Рекомендации: - - Удалить дублирование в `ConvertBack` и добавить тесты, проверяющие обратную последовательность преобразований - - Добавить `` в XML по использованию в XAML - ---- - -# Проверка следующих файлов - -Ниже отчёт по следующим трём файлам: `CombineMulti.cs`, `Composite.cs`, `Custom.cs`. - -## MathCore.WPF/Converters/CombineMulti.cs -- Комментарии: отсутствуют — требуется XML‑документация для класса и свойств `First`, `Then` -- Логические ошибки / потенциальные проблемы: - - В `Convert` бросается `InvalidOperationException`, если `First` не задан — это соответствует контракту, но лучше документировать поведение и обеспечить более понятное сообщение - - В `ConvertBack` метод при `Then` вызывает `then.ConvertBack(..., v != null ? v.GetType() : typeof(object), p, c)` — второй параметр `Type[]? tt` ожидает массив типов, а передаётся одиночный `Type`, это несоответствие сигнатуре `IMultiValueConverter.ConvertBack` и может привести к ошибкам при выполнении -- Рекомендации: - - Исправить вызов `then.ConvertBack` на корректную сигнатуру или документировать ожидаемую реализацию `then` - - Добавить XML‑документацию и тесты для последовательного комбинирования конвертеров - -## MathCore.WPW/Converters/Composite.cs -- Комментарии: присутствуют частично — требуется XML‑документация для класса (есть атрибут `ContentProperty`) и публичной коллекции `Converters` -- Логические ошибки / потенциальные проблемы: - - Метод `AddChild` использует `default` ветку перед `null` веткой в `switch`, что приведёт к `ArgumentException` вместо `ArgumentNullException` для `null` значений; порядок `case null` должен идти раньше `default` - - Сообщение исключения в `AddChild` использует `$"Объект {value.GetType()} ..."` при `value == null` приведёт к `NullReferenceException` — ещё одна причина обработать `null` заранее -- Рекомендации: - - Переместить `case null` перед `default` и улучшить текст исключения - - Добавить XML‑документацию для класса и методов, а также тесты - -## MathCore.WPW/Converters/Custom.cs -- Комментарии: отсутствуют — требуется XML‑документация для класса и делегатных свойств -- Логические ошибки / потенциальные проблемы: - - Свойства `Forward`, `ForwardParam`, `Backward`, `BackwardParam` могут быть `null` — текущая логика использует `Forward is null ? ForwardParam?.Invoke(v, p) : Forward(v)` что означает, если и `Forward` и `ForwardParam` null, вернётся `null` — документировать ожидаемое поведение - - Использование `Func`-полей в XAML может быть проблематичным — нужно документировать способ назначения этих функций (в коде, не в XAML) -- Рекомендации: - - Добавить XML‑документацию и тесты для всех комбинаций заполнения делегатов - - Рассмотреть изменение сигнатур на `Func` гарантирующие не-null результаты при необходимости - ---- - -# Проверка следующих файлов - -Ниже отчёт по следующим трём файлам: `CustomMulti.cs`, `DataLengthString.cs`, `DefaultIfNaN.cs`. - -## MathCore.WPF/Converters/CustomMulti.cs -- Комментарии: отсутствуют — требуется XML‑документация для класса и делегатных свойств -- Логические ошибки / потенциальные проблемы: - - Аналогично `Custom.cs`, делегаты могут быть `null` — текущая логика вернёт `null`, если оба `Forward` и `ForwardParam` равны `null` - - Использование делегатов в XAML проблематично — задокументировать способ назначения делегатов в коде -- Рекомендации: - - Добавить XML‑документацию и тесты для всех комбинаций делегатных свойств - - Рассмотреть валидацию на уровне ProvideValue или конструкторов - -## MathCore.WPF/Converters/DataLengthString.cs -- Комментарии: отсутствуют — требуется XML‑документация для класса -- Логические ошибки / потенциальные проблемы: - - В `Convert` используется `System.Convert.ToDouble(v)` без проверки `v == null` или проверки типа — это вызовет исключение `FormatException`/`NullReferenceException` если `v` не является числом - - Тип `ValueConversion` указан как `(double, DataLength)`, но `Convert` принимает `object? v` и напрямую конвертирует — лучше использовать `DoubleValueConverter.TryConvertToDouble` для безопасности -- Рекомендации: - - Добавить проверки на `null` и использовать безопасную конвертацию - - Добавить XML‑документацию и тесты: вход `null`, строковые значения, неожиданные типы - -## MathCore.WWP/Converters/DefaultIfNaN.cs -- Комментарии: отсутствуют — требуется XML‑документация для класса и свойства `DefaultValue` -- Логические ошибки / потенциальные проблемы: - - Primary constructor синтаксис `DefaultIfNaN(double DefaultValue)` используется; убедиться в совместимости с TFM - - `Convert` возвращает `v` по умолчанию если не double — возможно лучше возвращать `Binding.DoNothing` или `DependencyProperty.UnsetValue` при некорректном типе -- Рекомендации: - - Добавить XML‑документацию и тесты для NaN, non-double, null - - Рассмотреть явную проверку типа и безопасное приведение - ---- - -# Проверка следующих файлов - -Ниже отчёт по следующим трём файлам: `FirstLastItemConverter.cs`, `GreaterThan.cs`, `GreaterThanMulti.cs`. - -## MathCore.WPW/Converters/FirstLastItemConverter.cs -- Комментарии: отсутствуют — требуется XML‑документация для класса `FirstItemConverter` -- Логические ошибки / потенциальные проблемы: - - `GetFirstValue` использует ручную работу с `IEnumerator` и корректно освобождает ресурс в `finally`; можно упростить через `foreach` и `yield return`, но поведение корректно - - В `Convert` используются pattern matching и `Binding.DoNothing` для неподдерживаемых типов — поведение следует документировать -- Рекомендации: - - Добавить XML‑документацию и тесты: пустые коллекции, массивы, IList, IEnumerable - - Рассмотреть упрощение `GetFirstValue` через `foreach` для читабельности - -## MathCore.WPW/Converters/GreaterThan.cs -- Комментарии: отсутствуют — требуется XML‑документация для класса и свойства `Value` -- Логические ошибки: не обнаружено; реализация через `DoubleToBool` корректна -- Рекомендации: - - Добавить XML‑документацию и тесты для NaN, значения равные порогу, больше/меньше порога - -## MathCore.WPW/Converters/GreaterThanMulti.cs -- Комментарии: отсутствуют — требуется XML‑документация -- Логические ошибки: не обнаружено; метод правильно проверяет, что все последующие значения меньше первого -- Рекомендации: - - Добавить XML‑документацию и тесты для различных комбинаций значений, NaN, null - ---- - -# Проверка следующих файлов - -Ниже отчёт по следующим трём файлам: `GreaterThanOrEqual.cs`, `GreaterOrEqualThanMulti.cs`, `InIntervalValue.cs`. - -## MathCore.WPW/Converters/GreaterThanOrEqual.cs -- Комментарии: отсутствуют — требуется XML‑документация для класса и свойства `Value` -- Логические ошибки: не обнаружено; реализация через `DoubleToBool` корректна -- Рекомендации: - - Добавить XML‑документацию и тесты для NaN, значения равные порогу, больше/меньше порога - -## MathCore.WPW/Converters/GreaterOrEqualThanMulti.cs -- Комментарии: отсутствуют — требуется XML‑документация -- Логические ошибки: не обнаружено; метод правильно проверяет, что все последующие значения не больше первого -- Рекомендации: - - Добавить XML‑документацию и тесты для различных комбинаций значений, NaN, null - -## MathCore.WPW/Converters/InIntervalValue.cs -- Комментарии: отсутствуют — требуется XML‑документация для класса и конструктора `Interval` -- Логические ошибки / потенциальные проблемы: - - Primary constructor синтаксис `InIntervalValue(Interval interval)` используется; убедиться в совместимости с TFM - - Свойства `Min` и `Max` используют `ConstructorArgument` с именем `Min`/`Max` но обращаются к `interval` — порядок и имена аргументов нужно проверить - - Использование `is not double.NaN and var value` в `Convert` может быть семантически неверным — `double.NaN` сравнением не работает; лучше использовать `double.IsNaN` для проверки NaN -- Рекомендации: - - Заменить `is not double.NaN` на `!double.IsNaN(...)` - - Добавить XML‑документацию и тесты на нормализацию, NaN и граничные условия - ---- - -# Проверка следующих файлов - -Ниже отчёт по следующим трём файлам: `InRange.cs`, `Interpolation.cs`, `Inverse.cs`. - -## MathCore.WPW/Converters/InRange.cs -- Комментарии: отсутствуют — требуется XML‑документация для класса и свойств `Min`, `Max`, `MinInclude`, `MaxInclude` -- Логические ошибки / потенциальные проблемы: - - Primary constructor синтаксис `InRange(Interval interval)` используется; проверить совместимость с TFM - - Свойства `Min`/`Max` используют `ConstructorArgument` с именем `Min`/`Max` но обращаются к `interval` — нужно проверить соответствие имен и аргументов - - Метод `Convert` использует `v.IsNaN()` — если это расширение, убедиться, что оно присутствует; альтернативно использовать `double.IsNaN(v)` -- Рекомендации: - - Добавить XML‑документацию и тесты - - Убедиться в наличии расширения `IsNaN()` или заменить на `double.IsNaN` для совместимости - -## MathCore.WPW/Converters/Interpolation.cs -- Комментарии: отсутствуют — требуется XML‑документация для класса и свойства `Points` -- Логические ошибки / потенциальные проблемы: - - В `Convert` используется `_Polynom!.Value(v)` с null-forgiving; если `Points` не были установлены, это приведёт к NRE. Надо либо лениво инициализировать, либо возвращать `Binding.DoNothing`/`double.NaN` - - Конструктор `PointCollection` инициализация в сеттере использует `value.Select` — если `value` пустая коллекция, возможна ошибка при создании полинома (деление на ноль). Документировать ожидаемое поведение -- Рекомендации: - - Добавить проверку `_Polynom` в `Convert` и возврат `double.NaN` или `Binding.DoNothing`, если не инициализирован - - Добавить XML‑документацию и тесты для пустых/неполных`Points` - -## MathCore.WPW/Converters/Inverse.cs -- Комментарии: отсутствуют — требуется XML‑документация для класса -- Логические ошибки / потенциальные проблемы: - - Attribute `[MarkupExtensionReturnType(typeof(double))]` вероятно неверен — должен быть `typeof(Inverse)` или `typeof(IValueConverter)` - - Деление `1 / v` не защищено от `v == 0` — вернётся ±Infinity или NaN при v==0; документировать или обрабатывать -- Рекомендации: - - Исправить `MarkupExtensionReturnType` на `typeof(Inverse)` - - Добавить XML‑документацию и тесты; рассмотреть обработку нуля или документировать поведение - ---- - -# Проверка следующих файлов - -Ниже отчёт по следующим трём файлам: `IsNull.cs`, `IsNegative.cs`, `IsPositive.cs`. - -## MathCore.WWP/Converters/IsNull.cs -- Комментарии: отсутствуют — требуется XML‑документация для класса и свойства `Inverted` -- Логические ошибки / потенциальные проблемы: - - Используется primary constructor синтаксис `IsNull(bool Inverted)`; проверить совместимость с TFM - - `Convert` возвращает `Inverted ^ (v is null)` — корректно, но возвращаемый тип метода `Convert` в `ValueConverter` ожидает `object?` — результат `bool` подойдёт -- Рекомендации: - - Добавить XML‑документацию и тесты - - Рассмотреть замену синтаксиса на обычный класс с конструктором, если совместимость вызывает сомнения - -## MathCore.WPF/Converters/IsNegative.cs -- Комментарии: отсутствуют — требуется XML‑документация для класса -- Логические ошибки / потенциальные проблемы: - - Используется `v.IsNaN()` — проверить наличие расширения. Рекомендуется `double.IsNaN(v)` для ясности - - Возвращаемый тип `bool?` корректен при `DoubleToBool` контракте -- Рекомендации: - - Заменить на `double.IsNaN(v)` если нет расширения - - Добавить XML‑документацию и тесты - -## MathCore.WPF/Converters/IsPositive.cs -- Комментарии: отсутствуют — требуется XML‑документация для класса -- Логические ошибки / потенциальные проблемы: - - В `Convert` используется `v is double.NaN` — это всегда false. Нужно использовать `double.IsNaN(v)` - - Исправить поведение для NaN: возвращать `null` при NaN -- Рекомендации: - - Исправить `v is double.NaN` на `double.IsNaN(v)` - - Добавить XML‑документацию и тесты для NaN, положительных и отрицательных значений - ---- - -# Проверка следующих файлов - -Ниже отчёт по следующим трём файлам: `JoinStringConverter.cs`, `LastItemConverter.cs`, `LessThan.cs`. - -## MathCore.WPF/Converters/JoinStringConverter.cs -- Комментарии: отсутствуют — требуется XML‑документация для класса и методов -- Логические ошибки / потенциальные проблемы: - - Используются target‑typed элементы `TargetType`/`TargetTypes` с PascalCase для параметров — в проекте параметры методов обычно PascalCase, но стоит убедиться в стиле - - В `Convert` `string.Join(separator, values)` вызовет `ToString()` для каждого элемента и объединит `null` как пустую строку; ожидаемо - - В `ConvertBack` использованы сокращённые/невалидные синтаксические конструкции и неверный вызов `Split` с массивом-сепараторов — текущее выражение не скомпилируется - - Метод `ConvertBack` должен возвращать `object[]?`; корректная реализация: `return str.Split(new[] { separator }, StringSplitOptions.None).Cast().ToArray();` -- Рекомендации: - - Исправить `ConvertBack` на корректный синтаксис и добавить защиту от `null` - - Добавить XML‑документацию и тесты - -## MathCore.WPF/Converters/LastItemConverter.cs -- Комментарии: присутствуют атрибуты `MarkupExtensionReturnType` и использование pattern matching — соответствует стилю -- Логические ошибки / потенциальные проблемы: - - Использование ручного `IEnumerator` и `finally` корректно и безопасно, но можно упростить через `foreach`/LINQ если требуется - - Pattern matching `IList and [.., var last]` использует range pattern — проверить совместимость с целевыми TFM -- Рекомендации: - - Добавить XML‑документацию и тесты: пустые коллекции, `IList`, `Array`, `IEnumerable` - -## MathCore.WPF/Converters/LessThan.cs -- Комментарии: отсутствуют — требуется XML‑документация -- Логические ошибки / потенциальные проблемы: - - В `Convert` используется `v is double.NaN` — это всегда `false`; заменить на `double.IsNaN(v)` и возвращать `null` при NaN -- Рекомендации: - - Исправить проверку NaN - - Добавить XML‑документацию и тесты - ---- - -Осталось 51 файл(ов) для обработки. --- -# Продолжение отчёта — перенос в `ConvertersToRefactoring2.md` - -Для уменьшения риска потери контекста и чтобы держать файл `ConvertersToRefactoring.md` компактным, дальнейшие отчёты по файлам будут записываться в `ConvertersToRefactoring2.md`. Оригинальный файл сохранён и не меняется. - -С этого момента продолжу анализ и запись отчётов в `MathCore.WPF/Converters/ConvertersToRefactoring2.md`. +# Примечание +Полный алфавитный перечень конвертеров разбит на два файла: `ConvertersToRefactoring.md` (эта половина) и `ConvertersToRefactoring2.md` (вторая половина) diff --git a/MathCore.WPF/Converters/ConvertersToRefactoring2.md b/MathCore.WPF/Converters/ConvertersToRefactoring2.md index 19fb329..86c6185 100644 --- a/MathCore.WPF/Converters/ConvertersToRefactoring2.md +++ b/MathCore.WPF/Converters/ConvertersToRefactoring2.md @@ -1,275 +1,75 @@ --- -# Converters to refactoring — продолжение +# Converters to refactoring — завершение Дата: 2025-12-14 -Файл продолжения для отчётов по конвертерам. Здесь ведётся дальнейшая пофайловая проверка и рекомендации. +Статус: Добавление class‑level XML‑комментариев в конвертеры завершено + +Кратко: на этапе добавлены XML `` для публичных классов в каталоге `MathCore.WPF/Converters` и подкаталогах; соответствующие пункты о необходимости добавления комментариев удалены из списков задач + +Изменённые файлы (вторая половина): +- MathCore.WPF/Converters/JoinStringConverter.cs +- MathCore.WPF/Converters/LastItemConverter.cs +- MathCore.WPF/Converters/LessOrEqualThanMulti.cs +- MathCore.WPF/Converters/LessThan.cs +- MathCore.WPF/Converters/LessThanMulti.cs +- MathCore.WPF/Converters/LessThanOrEqual.cs +- MathCore.WPF/Converters/Linear.cs +- MathCore.WPF/Converters/Lambda.cs +- MathCore.WPF/Converters/LambdaConverter.cs +- MathCore.WPF/Converters/LambdaMulti.cs +- MathCore.WPF/Converters/Mapper.cs +- MathCore.WPF/Converters/MaxValue.cs +- MathCore.WPF/Converters/MinValue.cs +- MathCore.WPF/Converters/Mod.cs +- MathCore.WPF/Converters/Multiply.cs +- MathCore.WPF/Converters/MultiplyMany.cs +- MathCore.WPF/Converters/MultiValuesToCompositeCollection.cs +- MathCore.WPF/Converters/MultiValuesToEnumerable.cs +- MathCore.WPF/Converters/NANtoVisibility.cs +- MathCore.WPF/Converters/Null2Visibility.cs +- MathCore.WPF/Converters/Not.cs +- MathCore.WPF/Converters/OutRange.cs +- MathCore.WPF/Converters/Or.cs +- MathCore.WPF/Converters/Points2PathGeometry.cs +- MathCore.WPF/Converters/Range.cs +- MathCore.WPF/Converters/Reflection/AssemblyCompany.cs +- MathCore.WPF/Converters/Reflection/AssemblyConfiguration.cs +- MathCore.WPF/Converters/Reflection/AssemblyConverter.cs +- MathCore.WPF/Converters/Reflection/AssemblyCopyright.cs +- MathCore.WPF/Converters/Reflection/AssemblyDescription.cs +- MathCore.WPF/Converters/Reflection/AssemblyFileVersion.cs +- MathCore.WPF/Converters/Reflection/AssemblyProduct.cs +- MathCore.WPF/Converters/Reflection/AssemblyTime.cs +- MathCore.WPF/Converters/Reflection/AssemblyTitle.cs +- MathCore.WPF/Converters/Reflection/AssemblyTrademark.cs +- MathCore.WPF/Converters/Reflection/AssemblyVersion.cs +- MathCore.WPF/Converters/Round.cs +- MathCore.WPF/Converters/RoundAdaptive.cs +- MathCore.WPF/Converters/Sign.cs +- MathCore.WPF/Converters/SignValue.cs +- MathCore.WPF/Converters/Sin.cs +- MathCore.WPF/Converters/SecondsToTimeSpan.cs +- MathCore.WPF/Converters/SingleValue.cs +- MathCore.WPF/Converters/SwitchConverter.cs +- MathCore.WPF/Converters/SwitchConverter2.cs +- MathCore.WPF/Converters/StringConverters/ToLower.cs +- MathCore.WPF/Converters/StringConverters/ToUpper.cs +- MathCore.WPF/Converters/Subtraction.cs +- MathCore.WPF/Converters/SubtractionMulti.cs +- MathCore.WPF/Converters/TemperatureC2F.cs +- MathCore.WPF/Converters/TemperatureF2C.cs +- MathCore.WPF/Converters/Tan.cs +- MathCore.WPF/Converters/TimeDifferential.cs +- MathCore.WPF/Converters/ToString.cs +- MathCore.WPF/Converters/Truncate.cs +- MathCore.WPF/Converters/Trunc.cs +- MathCore.WPF/Converters/ValuesToPoint.cs --- -# Текущее состояние - -Перенос начат — все новые записи о проверке файлов будут добавляться в этот файл. Оригинальный `ConvertersToRefactoring.md` сохранён. - ---- - -# Проверка следующих файлов (продолжение) - -Ниже отчёт по следующим трём файлам: `LessThanMulti.cs`, `LessThanOrEqual.cs`, `LessOrEqualThanMulti.cs`. - -## MathCore.WPF/Converters/LessThanMulti.cs -- Комментарии: отсутствуют — требуется XML‑документация -- Логические ошибки: не обнаружено; реализация корректно сравнивает каждое последующее значение с первым и возвращает `true`, если все > первого -- Рекомендации: - - Добавить XML‑документацию и тесты для различных комбинаций (включая NaN и null) - -## MathCore.WPF/Converters/LessThanOrEqual.cs -- Комментарии: отсутствуют — требуется XML‑документация -- Логические ошибки / потенциальные проблемы: - - В `Convert` используется `v is double.NaN` — всегда `false`; заменить на `double.IsNaN(v)` и при NaN возвращать `null` -- Рекомендации: - - Исправить проверку NaN - - Добавить XML‑документацию и тесты - -## MathCore.WPF/Converters/LessOrEqualThanMulti.cs -- Комментарии: отсутствуют — требуется XML‑документация -- Логические ошибки: не обнаружено; реализация корректна и симметрична `GreaterOrEqualThanMulti` -- Рекомендации: - - Добавить XML‑документацию и тесты - ---- - -# Проверка следующих файлов (продолжение) - -Ниже отчёт по следующим трём файлам: `Linear.cs`, `Lambda.cs`, `LambdaConverter.cs`. - -## MathCore.WPF/Converters/Linear.cs -- Комментарии: класс имеет XML `` и отдельные `` для свойств — соответствует правилам -- Логические ошибки / потенциальные проблемы: - - Primary constructor синтаксис `Linear(double K, double B)` используется; проверить совместимость с TFM - - Свойство `K` используется в `From` делении `(x - b) / k` — возможное деление на ноль, следует документировать или защищать - - `ConvertBack` и `Convert` опираются на `Inverted` флаг — поведение корректно, но задокументировать -- Рекомендации: - - Добавить защиту при `K == 0` в `From` или документировать, что при K=0 будет Infinity/Exception - - Добавить тесты и примеры использования - -## MathCore.WPF/Converters/Lambda.cs -- Комментарии: отсутствуют — требуется XML‑документация для общих типов и делегатов -- Логические ошибки / потенциальные проблемы: - - Используется primary constructor для generic типа `Lambda(...)` — проверить совместимость с TFM - - Поля `_Converter` и `_BackConverter` инициализируются через переданные делегаты, но `BackConverter` может быть `null` — обработка через `throw new NotSupportedException()` корректна - - В `Convert`/`ConvertBack` выполняется приведение `(TValue)v` / `(TResult)v` без проверки типа — может бросить `InvalidCastException` для некорректных входных значений -- Рекомендации: - - Добавить проверки типов перед приведением: `if (v is TValue value) ...` и возвращать `Binding.DoNothing`/`null` по соглашению - - Добавить XML‑документацию и тесты - -## MathCore.WPF/Converters/LambdaConverter.cs -- Комментарии: отсутствуют — требуется XML‑документация -- Логические ошибки / потенциальные проблемы: - - Primary constructor синтаксис `LambdaConverter(LambdaConverter.Converter To, ...)` — проверить совместимость с TFM - - В `ConvertBack` при отсутствии `From` бросается `NotSupportedException` с сообщением на русском — это согласуется с проектными правилами по локализованным сообщениям -- Рекомендации: - - Добавить XML‑документацию и тесты - ---- - -# Проверка следующих файлов (продолжение) - -Ниже отчёт по следующим трём файлам: `Mapper.cs`, `MaxValue.cs`. - -## MathCore.WPF/Converters/Mapper.cs -- Комментарии: отсутствуют — требуется XML‑документация для класса и всех публичных свойств -- Логические ошибки / потенциальные проблемы: - - В сеттерах свойств `_k` пересчитывается как `(_MaxScale - _MinScale) / (_MaxValue - _MinValue)` без защиты от деления на ноль (когда `_MaxValue == _MinValue`) - - При изменении любого из четырёх свойств пересчёт `_k` происходит корректно, но начальные значения приводят к делению на ноль если `_MaxValue == _MinValue` - - Нет валидации входных значений (например, Min > Max) -- Рекомендации: - - Добавить защиту при пересчёте `_k`: если `_MaxValue == _MinValue` тогда `_k = 0` или бросать `ArgumentException` - - Добавить XML‑документацию и тесты; документировать поведение при вырожденном диапазоне - -## MathCore.WPF/Converters/MaxValue.cs -- Комментарии: отсутствуют — требуется XML‑документация для класса -- Логические ошибки / потенциальные проблемы: - - Метод `Convert(object[]? vv, ...)` возвращает `null` при пустом массиве; вероятно более согласованно возвращать `Binding.DoNothing` или `null` в зависимости от проекта - - Используется `vv.Max()` без `Cast`/`Comparer`, что потребует компаратор или элементы должны быть сравнимы; для смешанных типов это вызовет исключение - - Дuplicated `ConvertBack` methods are declared twice (for IMultiValueConverter and IValueConverter) — both throw `NotSupportedException`, this is correct -- Рекомендации: - - Добавить документацию и тесты; задокументировать ожидаемые типы элементов входного массива - - Рассмотреть использование `vv.Cast().Max()` с проверками или возвращение `Binding.DoNothing` при несравнимых типах - ---- - -# Проверка следующих файлов (продолжение) - -Ниже отчёт по следующим трём файлам: `MinValue.cs`, `Mod.cs`, `Multiply.cs`. - -## MathCore.WPF/Converters/MinValue.cs -- Комментарии: отсутствуют — требует XML‑документация для класса -- Логические ошибки / потенциальные проблемы: - - `Convert(object[]? vv, ...)` возвращает `vv?.Min()` — это может привести to исключению при несравнимых типах; необходимо документировать ожидаемые типы элементов - - Возвращаемое значение для `null`/пустого ввода — `null` — нужно задокументировать -- Рекомендации: - - Добавить XML‑документацию и тесты; рассмотреть валидацию типов перед вызовом `Min()` - -## MathCore.WPF/Converters/Mod.cs -- Комментарии: отсутствуют — требуется XML‑документация -- Логические ошибки / потенциальные проблемы: - - Используется primary constructor синтаксис `Mod(double M)`; проверить совместимость с TFM - - В `Convert` используются `IsNaN()` расширения — проверить наличие расширения или заменить на `double.IsNaN(...)` - - Поведение при `M == NaN` возвращает `p ?? v` — возможно неожиданно; документировать - - Деление по модулю `(p ?? v) % M` разыменовывается при `M == 0` — оператор `%` с нулём приведёт к `DivideByZeroException`? В C# `%` с double и 0 возвращает NaN или Infinity? Нужна проверка и документация -- Рекомендации: - - Заменить проверки `IsNaN()` на `double.IsNaN` при отсутствии расширения - - Добавить защиту/документацию для `M == 0` - - Добавить XML‑документацию и тесты - -## MathCore.WPF/Converters/Multiply.cs -- Комментарии: класс имеет `` — соответствует требованиям -- Логические ошибки: не обнаружено; класс корректно использует `SimpleDoubleValueConverter` -- Рекомендации: - - Добавить тесты для поведения при K==0 и NaN - ---- - -# Проверка следующих файлов (продолжение) - -Ниже отчёт по следующим трём файлам: `MultiplyMany.cs`, `MultiValuesToCompositeCollection.cs`, `MultiValuesToEnumerable.cs`. - -## MathCore.WPF/Converters/MultiplyMany.cs -- Комментарии: отсутствуют — требуется XML‑документация -- Логические ошибки / потенциальные проблемы: - - Поведение при `vv == null` возвращает `null`, при `vv == [null]` возвращает `double.NaN` — несогласованность, следует унифицировать - - Используется `DoubleValueConverter.TryConvertToDouble` — это правильно - - Возврат `double.NaN` для некорректных элементов документировать -- Рекомендации: - - Уточнить стратегию возврата при `null` и `null` элементах - - Добавить XML‑документацию и тесты - -## MathCore.WPF/Converters/MultiValuesToCompositeCollection.cs -- Комментарии: отсутствуют — требуется XML‑документация -- Логические ошибки: не обнаружено; корректно преобразует последовательности в `CompositeCollection` -- Рекомендации: - - Добавить XML‑документацию и тесты - -## MathCore.WPF/Converters/MultiValuesToEnumerable.cs -- Комментарии: отсутствуют — требуется XML‑документация -- Логические ошибки / потенциальные проблемы: - - В `ConvertBack` используется `tt!` и `ToArray()!` — потенциальные NRE; следует предварительно проверять `tt` и `v` - - `Zip(tt!, System.Convert.ChangeType)` ожидает `tt` длину соответствующую `IEnumerable` — возможны ошибки при несовпадении длин - - Поведение `Convert` возвращает `vv` как есть — документировать ожидания -- Рекомендации: - - Добавить проверки `tt` и `v` в `ConvertBack` и возвращать `null`/`Binding.DoNothing` при несовпадении - - Добавить XML‑документацию и тесты - ---- - -# Проверка следующих файлов (продолжение) - -Ниже отчёт по следующим трём файлам: `NaNtoVisibility.cs` (файл назван NANtoVisibility.cs), `Null2Visibility.cs`, `Not.cs`. - -## MathCore.WPF/Converters/NaNtoVisibility.cs -- Комментарии: отсутствуют — требуется XML‑документация для класса и свойств `Inverted`, `Collapsed` -- Логические ошибки / потенциальные проблемы: - - Атрибут `MarkupExtensionReturnType(typeof(NaNtoVisibility))` соответствует имени класса, но имя файла начинается с `NANtoVisibility.cs` — привести к единому стилю - - `Convert` приводит `v` к `(double)v` без проверки типа — может бросить `InvalidCastException` если `v` не `double` - - Возвращает `null` для `v == null` — лучше возвращать `DependencyProperty.UnsetValue` или `Binding.DoNothing` по соглашению -- Рекомендации: - - Добавить проверку типов: `if (v is double d) ...` или использовать `DoubleValueConverter.TryConvertToDouble` - - Добавить XML‑документацию и тесты - -## MathCore.WPF/Converters/Null2Visibility.cs -- Комментарии: отсутствуют — требуется XML‑документация -- Логические ошибки: не обнаружено; логика корректно возвращает `Visibility` в зависимости от `Inverted` и `Collapsed` -- Рекомендации: - - Задокументировать поведение и добавить тесты - -## MathCore.WPF/Converters/Not.cs -- Комментарии: отсутствуют — требуется XML‑документация -- Логические ошибки / потенциальные проблемы: - - Текущее выражение `!(bool?) v` может дать непредсказуемое поведение при `v == null` или при `v` не являющемся `bool` — лучше явно проверять: `v is bool b ? !b : Binding.DoNothing` или возвращать `null` - - `ConvertBack` также использует `!(bool?)v` — аналогично -- Рекомендации: - - Исправить обработку `null`/некорректных типов - - Добавить XML‑документацию и тесты - ---- - -# Проверка следующих файлов (продолжение) - -Ниже отчёт по следующим трём файлам: `OutRange.cs`, `Or.cs`, `Points2PathGeometry.cs`. - -## MathCore.WPF/Converters/OutRange.cs -- Комментарии: отсутствуют — требуется XML‑документация для класса и свойств -- Логические ошибки / потенциальные проблемы: - - Используется `interval` из primary constructor `OutRange(Interval interval)` — проверить совместимость синтаксиса и наличие типа `Interval` - - В `Convert` используется `v.IsNaN()` — заменить на `double.IsNaN(v)` если расширение отсутствует - - Логика `IncludeLimits` устанавливает оба флага, но не обрабатывает `null` явно — текущая реализация корректна -- Рекомендации: - - Добавить защиту на случай отсутствия `interval` и документацию - - Заменить `IsNaN()` на `double.IsNaN` при необходимости - -## MathCore.WPF/Converters/Or.cs -- Комментарии: отсутствуют — требуется XML‑документация -- Логические ошибки / потенциальные проблемы: - - Используется `vv?.Cast().Any(v => v) ?? NullDefaultValue` — приведёт to `InvalidCastException` при наличии `null` или несоответствующих типов. Лучше безопасно перебирать и проверять `is bool b` -- Рекомендации: - - Заменить `Cast()` на безопасную проверку и добавить тесты - - Добавить XML‑документацию - -## MathCore.WPF/Converters/Points2PathGeometry.cs -- Комментарии: отсутствуют — требуется XML‑документация -- Логические ошибки / потенциальные проблемы: - - Используется pattern matching `Point[] and [var start, .. { Length: > 0 } tail]` и `new PathGeometry { Figures = { new(start, tail.Select(...), false) } }` — компактно, но проверить совместимость TFM - - Возвращает `null` для неподдерживаемых типов — задокументировать -- Рекомендации: - - Добавить XML‑документацию и тесты - ---- - -# Проверка следующих файлов (продолжение) - -Ниже отчёт по следующим трём файлам: `Range.cs` и набору файлов `Reflection/*` - -## MathCore.WPF/Converters/Range.cs -- Комментарии: отсутствуют — требуется XML‑документация для класса и публичных свойств -- Логические ошибки / потенциальные проблемы: - - Используется primary constructor `Range(Interval interval)`; проверить совместимость синтаксиса и наличие типа `Interval` - - Свойства `Min`/`Max` используют `ConstructorArgument` и обращаются к `interval` — проверить порядок аргументов - - `Convert` возвращает `interval.Normalize(v)` — предположительно корректно, но проверить реализацию `Normalize` на NaN/Infinity -- Рекомендации: - - Добавить XML‑документацию и тесты - - Проверить `Interval` API для корректной работы со значениями NaN/Infinity - -## MathCore.WPF/Converters/Reflection/* -- Файлы: `AssemblyCompany.cs`, `AssemblyConfiguration.cs`, `AssemblyConverter.cs`, `AssemblyCopyright.cs`, - `AssemblyDescription.cs`, `AssemblyFileVersion.cs`, `AssemblyProduct.cs`, `AssemblyTime.cs`, `AssemblyTitle.cs`, - `AssemblyTrademark.cs`, `AssemblyVersion.cs`, `GetTypeAssembly.cs` -- Комментарии: отсутствуют XML‑комментарии в большинстве файлов — требуется добавить для публичных типов -- Логические ошибки / потенциальные проблемы: - - `AssemblyConverter` использует `Converter((Assembly)v)` в `Convert` без проверки типа — привести к безопасной обработке `v is Assembly asm ? Converter(asm) : null` - - В некоторых файлах атрибут `MarkupExtensionReturnType` указывает неверный тип (например, `AssemblyConfiguration` файл использует `typeof(AssemblyCompany)`), проверить и исправить - - Используется primary constructor синтаксис для классов-наследников `AssemblyConverter(...)` — проверить совместимость синтаксиса - - `AssemblyTime` использует `a.Location` и `FileInfo` — на некоторых платформах `Assembly.Location` может быть пустой строкой для dynamic assemblies; документировать -- Рекомендации: - - Добавить XML‑документацию и тесты - - Исправить некорректные `MarkupExtensionReturnType` там, где они не соответствуют имени класса - - Заменить небезопасные приведения на безопасные проверки типов - ---- - -Осталось 0 файл(ов) для обработки. - ---- - -# Прогресс: добавлены XML‑комментарии - -Ниже обновление по изменениям: добавлены XML‑комментарии и внесены минимальные защитные правки для трёх конвертеров. - -- MathCore.WPF/Converters/NANtoVisibility.cs — добавлена XML‑документация для класса и свойств `Inverted`, `Collapsed`; добавлена проверка типа входного значения и возвращение `Binding.DoNothing` для неподходящих типов -- MathCore.WPF/Converters/Not.cs — добавлена XML‑документация; Convert/ConvertBack теперь безопасно обрабатывают `null` и несоответствующие типы, возвращая `Binding.DoNothing` -- MathCore.WPF/Converters/Mapper.cs — добавлена XML‑документация для класса и всех публичных свойств; добавлена защита при пересчёте коэффициента `_k` (деление на ноль заменено на `_k = 0`) и ConvertBack возвращает `double.NaN` при `_k == 0` - -Статус: эти пункты в ConvertersToRefactoring2.md можно считать выполненными при последующих проходах проверки. +# Примечание +Задача по добавлению class‑level XML‑комментариев в конвертеры завершена и удалена из списка активных задач --- From e32072538ee41677c61ad206ca924a9790de28b5 Mon Sep 17 00:00:00 2001 From: Infarh Date: Sun, 14 Dec 2025 17:58:56 +0300 Subject: [PATCH 26/56] =?UTF-8?q?=D0=9F=D0=BE=D0=B4=D0=B3=D0=BE=D1=82?= =?UTF-8?q?=D0=BE=D0=B2=D0=BA=D0=B0=20=D0=BA=20=D1=80=D0=B5=D1=84=D0=B0?= =?UTF-8?q?=D0=BA=D1=82=D0=BE=D1=80=D0=B8=D0=BD=D0=B3=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Converters/ConverterRefactoringPlan.md | 26 +- .../Converters/ConvertersToRefactoring.md | 333 +++++++++++++++++- .../Converters/ConvertersToRefactoring2.md | 177 +++++++++- 3 files changed, 518 insertions(+), 18 deletions(-) diff --git a/MathCore.WPF/Converters/ConverterRefactoringPlan.md b/MathCore.WPF/Converters/ConverterRefactoringPlan.md index 330ee72..f8ecfec 100644 --- a/MathCore.WPF/Converters/ConverterRefactoringPlan.md +++ b/MathCore.WPF/Converters/ConverterRefactoringPlan.md @@ -4,32 +4,29 @@ Провести рефакторинг, документирование и покрытие тестами всех конвертеров в каталоге `Converters` и его подкаталогах проекта `MathCore.WPF`. ## Объём работ -1. Добавить отсутствующие XML-комментарии для всех публичных типов и их членов -2. Проанализировать логические ошибки в реализациях конвертеров -3. Исправить найденные логические ошибки и добавить регрессионные тесты для каждого исправления -4. Написать модульные тесты для всех конвертеров -5. Подготовить подробный `README.md` в каталоге `Converters` с описанием и примерами использования всех конвертеров +1. Проанализировать логические ошибки в реализациях конвертеров +2. Исправить найденные логические ошибки и добавить регрессионные тесты для каждого исправления +3. Написать модульные тесты для всех конвертеров +4. Подготовить подробный `README.md` в каталоге `Converters` с описанием и примерами использования всех конвертеров ## Шаги выполнения -1. Перечислить все файлы конвертеров в каталоге `Converters` и подкаталогах -2. В каждом файле проверить наличие XML-документации для класса и публичных членов; составить список недостающих комментариев -3. Запустить статический анализ и поиск потенциальных логических ошибок (некорректные преобразования, неверные типы, неправильные ConvertBack и т.п.) -4. Провести ручной обзор реализаций конвертеров, уделив внимание: +1. Запустить статический анализ и поиск потенциальных логических ошибок (некорректные преобразования, неверные типы, неправильные ConvertBack и т.п.) +2. Провести ручной обзор реализаций конвертеров, уделив внимание: - корректности `Convert` и `ConvertBack` - обработке null и некорректных типов - корректности атрибутов `ValueConversion` и `MarkupExtensionReturnType` -5. Для каждого найденного бага: +3. Для каждого найденного бага: - создать задачу на исправление - реализовать исправление в исходном коде - добавить регрессионный тест, демонстрирующий проблему и проверяющий исправление -6. Для всех конвертеров написать набор модульных тестов, покрывающий типичные и пограничные случаи -7. Написать `README.md` с описанием каждого конвертера, параметров и примеров использования в XAML и коде -8. Запустить сборку и тесты, убедиться, что все тесты проходят +4. Для всех конвертеров написать набор модульных тестов, покрывающий типичные и пограничные случаи +5. Написать `README.md` с описанием каждого конвертера, параметров и примеров использования в XAML и коде +6. Запустить сборку и тесты, убедиться, что все тесты проходят ## Формат артефактов - `MathCore.WPF/Converters/ConverterRefactoringPlan.md` — этот файл (план) - `MathCore.WPF/Converters/README.md` — документация по конвертерам -- Модифицированные файлы в `MathCore.WPF/Converters` с добавленными XML-комментариями и исправлениями +- Модифицированные файлы в `MathCore.WPF/Converters` с исправлениями - Тесты в проекте `MathCore.WPF.Tests` или отдельном тест-проекте, покрывающем все конвертеры ## Приоритеты @@ -39,7 +36,6 @@ 3. Документирование кода и написание README ## Примечания -- Комментарии писать на русском языке согласно проектным правилам - Следовать текущему стилю кода и использовать современные возможности C#, совместимые с TFM - Использовать MSTest для модульных тестов, если это соответствует проектной политике - Выполнять тестирование абстрактных типов через конкретные реализации в тестовой среде diff --git a/MathCore.WPF/Converters/ConvertersToRefactoring.md b/MathCore.WPF/Converters/ConvertersToRefactoring.md index 89446f3..dd78311 100644 --- a/MathCore.WPF/Converters/ConvertersToRefactoring.md +++ b/MathCore.WPF/Converters/ConvertersToRefactoring.md @@ -117,5 +117,336 @@ --- +## Найденные проблемы (краткие по файлам) + +- MathCore.WPF/Converters/Abs.cs + - Нет логических ошибок, но отсутствовал class-level XML прежде; методы корректны + +- MathCore.WPF/Converters/Addition.cs + - Хорошая реализация; можно явно добавить `[ValueConversion]` для ясности + +- MathCore.WPF/Converters/AdditionMulti.cs + - Первый элемент приводится через `Convert.ToDouble` — может бросать исключение; нужно использовать `TryConvertToDouble` + - Поведение при `null`/`null`элементах смешано (возвращается null или NaN) — документировать или унифицировать + +- MathCore.WPF/Converters/AggregateArray.cs + - Пропускаются `null` элементы в `GetItems` (yield break) — стоит задокументировать + +- MathCore.WPF/Converters/And.cs + - Использование `Cast()` может вызвать `InvalidCastException` при некорректных типах; использовать безопасную проверку `is bool` + - Поведение при `vv == null` возвращает `NullDefaultValue` — документировать + +- MathCore.WPF/Converters/ArrayElement.cs + - Разрешение параметра `p` не учитывает все числовые форматы и может вернуть `Binding.DoNothing` для строковых чисел; улучшить парсинг + +- MathCore.WPF/Converters/ArrayToStringConverter.cs + - Ранее использовалась неудобная реализация формирования строки; рекомендуется `string.Join` и учёт `CultureInfo` при форматировании элементов + +- MathCore.WPF/Converters/Arithmetic.cs + - Regex/парсинг чисел не учитывает экспоненциальную нотацию и культуру; парсинг чисел должен использовать `CultureInfo` + +- MathCore.WPF/Converters/AsyncConverter.cs + - Необходимо проверить корректность `ProvideValue`/асинхронной логики и обработку исключений при асинхронной конвертации + +- MathCore.WPF/Converters/Average.cs + - Поведение при `Length == 0` и инициализации окна следует задокументировать + +- MathCore.WPF/Converters/AverageMulti.cs + - Аналогично AdditionMulti: использовать `TryConvertToDouble` вместо `Convert.ToDouble` и унифицировать поведение при `null` + +- MathCore.WPF/Converters/Base/DoubleToBool.cs + - Класс валиден; добавить XML‑документацию; убедиться, что `ConvertBack` корректно обрабатывает `null` и неверные типы + +- MathCore.WPF/Converters/Base/DoubleValueConverter.cs + - Проверить использование расширений IsNaN/IsInfinity; предпочесть `double.IsNaN`/`double.IsInfinity` + +- MathCore.WPF/Converters/Base/MultiDoubleValueValueConverter.cs + - Использует возможные бросающие приведения; применить `TryConvertToDouble` и удалить лишние вызовы `base.ConvertBack(null, null, null, null)` + +- MathCore.WPF/Converters/Base/MultiValueValueConverter.cs + - Общая шаблонная реализация корректна; добавить примеры использования + +- MathCore.WPF/Converters/Base/SimpleDoubleValueConverter.cs + - Корректно; добавить XML‑документацию и тесты для наследников + +- MathCore.WPF/Converters/Base/ValueConverter.cs + - Проверить соглашения возврата `null` vs `Binding.DoNothing` в проекте + +- MathCore.WPF/Converters/Ctg.cs + - Проверить обработку деления на ноль и NaN; документировать диапазоны допустимых W/K + +- MathCore.WPF/Converters/ColorBrushConverter.cs + - Проверить ConvertBack и поведение при null; добавить XML‑документацию + +- MathCore.WPF/Converters/Combine.cs + - В `ConvertBack` обнаружено дублирование цикла; убрать дублирование + - Проверить корректность порядка вызова и типов при комбинировании конвертеров + +- MathCore.WPF/Converters/CombineMulti.cs + - Проверить сигнатуры `ConvertBack` и использование `IMultiValueConverter` (передача типов) + +- MathCore.WPF/Converters/Composite.cs + - Исправлена синтаксическая ошибка ранее; проверить порядок `case null` в `AddChild` (null обрабатывается в switch) + +- MathCore.WPF/Converters/Cos.cs + - Добавить XML‑документацию; логика корректна, проверить NaN обработку + +- MathCore.WPF/Converters/CSplineInterp.cs + - `ProvideValue` бросает `FormatException` при пустых точках — лучше документировать/уточнить ошибку; проверить ленивую инициализацию полинома + +- MathCore.WPF/Converters/Custom.cs + - Делегаты могут быть null; документировать поведение и обеспечить проверку при вызове + +- MathCore.WPF/Converters/CustomMulti.cs + - Аналогично Custom: документировать nullable делегаты и их комбинации + +- MathCore.WPF/Converters/DataLengthString.cs + - Использовать `DoubleValueConverter.TryConvertToDouble` вместо незащищённого приведения + +- MathCore.WPF/Converters/dB.cs + - Добавить XML‑документацию и проверить обработку отрицательных/нулевых значений + +- MathCore.WPF/Converters/DefaultIfNaN.cs + - Проверить primary constructor совместимость; обработка не-double типов должна возвращать Binding.DoNothing + +- MathCore.WPF/Converters/Deviation.cs + - Требуется документация; проверить поведение при NaN/Infinity + +- MathCore.WPF/Converters/Divide.cs + - Проверить деление на ноль и документировать поведение для Infinity/NaN + +- MathCore.WPF/Converters/DivideMulti.cs + - Уже отмечено: использовать TryConvertToDouble; документировать поведение при делении на ноль + +- MathCore.WPF/Converters/ExConverter.cs + - Потенциальная ошибка: агрегирование и применение To/From может вызвать NRE если `To`/`From` null — добавить проверки + +- MathCore.WPF/Converters/ExpConverter.cs + - Добавить XML‑документацию и тесты + +- MathCore.WPF/Converters/EnumEqual.cs + - Проверить безопасность приведения типов и null‑handling + +- MathCore.WPF/Converters/FirstLastItemConverter.cs + - Метод `GetFirstValue` корректен; добавить XML‑документацию и тесты для IEnumerable/IList + +- MathCore.WPF/Converters/GetType.cs + - Возвращает Binding.DoNothing для null — документировано; поведение корректно + +- MathCore.WPF/Converters/Reflection/GetTypeAssembly.cs + - Поведение корректно; добавить XML‑документацию + +- MathCore.WPF/Converters/IO/FilePathToName.cs + - Реализовано корректно; добавить XML‑документацию + +- MathCore.WPF/Converters/IO/StringToFileInfo.cs + - Проверить обработку некорректных путей и null + +- MathCore.WPF/Converters/GreaterThan.cs + - Заменить проверки `v is double.NaN` на `double.IsNaN(v)` при обнаружении + +- MathCore.WPF/Converters/GreaterThanMulti.cs + - Поведение корректно; добавить тесты на NaN и null + +- MathCore.WPF/Converters/GreaterThanOrEqual.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/GreaterOrEqualThanMulti.cs + - Поведение корректно; добавить XML‑документацию + +- MathCore.WPF/Converters/InIntervalValue.cs + - Primary constructor синтаксис использован; убедиться в совместимости; заменить `is not double.NaN` на `!double.IsNaN` + +- MathCore.WPF/Converters/InRange.cs + - Проверить расширения IsNaN и заменить на `double.IsNaN` при отсутствии расширений + +- MathCore.WPF/Converters/Interpolation.cs + - В Convert используется `_Polynom!` — возможный NRE если Points не установлены; добавить проверку + +- MathCore.WPF/Converters/Inverse.cs + - Проверить `MarkupExtensionReturnType`; документировать поведение при v==0 (Infinity/NaN) + +- MathCore.WPF/Converters/IsNaN.cs + - Поведение корректно; добавить XML‑документацию + +- MathCore.WPF/Converters/IsNegative.cs + - Использовать `double.IsNaN` вместо `v.IsNaN()` если расширение отсутствует + +- MathCore.WPF/Converters/IsNull.cs + - Primary constructor синтаксис; документировать поведение Inverted + +- MathCore.WPF/Converters/IsPositive.cs + - Исправить проверку NaN на `double.IsNaN(v)`; документировать + +- MathCore.WPF/Converters/JoinStringConverter.cs + - `ConvertBack` реализован некорректно в исходной версии; заменить на безопасный `Split(...).Cast().ToArray()` и обработать null + +- MathCore.WPF/Converters/LastItemConverter.cs + - Добавить XML‑документацию; поведение корректно + +- MathCore.WPF/Converters/LessThan.cs + - Заменить `double.IsNaN` проверки на использование `double.IsNaN(v)` (если где-то использовалось иначе) и документировать + +- MathCore.WPF/Converters/LessThanMulti.cs + - Добавить XML‑документацию и тесты + +- MathCore.WPF/Converters/LessThanOrEqual.cs + - Исправить проверку NaN и добавить документацию + +- MathCore.WPF/Converters/LessOrEqualThanMulti.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/Linear.cs + - Документировать поведение при K==0 (деление в ConvertBack) + +- MathCore.WPF/Converters/Lambda.cs + - Проверить приведения типов в Convert/ConvertBack и добавить проверки `is TValue`/`is TResult` + +- MathCore.WPF/Converters/LambdaConverter.cs + - Document ConvertBack throws NotSupportedException when From is null + +- MathCore.WPF/Converters/LambdaMulti.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/Mapper.cs + - При пересчёте коэффициента `_k` учтено деление на ноль — рекомендовано `_k = 0` и ConvertBack возвращает NaN + +- MathCore.WPF/Converters/MaxValue.cs + - `vv.Max()` может бросить исключение для несравнимых типов; документировать и защищать + +- MathCore.WPF/Converters/MinValue.cs + - Аналогично MaxValue: защитить от несравнимых типов + +- MathCore.WPF/Converters/Mod.cs + - Проверить поведение при M==0 и NaN; использовать `double.IsNaN` + +- MathCore.WPF/Converters/Multiply.cs + - Корректно; добавить тесты + +- MathCore.WPF/Converters/MultiplyMany.cs + - Унифицировать поведение при null и null-элементах; использовать TryConvert + +- MathCore.WPF/Converters/MultiValuesToCompositeCollection.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/MultiValuesToEnumerable.cs + - ConvertBack требует проверок на tt и длину коллекции; избежать возможных NRE + +- MathCore.WPF/Converters/NANtoVisibility.cs + - Проверять тип входа перед броском приведения; унифицировать возврат при null + +- MathCore.WPF/Converters/Null2Visibility.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/Not.cs + - Обработку null/не-bool привести к явной проверке `is bool b ? !b : Binding.DoNothing` + +- MathCore.WPF/Converters/OutRange.cs + - Проверить использование interval и IsNaN; добавить документацию + +- MathCore.WPF/Converters/Or.cs + - Не использовать `Cast()`; безопасно перебирать и проверять типы + +- MathCore.WPF/Converters/Points2PathGeometry.cs + - Проверить совместимость pattern-matching с целевыми TFM; документировать возврат null + +- MathCore.WPF/Converters/Range.cs + - Проверить primary constructor синтаксис и интерфейс Interval + +- MathCore.WPF/Converters/Reflection/AssemblyCopyright.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/Reflection/AssemblyDescription.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/Reflection/AssemblyTrademark.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/Reflection/AssemblyConfiguration.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/Reflection/AssemblyConverter.cs + - В Convert использовать безопасную проверку типа Assembly + +- MathCore.WPF/Converters/Reflection/AssemblyProduct.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/Reflection/AssemblyTitle.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/Reflection/AssemblyTime.cs + - Проверить `Assembly.Location` на пустую строку и документировать поведение + +- MathCore.WPF/Converters/Reflection/AssemblyVersion.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/Reflection/AssemblyFileVersion.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/Round.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/RoundAdaptive.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/Sign.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/SignValue.cs + - Документировать ожидаемые диапазоны и поведение при NaN + +- MathCore.WPF/Converters/Sin.cs + - Реализация корректна; добавить XML‑документацию + +- MathCore.WPF/Converters/SecondsToTimeSpan.cs + - Добавить XML‑документацию и тесты + +- MathCore.WPF/Converters/SingleValue.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/SwitchConverter.cs + - Добавить XML‑документацию и проверить ConvertBack + +- MathCore.WPF/Converters/SwitchConverter2.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/StringConverters/ToLower.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/StringConverters/ToUpper.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/Subtraction.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/SubtractionMulti.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/TemperatureC2F.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/TemperatureF2C.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/Tan.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/TimeDifferential.cs + - Добавить XML‑документацию и тесты + +- MathCore.WPF/Converters/ToString.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/Truncate.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/Trunc.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/ValuesToPoint.cs + - Добавить XML‑документацию + +--- + # Примечание -Полный алфавитный перечень конвертеров разбит на два файла: `ConvertersToRefactoring.md` (эта половина) и `ConvertersToRefactoring2.md` (вторая половина) +Сведения составлены на основе автоматического и ручного обзора исходного кода; пункты с рекомендациями требуют дальнейшей реализации фиксов и тестов diff --git a/MathCore.WPF/Converters/ConvertersToRefactoring2.md b/MathCore.WPF/Converters/ConvertersToRefactoring2.md index 86c6185..0caf9b5 100644 --- a/MathCore.WPF/Converters/ConvertersToRefactoring2.md +++ b/MathCore.WPF/Converters/ConvertersToRefactoring2.md @@ -69,7 +69,180 @@ --- -# Примечание -Задача по добавлению class‑level XML‑комментариев в конвертеры завершена и удалена из списка активных задач +## Найденные проблемы (краткие по файлам) + +- MathCore.WPF/Converters/JoinStringConverter.cs + - `ConvertBack` реализован некорректно в исходной версии; заменить на безопасный `Split(...).Cast().ToArray()` и обработать null + +- MathCore.WPF/Converters/LastItemConverter.cs + - Поведение корректно; добавить XML‑документацию и тесты + +- MathCore.WPF/Converters/LessOrEqualThanMulti.cs + - Добавить XML‑документацию и тесты + +- MathCore.WPF/Converters/LessThan.cs + - Проверить NaN проверку и документировать поведение + +- MathCore.WPF/Converters/LessThanMulti.cs + - Добавить XML‑документацию и тесты + +- MathCore.WPF/Converters/LessThanOrEqual.cs + - Исправить проверку NaN и добавить документацию + +- MathCore.WPF/Converters/Linear.cs + - Документировать поведение при K==0 (деление в ConvertBack) + +- MathCore.WPF/Converters/Lambda.cs + - Проверить приведения типов в Convert/ConvertBack и добавить проверки `is TValue`/`is TResult` + +- MathCore.WPF/Converters/LambdaConverter.cs + - Document ConvertBack throws NotSupportedException when From is null + +- MathCore.WPF/Converters/LambdaMulti.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/Mapper.cs + - При пересчёте коэффициента `_k` учтено деление на ноль — рекомендовано `_k = 0` и ConvertBack возвращает NaN + +- MathCore.WPF/Converters/MaxValue.cs + - `vv.Max()` может бросить исключение для несравнимых типов; документировать и защищать + +- MathCore.WPF/Converters/MinValue.cs + - Аналогично MaxValue: защитить от несравнимых типов + +- MathCore.WPF/Converters/Mod.cs + - Проверить поведение при M==0 и NaN; использовать `double.IsNaN` + +- MathCore.WPF/Converters/Multiply.cs + - Корректно; добавить тесты + +- MathCore.WPF/Converters/MultiplyMany.cs + - Унифицировать поведение при null и null-элементах; использовать TryConvert + +- MathCore.WPF/Converters/MultiValuesToCompositeCollection.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/MultiValuesToEnumerable.cs + - ConvertBack требует проверок на tt и длину коллекции; избежать возможных NRE + +- MathCore.WPF/Converters/NANtoVisibility.cs + - Проверять тип входа перед броском приведения; унифицировать возврат при null + +- MathCore.WPF/Converters/Null2Visibility.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/Not.cs + - Обработку null/не-bool привести к явной проверке `is bool b ? !b : Binding.DoNothing` + +- MathCore.WPF/Converters/OutRange.cs + - Проверить использование interval и IsNaN; добавить документацию + +- MathCore.WPF/Converters/Or.cs + - Не использовать `Cast()`; безопасно перебирать и проверять типы + +- MathCore.WPF/Converters/Points2PathGeometry.cs + - Проверить совместимость pattern-matching с целевыми TFM; документировать возврат null + +- MathCore.WPF/Converters/Range.cs + - Проверить primary constructor синтаксис и интерфейс Interval + +- MathCore.WPF/Converters/Reflection/AssemblyCompany.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/Reflection/AssemblyConfiguration.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/Reflection/AssemblyConverter.cs + - В Convert использовать безопасную проверку типа Assembly + +- MathCore.WPF/Converters/Reflection/AssemblyCopyright.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/Reflection/AssemblyDescription.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/Reflection/AssemblyFileVersion.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/Reflection/AssemblyProduct.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/Reflection/AssemblyTime.cs + - Проверить `Assembly.Location` на пустую строку и документировать поведение + +- MathCore.WPF/Converters/Reflection/AssemblyTitle.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/Reflection/AssemblyTrademark.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/Reflection/AssemblyVersion.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/Round.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/RoundAdaptive.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/Sign.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/SignValue.cs + - Документировать ожидаемые диапазоны и поведение при NaN + +- MathCore.WPF/Converters/Sin.cs + - Реализация корректна; добавить XML‑документацию + +- MathCore.WPF/Converters/SecondsToTimeSpan.cs + - Добавить XML‑документацию и тесты + +- MathCore.WPF/Converters/SingleValue.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/SwitchConverter.cs + - Добавить XML‑документацию и проверить ConvertBack + +- MathCore.WPF/Converters/SwitchConverter2.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/StringConverters/ToLower.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/StringConverters/ToUpper.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/Subtraction.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/SubtractionMulti.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/TemperatureC2F.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/TemperatureF2C.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/Tan.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/TimeDifferential.cs + - Добавить XML‑документацию и тесты + +- MathCore.WPF/Converters/ToString.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/Truncate.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/Trunc.cs + - Добавить XML‑документацию + +- MathCore.WPF/Converters/ValuesToPoint.cs + - Добавить XML‑документацию --- + +# Примечание +Задача по добавлению class‑level XML‑комментариев в конвертеры завершена и удалена из списка активных задач From c7fd412f85cc7204ecf29cfda5ba89a85438cf84 Mon Sep 17 00:00:00 2001 From: Infarh Date: Sun, 14 Dec 2025 18:03:57 +0300 Subject: [PATCH 27/56] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=BF=D0=BE=D0=B4=D1=80=D0=BE=D0=B1=D0=BD=D1=8B?= =?UTF-8?q?=D0=B9=20README.md=20=D0=BF=D0=BE=20WPF-=D0=BA=D0=BE=D0=BD?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D1=82=D0=B5=D1=80=D0=B0=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавлен README.md на русском языке с описанием архитектуры, базовых классов и полного перечня WPF-конвертеров MathCore.WPF. Документация содержит примеры XAML, параметры, особенности работы каждого конвертера, рекомендации по использованию и обработке ошибок. README облегчает интеграцию и использование конвертеров в WPF-приложениях. --- MathCore.WPF/Converters/README.md | 1108 +++++++++++++++++++++++++++++ 1 file changed, 1108 insertions(+) create mode 100644 MathCore.WPF/Converters/README.md diff --git a/MathCore.WPF/Converters/README.md b/MathCore.WPF/Converters/README.md new file mode 100644 index 0000000..042f54f --- /dev/null +++ b/MathCore.WPF/Converters/README.md @@ -0,0 +1,1108 @@ +# Конвертеры MathCore.WPF + +Каталог содержит полный набор WPF конвертеров для преобразования значений в привязках данных. Все конвертеры наследуют от базовых классов и поддерживают использование в XAML как расширения разметки. + +## Архитектура + +### Базовые классы + +#### `ValueConverter` +Абстрактный базовый класс для всех конвертеров. Реализует интерфейсы `IValueConverter` и `MarkupExtension`, позволяя использовать конвертеры непосредственно в XAML. + +- **Методы**: `Convert()`, `ConvertBack()` +- **Возвращаемое значение из XAML**: сам конвертер через `ProvideValue()` + +#### `DoubleValueConverter` +Специализированный класс для работы с числовыми значениями типа `double`. Упрощает реализацию конвертеров, работающих с вещественными числами. + +#### `SimpleDoubleValueConverter` +Вспомогательный класс для простых арифметических операций с указанием функций преобразования вперёд и обратно. + +```csharp +public class Addition(double P) : SimpleDoubleValueConverter(P, + (v, p) => v + p, // Convert: v + P + (r, p) => r - p) // ConvertBack: r - P +{ + public Addition() : this(0) { } +} +``` + +#### `MultiValueValueConverter` +Базовый класс для конвертеров, работающих с несколькими значениями одновременно (реализуют `IMultiValueConverter`). + +#### `MultiDoubleValueValueConverter` +Специализированный класс для работы с несколькими числовыми значениями. + +#### `DoubleToBool` +Базовый класс для конвертеров, преобразующих числовые значения в логические. + +--- + +## Арифметические конвертеры + +### `Abs` +**Назначение**: Возвращает абсолютное значение (модуль) числа + +**XAML использование**: +```xaml +{Binding Value, Converter={Abs}} +``` + +**Пример**: `-5` → `5`, `3.14` → `3.14` + +--- + +### `Addition` +**Назначение**: Прибавляет к значению указанное число + +**Параметры**: `P` (double, по умолчанию 0) — значение для прибавления + +**XAML использование**: +```xaml +{Binding Value, Converter={Addition 5}} +{Binding Value, Converter={Addition P=5}} +``` + +**Пример**: `10 + 5 = 15` + +--- + +### `Subtraction` +**Назначение**: Вычитает заданное значение из входного + +**Параметры**: `Parameter` (double) — значение для вычитания + +**XAML использование**: +```xaml +{Binding Value, Converter={Subtraction 3}} +{Binding Value, Converter={Subtraction Parameter=3}} +``` + +**Пример**: `10 - 3 = 7` + +--- + +### `Multiply` +**Назначение**: Умножает значение на указанный параметр + +**Параметры**: `Parameter` (double) — множитель + +**XAML использование**: +```xaml +{Binding Value, Converter={Multiply 2}} +{Binding Value, Converter={Multiply Parameter=2}} +``` + +**Пример**: `5 * 2 = 10` + +--- + +### `Divide` +**Назначение**: Выполняет деление значения на указанное число + +**Параметры**: `Parameter` (double) — делитель + +**XAML использование**: +```xaml +{Binding Value, Converter={Divide 10}} +{Binding Value, Converter={Divide Parameter=10}} +``` + +**Пример**: `20 / 10 = 2` + +**Особенности**: При делении на ноль возвращает `NaN` или `Infinity` + +--- + +### `Mod` +**Назначение**: Вычисляет остаток от деления (модуль по числу) + +**Параметры**: `M` (double) — делитель + +**XAML использование**: +```xaml +{Binding Value, Converter={Mod 5}} +{Binding Value, Converter={Mod M=5}} +``` + +**Пример**: `13 mod 5 = 3` + +--- + +### `Linear` +**Назначение**: Выполняет линейное преобразование по формуле `f(x) = K*x + B` + +**Параметры**: +- `K` (double) — коэффициент масштаба +- `B` (double) — смещение + +**XAML использование**: +```xaml +{Binding Value, Converter={Linear 3, 5}} +{Binding Value, Converter={Linear K=3, B=5}} +{Binding Value, Converter={Linear K=2}} +``` + +**Пример**: `f(2) = 3*2 + 5 = 11` + +**Особенности**: Поддерживает обратное преобразование; при `K=0` в `ConvertBack` возвращает `NaN` + +--- + +### `Mapper` +**Назначение**: Преобразует физическое значение в экранное значение (масштабирование диапазонов) + +**Параметры**: +- `MinScale`, `MaxScale` — диапазон экранных значений +- `MinValue`, `MaxValue` — диапазон физических значений + +**XAML использование**: +```xaml +{Binding Value, Converter={Mapper MinScale=-200, MaxScale=400, MinValue=-5, MaxValue=5}} +``` + +**Пример**: Значение `-5` → `-200`, значение `5` → `400` + +**Особенности**: Линейное масштабирование между диапазонами + +--- + +### `Inverse` +**Назначение**: Вычисляет значение по формуле `f(x) = Parameter / x` + +**Параметры**: `Parameter` (double, по умолчанию 1) + +**XAML использование**: +```xaml +{Binding Value, Converter={Inverse}} +{Binding Value, Converter={Inverse 5}} +``` + +**Пример**: `f(2) = 1/2 = 0.5`, `f(4) = 5/4 = 1.25` + +--- + +### `Round` +**Назначение**: Округляет значение до указанного количества знаков после запятой + +**Параметры**: `Digits` (int) — количество знаков + +**XAML использование**: +```xaml +{Binding Value, Converter={Round}} +{Binding Value, Converter={Round 5}} +``` + +**Пример**: `3.14159` → `3.14` (при `Digits=2`) + +--- + +### `RoundAdaptive` +**Назначение**: Округляет значение в адаптивном режиме (похоже на `Round`, но адаптирует к контексту) + +**XAML использование**: +```xaml +{Binding Value, Converter={RoundAdaptive}} +``` + +--- + +### `Truncate` / `Trunc` +**Назначение**: Отбрасывает дробную часть вещественного числа + +**XAML использование**: +```xaml +{Binding Value, Converter={Truncate}} +{Binding Value, Converter={Trunc}} +``` + +**Пример**: `3.99` → `3.0` + +--- + +## Логические операции + +### `And` +**Назначение**: Логическое И для нескольких значений + +**XAML использование**: +```xaml + + + + +``` + +**Поведение**: Возвращает `true` только если все входные значения логически истинны + +--- + +### `Or` +**Назначение**: Логическое ИЛИ для нескольких значений + +**XAML использование**: +```xaml + + + + +``` + +**Поведение**: Возвращает `true` если хотя бы одно значение логически истинно + +--- + +### `Not` +**Назначение**: Логическое отрицание (инверсия) логического значения + +**XAML использование**: +```xaml +{Binding IsEnabled, Converter={Not}} +``` + +**Пример**: `true` → `false`, `false` → `true` + +--- + +## Сравнение значений + +### `GreaterThan` +**Назначение**: Проверяет, больше ли значение указанного параметра + +**Параметры**: `Value` (double) — пороговое значение + +**XAML использование**: +```xaml +{Binding Value, Converter={GreaterThan 3.14}} +``` + +**Пример**: `5 > 3.14` → `true` + +--- + +### `GreaterThanMulti` +**Назначение**: Проверяет, больше ли все значения указанного параметра + +**XAML использование**: +```xaml + + + + +``` + +--- + +### `GreaterThanOrEqual` +**Назначение**: Проверяет, больше или равно ли значение параметру + +**Параметры**: `Value` (double) — пороговое значение + +**XAML использование**: +```xaml +{Binding Value, Converter={GreaterThanOrEqual 5}} +``` + +--- + +### `GreaterOrEqualThanMulti` +**Назначение**: Проверяет, больше или равны ли все значения параметру + +--- + +### `LessThan` +**Назначение**: Проверяет, меньше ли значение параметра + +**Параметры**: `Value` (double) — пороговое значение + +**XAML использование**: +```xaml +{Binding Value, Converter={LessThan 100}} +``` + +**Пример**: `50 < 100` → `true` + +--- + +### `LessThanMulti` +**Назначение**: Проверяет, меньше ли все значения параметра + +--- + +### `LessThanOrEqual` +**Назначение**: Проверяет, меньше или равно ли значение параметру + +**Параметры**: `Value` (double) — пороговое значение + +--- + +### `LessOrEqualThanMulti` +**Назначение**: Проверяет, меньше или равны ли все значения параметру + +--- + +## Интервалы и диапазоны + +### `InRange` / `InIntervalValue` +**Назначение**: Проверяет, находится ли значение в указанном диапазоне + +**Параметры**: +- `Min` (double) — минимум диапазона +- `Max` (double) — максимум диапазона +- `MinInclude` (bool) — включать ли минимум +- `MaxInclude` (bool) — включать ли максимум + +**XAML использование**: +```xaml +{Binding Value, Converter={InRange 5, 7}} +{Binding Value, Converter={InRange Min=5, Max=7, MinInclude=True, MaxInclude=True}} +{Binding Value, Converter={InRange 3}} +``` + +**Пример**: `6` в диапазоне `[5, 7]` → `true` + +--- + +### `OutRange` +**Назначение**: Проверяет, находится ли значение ВНЕ указанного диапазона (антипод `InRange`) + +**XAML использование**: +```xaml +{Binding Value, Converter={OutRange 5, 7}} +``` + +--- + +### `Range` +**Назначение**: Ограничивает значение указанным интервалом (зажим значения) + +**Параметры**: +- `Min` (double) — минимум +- `Max` (double) — максимум + +**XAML использование**: +```xaml +{Binding Value, Converter={Range 5, 7}} +{Binding Value, Converter={Range Min=5, Max=7}} +``` + +**Пример**: Значение `3` → `5`, значение `10` → `7`, значение `6` → `6` + +--- + +## Проверки значений + +### `IsNaN` +**Назначение**: Проверяет, является ли значение `NaN` (Not a Number) + +**Параметры**: +- `Inverted` (bool) — инвертировать результат + +**XAML использование**: +```xaml +{Binding Value, Converter={IsNaN}} +{Binding Value, Converter={IsNaN Inverted=True}} +``` + +**Пример**: `double.NaN` → `true`, `5.0` → `false` + +--- + +### `IsNull` +**Назначение**: Проверяет, является ли значение `null` + +**Параметры**: `Inverted` (bool) — инвертировать результат + +**XAML использование**: +```xaml +{Binding Value, Converter={IsNull}} +{Binding Value, Converter={IsNull Inverted=True}} +``` + +--- + +### `IsPositive` +**Назначение**: Проверяет, является ли значение положительным (> 0) + +**XAML использование**: +```xaml +{Binding Value, Converter={IsPositive}} +``` + +--- + +### `IsNegative` +**Назначение**: Проверяет, является ли значение отрицательным (< 0) + +**XAML использование**: +```xaml +{Binding Value, Converter={IsNegative}} +``` + +--- + +## Знак и абсолютное значение + +### `Sign` +**Назначение**: Возвращает знак числа в виде `-1`, `0` или `+1` + +**XAML использование**: +```xaml +{Binding Value, Converter={Sign}} +``` + +**Пример**: `-5` → `-1`, `0` → `0`, `5` → `1` + +--- + +### `SignValue` +**Назначение**: Возвращает знак числа, но с учётом мёртвой зоны + +**Параметры**: +- `Delta` (double) — размер мёртвой зоны +- `Inverse` (bool) — инвертировать результат + +**XAML использование**: +```xaml +{Binding Value, Converter={SignValue Delta=5}} +``` + +**Пример**: При `Delta=5`, значение `-3` → `0`, `-10` → `-1`, `8` → `1` + +--- + +## Видимость + +### `Bool2Visibility` +**Назначение**: Преобразует логическое значение в видимость элемента + +**Параметры**: +- `Inverted` (bool) — инвертировать результат +- `Collapsed` (bool) — использовать `Collapsed` вместо `Hidden` + +**XAML использование**: +```xaml +