From b792639635d4876f42afeb8b9b8ac9f057fa7d07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81d=C3=A1m=20Kov=C3=A1cs?= Date: Tue, 15 Aug 2023 13:17:42 +0200 Subject: [PATCH] New binding mechanism: Expression tracking --- .../IRootViewModel.cs | 2 + src/ConsoleApp/FileTime.ConsoleUI.App/App.cs | 15 +- .../Controls/CommandPalette.cs | 2 +- .../Controls/Timeline.cs | 46 ++++ .../FileTime.ConsoleUI.App/MainWindow.cs | 48 ++-- .../FileTime.ConsoleUI.App/RootViewModel.cs | 6 +- .../FileTime.ConsoleUI.App/Startup.cs | 1 + .../FileTime.ConsoleUI.Styles/DefaultTheme.cs | 6 + .../CommandScheduler.cs | 2 +- src/FileTime.sln | 7 + src/Library/TerminalUI.Tests/BindingTests.cs | 259 ++++++++++++++++++ src/Library/TerminalUI.Tests/GlobalUsings.cs | 1 + .../Models/TestCollectionItem.cs | 10 + .../TerminalUI.Tests/Models/TestItem.cs | 6 + .../Models/TestNestedCollectionItem.cs | 22 ++ .../TerminalUI.Tests/Models/TestViewModel.cs | 32 +++ .../TerminalUI.Tests/TerminalUI.Tests.csproj | 33 +++ src/Library/TerminalUI/Binding.cs | 37 ++- src/Library/TerminalUI/Controls/TextBlock.cs | 4 +- src/Library/TerminalUI/EventLoop.cs | 12 +- .../ExpressionTrackers/BinaryTracker.cs | 37 +++ .../ExpressionTrackers/ConditionalTracker.cs | 44 +++ .../ExpressionTrackers/ConstantTracker.cs | 13 + .../ExpressionParameterTrackerCollection.cs | 19 ++ .../ExpressionTrackerBase.cs | 82 ++++++ .../ExpressionTrackers/IExpressionTracker.cs | 9 + .../ExpressionTrackers/MemberTracker.cs | 82 ++++++ .../ExpressionTrackers/MethodCallTracker.cs | 40 +++ .../ExpressionTrackers/ParameterTracker.cs | 31 +++ .../ExpressionTrackers/UnaryTracker.cs | 46 ++++ .../{Binding.cs => BindingExtensions.cs} | 2 +- .../TerminalUI/Extensions/ViewExtensions.cs | 2 +- .../TerminalUI/PropertyChangeTracker.cs | 149 ---------- .../TerminalUI/PropertyChangedHandler.cs | 30 +- src/Library/TerminalUI/PropertyTrackerBase.cs | 145 ++++------ 35 files changed, 971 insertions(+), 311 deletions(-) create mode 100644 src/ConsoleApp/FileTime.ConsoleUI.App/Controls/Timeline.cs create mode 100644 src/Library/TerminalUI.Tests/BindingTests.cs create mode 100644 src/Library/TerminalUI.Tests/GlobalUsings.cs create mode 100644 src/Library/TerminalUI.Tests/Models/TestCollectionItem.cs create mode 100644 src/Library/TerminalUI.Tests/Models/TestItem.cs create mode 100644 src/Library/TerminalUI.Tests/Models/TestNestedCollectionItem.cs create mode 100644 src/Library/TerminalUI.Tests/Models/TestViewModel.cs create mode 100644 src/Library/TerminalUI.Tests/TerminalUI.Tests.csproj create mode 100644 src/Library/TerminalUI/ExpressionTrackers/BinaryTracker.cs create mode 100644 src/Library/TerminalUI/ExpressionTrackers/ConditionalTracker.cs create mode 100644 src/Library/TerminalUI/ExpressionTrackers/ConstantTracker.cs create mode 100644 src/Library/TerminalUI/ExpressionTrackers/ExpressionParameterTrackerCollection.cs create mode 100644 src/Library/TerminalUI/ExpressionTrackers/ExpressionTrackerBase.cs create mode 100644 src/Library/TerminalUI/ExpressionTrackers/IExpressionTracker.cs create mode 100644 src/Library/TerminalUI/ExpressionTrackers/MemberTracker.cs create mode 100644 src/Library/TerminalUI/ExpressionTrackers/MethodCallTracker.cs create mode 100644 src/Library/TerminalUI/ExpressionTrackers/ParameterTracker.cs create mode 100644 src/Library/TerminalUI/ExpressionTrackers/UnaryTracker.cs rename src/Library/TerminalUI/Extensions/{Binding.cs => BindingExtensions.cs} (97%) delete mode 100644 src/Library/TerminalUI/PropertyChangeTracker.cs diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/IRootViewModel.cs b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/IRootViewModel.cs index f159f15..c70b3a2 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/IRootViewModel.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/IRootViewModel.cs @@ -1,5 +1,6 @@ using FileTime.App.CommandPalette.ViewModels; using FileTime.App.Core.ViewModels; +using FileTime.App.Core.ViewModels.Timeline; using FileTime.ConsoleUI.App.Services; using FileTime.Core.Interactions; @@ -13,5 +14,6 @@ public interface IRootViewModel string MachineName { get; } ICommandPaletteViewModel CommandPalette { get; } IDialogService DialogService { get; } + ITimelineViewModel TimelineViewModel { get; } event Action? FocusReadInputElement; } \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs index 27f7c16..915b6d0 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs @@ -2,7 +2,11 @@ using FileTime.App.Core.Services; using FileTime.App.Core.ViewModels; using FileTime.ConsoleUI.App.KeyInputHandling; +using FileTime.Core.Command.CreateContainer; +using FileTime.Core.Models; +using FileTime.Core.Timeline; using GeneralInputKey; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using TerminalUI; using TerminalUI.ConsoleDrivers; @@ -27,6 +31,7 @@ public class App : IApplication private readonly IConsoleDriver _consoleDriver; private readonly IAppState _appState; private readonly ILogger _logger; + private readonly IServiceProvider _serviceProvider; private readonly IKeyInputHandlerService _keyInputHandlerService; private readonly Thread _renderThread; @@ -39,7 +44,8 @@ public class App : IApplication IApplicationContext applicationContext, IConsoleDriver consoleDriver, IAppState appState, - ILogger logger) + ILogger logger, + IServiceProvider serviceProvider) { _lifecycleService = lifecycleService; _keyInputHandlerService = keyInputHandlerService; @@ -50,6 +56,7 @@ public class App : IApplication _consoleDriver = consoleDriver; _appState = appState; _logger = logger; + _serviceProvider = serviceProvider; _renderThread = new Thread(Render); } @@ -74,6 +81,12 @@ public class App : IApplication var focusManager = _applicationContext.FocusManager; + var command = _serviceProvider.GetRequiredService(); + command.Init(new FullName("local/C:/Test3"), "container1"); + var scheduler = _serviceProvider.GetRequiredService(); + + scheduler.AddCommand(command); + while (_applicationContext.IsRunning) { try diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/CommandPalette.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/CommandPalette.cs index 41ac5ae..ddaa60c 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/CommandPalette.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/CommandPalette.cs @@ -129,7 +129,7 @@ public class CommandPalette }; root.WithPropertyChangedHandler(r => r.IsVisible, - (_, _, isVisible) => + (_, isVisible) => { if (isVisible) { diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/Timeline.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/Timeline.cs new file mode 100644 index 0000000..e3f25ee --- /dev/null +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/Timeline.cs @@ -0,0 +1,46 @@ +using FileTime.App.Core.ViewModels.Timeline; +using TerminalUI.Controls; +using TerminalUI.Extensions; + +namespace FileTime.ConsoleUI.App.Controls; + +public class Timeline +{ + public IView View() + { + var root = new Grid + { + ChildInitializer = + { + new ItemsControl() + { + ItemTemplate = () => + { + var grid = new Grid() + { + ChildInitializer = + { + new TextBlock() + { + + }.Setup(t => t.Bind( + t, + dc => dc.DisplayLabel.Value, + t => t.Text)) + } + }; + + return grid; + } + } + .Setup(i => i.Bind( + i, + dc => dc.TimelineViewModel.ParallelCommandsGroups[0].Commands, + i => i.ItemsSource, + v => v)) + } + }; + + return root; + } +} \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/MainWindow.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/MainWindow.cs index ec7187c..f93fcd1 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/MainWindow.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/MainWindow.cs @@ -1,17 +1,13 @@ -using System.Collections.Specialized; -using System.ComponentModel; -using FileTime.App.Core.Models.Enums; +using FileTime.App.Core.Models.Enums; using FileTime.App.Core.ViewModels; using FileTime.ConsoleUI.App.Controls; using FileTime.ConsoleUI.App.Styling; using FileTime.Core.Enums; -using FileTime.Core.Interactions; using TerminalUI; using TerminalUI.Color; using TerminalUI.Controls; using TerminalUI.Extensions; using TerminalUI.Models; -using TerminalUI.Traits; using TerminalUI.ViewExtensions; namespace FileTime.ConsoleUI.App; @@ -23,6 +19,7 @@ public class MainWindow private readonly ITheme _theme; private readonly CommandPalette _commandPalette; private readonly Dialogs _dialogs; + private readonly Timeline _timeline; private readonly Lazy _root; @@ -31,13 +28,15 @@ public class MainWindow IApplicationContext applicationContext, ITheme theme, CommandPalette commandPalette, - Dialogs dialogs) + Dialogs dialogs, + Timeline timeline) { _rootViewModel = rootViewModel; _applicationContext = applicationContext; _theme = theme; _commandPalette = commandPalette; _dialogs = dialogs; + _timeline = timeline; _root = new Lazy(Initialize); } @@ -67,7 +66,7 @@ public class MainWindow private Grid MainContent() => new() { - RowDefinitionsObject = "Auto * Auto Auto", + RowDefinitionsObject = "Auto * Auto Auto Auto", ChildInitializer = { new Grid @@ -128,27 +127,19 @@ public class MainWindow SelectedsItemsView().WithExtension(new GridPositionExtension(2, 0)), } }, - new Grid - { - Extensions = - { - new GridPositionExtension(0, 2) - }, - ChildInitializer = - { - PossibleCommands() - } - }, new ItemsControl { MaxHeight = 5, Extensions = { - new GridPositionExtension(0, 3) + new GridPositionExtension(0, 2) }, ItemTemplate = () => { - return new TextBlock() + return new TextBlock + { + Foreground = _theme.WarningForegroundColor + } .Setup(t => t.Bind( t, dc => dc, @@ -159,7 +150,19 @@ public class MainWindow i, root => root.AppState.PopupTexts, c => c.ItemsSource - )) + )), + new Grid + { + Extensions = + { + new GridPositionExtension(0, 3) + }, + ChildInitializer = + { + PossibleCommands() + } + }, + _timeline.View().WithExtension(new GridPositionExtension(0, 4)), } }; @@ -341,7 +344,8 @@ public class MainWindow textBlock.Bind( textBlock, dc => dc == null ? _theme.DefaultForegroundColor : ToForegroundColor(dc.ViewMode.Value, dc.BaseItem.Type), - tb => tb.Foreground + tb => tb.Foreground, + v => v ); textBlock.Bind( textBlock, diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/RootViewModel.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/RootViewModel.cs index 72873ca..30c47ec 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/RootViewModel.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/RootViewModel.cs @@ -1,5 +1,6 @@ using FileTime.App.CommandPalette.ViewModels; using FileTime.App.Core.ViewModels; +using FileTime.App.Core.ViewModels.Timeline; using FileTime.ConsoleUI.App.Services; using FileTime.Core.Interactions; @@ -13,18 +14,21 @@ public class RootViewModel : IRootViewModel public IConsoleAppState AppState { get; } public ICommandPaletteViewModel CommandPalette { get; } public IDialogService DialogService { get; } + public ITimelineViewModel TimelineViewModel { get; } public event Action? FocusReadInputElement; public RootViewModel( IConsoleAppState appState, IPossibleCommandsViewModel possibleCommands, ICommandPaletteViewModel commandPalette, - IDialogService dialogService) + IDialogService dialogService, + ITimelineViewModel timelineViewModel) { AppState = appState; PossibleCommands = possibleCommands; CommandPalette = commandPalette; DialogService = dialogService; + TimelineViewModel = timelineViewModel; DialogService.ReadInput.PropertyChanged += (o, e) => { diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/Startup.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/Startup.cs index d05678e..2ebf001 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/Startup.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/Startup.cs @@ -37,6 +37,7 @@ public static class Startup services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); + services.TryAddSingleton(); return services; } } \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.Styles/DefaultTheme.cs b/src/ConsoleApp/FileTime.ConsoleUI.Styles/DefaultTheme.cs index a312c40..d6e206a 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.Styles/DefaultTheme.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.Styles/DefaultTheme.cs @@ -16,6 +16,8 @@ public record Theme( IColor? MarkedSelectedItemBackgroundColor, IColor? SelectedItemColor, IColor? SelectedTabBackgroundColor, + IColor? WarningForegroundColor, + IColor? ErrorForegroundColor, ListViewItemTheme ListViewItemTheme, Type? ForegroundColors, Type? BackgroundColors) : ITheme, IColorSampleProvider; @@ -34,6 +36,8 @@ public static class DefaultThemes MarkedSelectedItemBackgroundColor: Color256Colors.Foregrounds.Yellow, SelectedItemColor: Color256Colors.Foregrounds.Black, SelectedTabBackgroundColor: Color256Colors.Backgrounds.Green, + WarningForegroundColor: Color256Colors.Foregrounds.Yellow, + ErrorForegroundColor: Color256Colors.Foregrounds.Red, ListViewItemTheme: new( SelectedBackgroundColor: Color256Colors.Backgrounds.Gray, SelectedForegroundColor: Color256Colors.Foregrounds.Black @@ -54,6 +58,8 @@ public static class DefaultThemes MarkedSelectedItemBackgroundColor: ConsoleColors.Foregrounds.Yellow, SelectedItemColor: ConsoleColors.Foregrounds.Black, SelectedTabBackgroundColor: ConsoleColors.Backgrounds.Green, + WarningForegroundColor: ConsoleColors.Foregrounds.Yellow, + ErrorForegroundColor: ConsoleColors.Foregrounds.Red, ListViewItemTheme: new( SelectedBackgroundColor: ConsoleColors.Backgrounds.Gray, SelectedForegroundColor: ConsoleColors.Foregrounds.Black diff --git a/src/Core/FileTime.Core.Timeline/CommandScheduler.cs b/src/Core/FileTime.Core.Timeline/CommandScheduler.cs index 372e4d9..8be2142 100644 --- a/src/Core/FileTime.Core.Timeline/CommandScheduler.cs +++ b/src/Core/FileTime.Core.Timeline/CommandScheduler.cs @@ -13,7 +13,7 @@ public class CommandScheduler : ICommandScheduler private readonly Subject _containerToRefresh = new(); private readonly object _guard = new(); - private bool _isRunningEnabled = true; + private bool _isRunningEnabled = /*true*/ false; private bool _resourceIsInUse; public IObservable ContainerToRefresh { get; } diff --git a/src/FileTime.sln b/src/FileTime.sln index c13f61b..e6712a7 100644 --- a/src/FileTime.sln +++ b/src/FileTime.sln @@ -129,6 +129,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GeneralInputKey", "Library\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalUI.DependencyInjection", "Library\TerminalUI.DependencyInjection\TerminalUI.DependencyInjection.csproj", "{E72F6430-0E6E-4818-BD5F-114893ACB18E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalUI.Tests", "Library\TerminalUI.Tests\TerminalUI.Tests.csproj", "{30B6E288-F314-494B-8550-1329BFF664D2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -351,6 +353,10 @@ Global {E72F6430-0E6E-4818-BD5F-114893ACB18E}.Debug|Any CPU.Build.0 = Debug|Any CPU {E72F6430-0E6E-4818-BD5F-114893ACB18E}.Release|Any CPU.ActiveCfg = Release|Any CPU {E72F6430-0E6E-4818-BD5F-114893ACB18E}.Release|Any CPU.Build.0 = Release|Any CPU + {30B6E288-F314-494B-8550-1329BFF664D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {30B6E288-F314-494B-8550-1329BFF664D2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {30B6E288-F314-494B-8550-1329BFF664D2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {30B6E288-F314-494B-8550-1329BFF664D2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -411,6 +417,7 @@ Global {6C3C3151-9341-4792-9B0B-A11C0658524E} = {07CA18AA-B85D-4DEE-BB86-F569F6029853} {91AE5B64-042B-4660-A8E8-D247E6E14A1E} = {07CA18AA-B85D-4DEE-BB86-F569F6029853} {E72F6430-0E6E-4818-BD5F-114893ACB18E} = {07CA18AA-B85D-4DEE-BB86-F569F6029853} + {30B6E288-F314-494B-8550-1329BFF664D2} = {07CA18AA-B85D-4DEE-BB86-F569F6029853} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF} diff --git a/src/Library/TerminalUI.Tests/BindingTests.cs b/src/Library/TerminalUI.Tests/BindingTests.cs new file mode 100644 index 0000000..ab0e17b --- /dev/null +++ b/src/Library/TerminalUI.Tests/BindingTests.cs @@ -0,0 +1,259 @@ +using TerminalUI.Controls; +using TerminalUI.Extensions; +using TerminalUI.Tests.Models; + +namespace TerminalUI.Tests; + +public class BindingTests +{ + private static string TestMapperMethodStatic(string s) => s.ToUpper(); + private string TestMapperMethod(string s) => s.ToUpper(); + + [Fact] + public void StaticCaptureMethodBinding_ByDefault_ShouldSetValue() + { + var testViewModel = new TestViewModel + { + Text = "test" + }; + var grid = new Grid + { + DataContext = testViewModel + }; + var txtb1 = grid.CreateChild>(); + + txtb1.Bind( + txtb1, + vm => TestMapperMethodStatic(vm.Text), + t => t.Text, + v => v); + + Assert.Equal("TEST", txtb1.Text); + } + + [Fact] + public void StaticCaptureMethodBinding_AfterUpdate_ShouldSetValue() + { + var testViewModel = new TestViewModel + { + Text = "test" + }; + var grid = new Grid + { + DataContext = testViewModel + }; + var txtb1 = grid.CreateChild>(); + + txtb1.Bind( + txtb1, + vm => TestMapperMethodStatic(vm.Text), + t => t.Text, + v => v); + + testViewModel.Text = "Updated"; + + Assert.Equal("UPDATED", txtb1.Text); + } + + [Fact] + public void CaptureMethodBinding_ByDefault_ShouldSetValue() + { + var testViewModel = new TestViewModel + { + Text = "test" + }; + var grid = new Grid + { + DataContext = testViewModel + }; + var txtb1 = grid.CreateChild>(); + + txtb1.Bind( + txtb1, + vm => TestMapperMethod(vm.Text), + t => t.Text, + v => v); + + Assert.Equal("TEST", txtb1.Text); + } + + [Fact] + public void CaptureMethodBinding_AfterUpdate_ShouldSetValue() + { + var testViewModel = new TestViewModel + { + Text = "test" + }; + var grid = new Grid + { + DataContext = testViewModel + }; + var txtb1 = grid.CreateChild>(); + + txtb1.Bind( + txtb1, + vm => TestMapperMethod(vm.Text), + t => t.Text, + v => v); + + testViewModel.Text = "Updated"; + + Assert.Equal("UPDATED", txtb1.Text); + } + + [Fact] + public void FallbackValue_BindingFails_ShouldBeApplied() + { + var grid = new Grid(); + var txtb1 = grid.CreateChild>(); + + txtb1.Bind( + txtb1, + vm => vm.Items, + t => t.Text, + v => v.ToString(), + fallbackValue: "Fallback"); + + Assert.Equal("Fallback", txtb1.Text); + } + + [Fact] + public void ConditionalExpression_WhenTrue_ShouldResultInTrueValue() + { + var grid = new Grid(); + var txtb1 = grid.CreateChild>(); + + txtb1.Bind( + txtb1, + vm => vm == null ? "null" : "not null", + t => t.Text, + v => v); + + Assert.Equal("null", txtb1.Text); + } + + [Fact] + public void ConditionalExpression_WhenFalse_ShouldResultInFalseValue() + { + var testViewModel = new TestViewModel(); + var grid = new Grid + { + DataContext = testViewModel + }; + var txtb1 = grid.CreateChild>(); + + txtb1.Bind( + txtb1, + vm => vm == null ? "null" : "not null", + t => t.Text, + v => v); + + Assert.Equal("not null", txtb1.Text); + } + + [Fact] + public void ConditionalExpressionWithNestedValues_WhenTrue_ShouldResultInTrueNestedValue() + { + var testViewModel = new TestViewModel(); + var grid = new Grid + { + DataContext = testViewModel + }; + var txtb1 = grid.CreateChild>(); + + txtb1.Bind( + txtb1, + vm => vm != null ? vm.Items.Count : -1, + t => t.Text, + v => v.ToString()); + + Assert.Equal("1", txtb1.Text); + } + + [Fact] + public void ConditionalExpressionWithNestedValues_WhenFalse_ShouldResultInFalseNestedValue() + { + var testViewModel = new TestViewModel(); + var grid = new Grid + { + DataContext = testViewModel + }; + var txtb1 = grid.CreateChild>(); + + txtb1.Bind( + txtb1, + vm => vm == null ? -1 : vm.Items.Count, + t => t.Text, + v => v.ToString()); + + Assert.Equal("1", txtb1.Text); + } + + [Fact] + public void MixedBindingWithMethodCall_WhenRootChanged_ShouldUpdate() + { + var testViewModel = new TestViewModel(); + var grid = new Grid + { + DataContext = testViewModel + }; + var txtb1 = grid.CreateChild>(); + + txtb1.Bind( + txtb1, + vm => vm.Items[0].GetNestedItem(0).OtherItems.Count, + t => t.Text, + v => v.ToString()); + + testViewModel.Items = new() + { + TestNestedCollectionItem.Create(4, + TestNestedCollectionItem.Create( + 6, + TestNestedCollectionItem.Create(4) + ), + TestNestedCollectionItem.Create(1), + TestNestedCollectionItem.Create(2), + TestNestedCollectionItem.Create(3) + ), + TestNestedCollectionItem.Create(1), + TestNestedCollectionItem.Create(2), + }; + + Assert.Equal("6", txtb1.Text); + } + + [Fact] + public void NestedPropertyBinding_WhenRootChanged_ShouldUpdateText() + { + var testViewModel = new TestViewModel(); + var grid = new Grid + { + DataContext = testViewModel + }; + var txtb1 = grid.CreateChild>(); + + txtb1.Bind( + txtb1, + vm => vm.Items.Count, + t => t.Text, + v => v.ToString()); + + testViewModel.Items = new List + { + TestNestedCollectionItem.Create(4, + TestNestedCollectionItem.Create( + 6, + TestNestedCollectionItem.Create(4) + ), + TestNestedCollectionItem.Create(1), + TestNestedCollectionItem.Create(2), + TestNestedCollectionItem.Create(3) + ), + TestNestedCollectionItem.Create(1), + TestNestedCollectionItem.Create(2), + }; + + Assert.Equal("3", txtb1.Text); + } +} \ No newline at end of file diff --git a/src/Library/TerminalUI.Tests/GlobalUsings.cs b/src/Library/TerminalUI.Tests/GlobalUsings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/src/Library/TerminalUI.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/src/Library/TerminalUI.Tests/Models/TestCollectionItem.cs b/src/Library/TerminalUI.Tests/Models/TestCollectionItem.cs new file mode 100644 index 0000000..4a68f42 --- /dev/null +++ b/src/Library/TerminalUI.Tests/Models/TestCollectionItem.cs @@ -0,0 +1,10 @@ +namespace TerminalUI.Tests.Models; + +public class TestCollectionItem +{ + public List? Items1 { get; set; } = new() + { + new TestItem() {Name = "Name1"}, + new TestItem() {Name = "Name2"}, + }; +} \ No newline at end of file diff --git a/src/Library/TerminalUI.Tests/Models/TestItem.cs b/src/Library/TerminalUI.Tests/Models/TestItem.cs new file mode 100644 index 0000000..58e4fb8 --- /dev/null +++ b/src/Library/TerminalUI.Tests/Models/TestItem.cs @@ -0,0 +1,6 @@ +namespace TerminalUI.Tests.Models; + +public class TestItem +{ + public string? Name { get; set; } +} \ No newline at end of file diff --git a/src/Library/TerminalUI.Tests/Models/TestNestedCollectionItem.cs b/src/Library/TerminalUI.Tests/Models/TestNestedCollectionItem.cs new file mode 100644 index 0000000..dc07006 --- /dev/null +++ b/src/Library/TerminalUI.Tests/Models/TestNestedCollectionItem.cs @@ -0,0 +1,22 @@ +namespace TerminalUI.Tests.Models; + +public class TestNestedCollectionItem +{ + public List Items { get; set; } = new(); + public List OtherItems { get; set; } = new(); + + public TestNestedCollectionItem GetNestedItem(int index) => Items[index]; + public TestCollectionItem GetSimpleItem(int index) => OtherItems[index]; + + public static TestNestedCollectionItem Create(int otherItemCount, params TestNestedCollectionItem[] items) + { + var collection = new TestNestedCollectionItem() + { + Items = items.ToList() + }; + for (var i = 0; i < otherItemCount; i++) + collection.OtherItems.Add(new TestCollectionItem()); + + return collection; + } +} \ No newline at end of file diff --git a/src/Library/TerminalUI.Tests/Models/TestViewModel.cs b/src/Library/TerminalUI.Tests/Models/TestViewModel.cs new file mode 100644 index 0000000..1258b30 --- /dev/null +++ b/src/Library/TerminalUI.Tests/Models/TestViewModel.cs @@ -0,0 +1,32 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; +using PropertyChanged.SourceGenerator; + +namespace TerminalUI.Tests.Models; + +public sealed partial class TestViewModel : INotifyPropertyChanged +{ + [Notify] private List _items; + [Notify] private string _text; + + public TestViewModel() + { + _text = "Initial text"; + _items = new List + { + TestNestedCollectionItem.Create( + 3, + TestNestedCollectionItem.Create( + 1, + TestNestedCollectionItem.Create(2) + ), + new() + ) + }; + } + + public event PropertyChangedEventHandler? PropertyChanged; + + private void OnPropertyChanged([CallerMemberName] string? propertyName = null) + => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); +} \ No newline at end of file diff --git a/src/Library/TerminalUI.Tests/TerminalUI.Tests.csproj b/src/Library/TerminalUI.Tests/TerminalUI.Tests.csproj new file mode 100644 index 0000000..beeb9bb --- /dev/null +++ b/src/Library/TerminalUI.Tests/TerminalUI.Tests.csproj @@ -0,0 +1,33 @@ + + + + net7.0 + enable + enable + + false + true + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/src/Library/TerminalUI/Binding.cs b/src/Library/TerminalUI/Binding.cs index 29f792a..2628ad9 100644 --- a/src/Library/TerminalUI/Binding.cs +++ b/src/Library/TerminalUI/Binding.cs @@ -2,11 +2,12 @@ using System.Linq.Expressions; using System.Reflection; using TerminalUI.Controls; +using TerminalUI.ExpressionTrackers; using TerminalUI.Traits; namespace TerminalUI; -public sealed class Binding : PropertyTrackerBase +public sealed class Binding : PropertyTrackerBase, IDisposable { private readonly Func _dataContextMapper; private IView _dataSourceView; @@ -15,6 +16,7 @@ public sealed class Binding : Property private readonly Func _converter; private readonly TResult? _fallbackValue; private IDisposableCollection? _propertySourceDisposableCollection; + private readonly string _parameterName; public Binding( IView dataSourceView, @@ -23,7 +25,7 @@ public sealed class Binding : Property PropertyInfo targetProperty, Func converter, TResult? fallbackValue = default - ) : base(() => dataSourceView.DataContext, dataSourceExpression) + ) : base(dataSourceExpression) { ArgumentNullException.ThrowIfNull(dataSourceView); ArgumentNullException.ThrowIfNull(dataSourceExpression); @@ -37,14 +39,14 @@ public sealed class Binding : Property _converter = converter; _fallbackValue = fallbackValue; - UpdateTrackers(); + _parameterName = dataSourceExpression.Parameters[0].Name!; + Parameters.SetValue(_parameterName, dataSourceView.DataContext); + dataSourceView.PropertyChanged += View_PropertyChanged; - UpdateTargetProperty(); + Update(true); AddToSourceDisposables(propertySource); - - dataSourceView.AddDisposable(this); } private void AddToSourceDisposables(object? propertySource) @@ -60,18 +62,23 @@ public sealed class Binding : Property { if (e.PropertyName != nameof(IView.DataContext)) return; - UpdateTrackers(); - UpdateTargetProperty(); + Parameters.SetValue(_parameterName, _dataSourceView.DataContext); + Update(true); } - protected override void Update(string propertyPath) => UpdateTargetProperty(); - - private void UpdateTargetProperty() + protected override void Update(bool couldCompute) { - TResult value; + TResult? value; try { - value = _converter(_dataContextMapper(_dataSourceView.DataContext)); + if (couldCompute) + { + value = _converter(_dataContextMapper(_dataSourceView.DataContext)); + } + else + { + value = _fallbackValue; + } } catch { @@ -87,9 +94,9 @@ public sealed class Binding : Property } } - public override void Dispose() + public void Dispose() { - base.Dispose(); + //base.Dispose(); _propertySourceDisposableCollection?.RemoveDisposable(this); _dataSourceView.RemoveDisposable(this); _dataSourceView.PropertyChanged -= View_PropertyChanged; diff --git a/src/Library/TerminalUI/Controls/TextBlock.cs b/src/Library/TerminalUI/Controls/TextBlock.cs index 5ed72f6..e12576a 100644 --- a/src/Library/TerminalUI/Controls/TextBlock.cs +++ b/src/Library/TerminalUI/Controls/TextBlock.cs @@ -27,11 +27,11 @@ public sealed partial class TextBlock : View, T>, IDisplayView public TextBlock() { - this.Bind( + /*this.Bind( this, dc => dc == null ? string.Empty : dc.ToString(), tb => tb.Text - ); + );*/ RerenderProperties.Add(nameof(Text)); RerenderProperties.Add(nameof(TextAlignment)); diff --git a/src/Library/TerminalUI/EventLoop.cs b/src/Library/TerminalUI/EventLoop.cs index e5e8e34..79c8e34 100644 --- a/src/Library/TerminalUI/EventLoop.cs +++ b/src/Library/TerminalUI/EventLoop.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Logging; +using System.Diagnostics; +using Microsoft.Extensions.Logging; namespace TerminalUI; @@ -32,14 +33,15 @@ public class EventLoop : IEventLoop { foreach (var action in _permanentQueue) { - try - { + /*try + {*/ action(); - } + /*} catch (Exception e) { + Debug.Fail(e.Message); _logger.LogError(e, "Error while processing action in permanent queue"); - } + }*/ } } } \ No newline at end of file diff --git a/src/Library/TerminalUI/ExpressionTrackers/BinaryTracker.cs b/src/Library/TerminalUI/ExpressionTrackers/BinaryTracker.cs new file mode 100644 index 0000000..20c22d6 --- /dev/null +++ b/src/Library/TerminalUI/ExpressionTrackers/BinaryTracker.cs @@ -0,0 +1,37 @@ +using System.Linq.Expressions; + +namespace TerminalUI.ExpressionTrackers; + +public class BinaryTracker : ExpressionTrackerBase +{ + private readonly IExpressionTracker _leftExpressionTracker; + private readonly IExpressionTracker _rightExpressionTracker; + private readonly Func _computer; + + public BinaryTracker( + BinaryExpression binaryExpression, + IExpressionTracker leftExpressionTracker, + IExpressionTracker rightExpressionTracker) + { + _leftExpressionTracker = leftExpressionTracker; + _rightExpressionTracker = rightExpressionTracker; + ArgumentNullException.ThrowIfNull(leftExpressionTracker); + ArgumentNullException.ThrowIfNull(rightExpressionTracker); + + SubscribeToValueChanges = false; + SubscribeToTracker(leftExpressionTracker); + SubscribeToTracker(rightExpressionTracker); + + _computer = binaryExpression.NodeType switch + { + ExpressionType.Equal => (v1, v2) => Equals(v1, v2), + ExpressionType.NotEqual => (v1, v2) => !Equals(v1, v2), + _ => throw new NotImplementedException() + }; + + UpdateValueAndChangeTrackers(); + } + + protected override object? ComputeValue() + => _computer(_leftExpressionTracker.GetValue(), _rightExpressionTracker.GetValue()); +} \ No newline at end of file diff --git a/src/Library/TerminalUI/ExpressionTrackers/ConditionalTracker.cs b/src/Library/TerminalUI/ExpressionTrackers/ConditionalTracker.cs new file mode 100644 index 0000000..c636f97 --- /dev/null +++ b/src/Library/TerminalUI/ExpressionTrackers/ConditionalTracker.cs @@ -0,0 +1,44 @@ +using System.Linq.Expressions; + +namespace TerminalUI.ExpressionTrackers; + +public class ConditionalTracker : ExpressionTrackerBase +{ + private readonly IExpressionTracker _testExpressionTracker; + private readonly IExpressionTracker _ifTrueExpressionTracker; + private readonly IExpressionTracker _ifFalseExpressionTracker; + + public ConditionalTracker( + ConditionalExpression conditionalExpression, + IExpressionTracker testExpressionTracker, + IExpressionTracker ifTrueExpressionTracker, + IExpressionTracker ifFalseExpressionTracker) + { + ArgumentNullException.ThrowIfNull(conditionalExpression); + ArgumentNullException.ThrowIfNull(testExpressionTracker); + ArgumentNullException.ThrowIfNull(ifTrueExpressionTracker); + + SubscribeToValueChanges = false; + + _testExpressionTracker = testExpressionTracker; + _ifTrueExpressionTracker = ifTrueExpressionTracker; + _ifFalseExpressionTracker = ifFalseExpressionTracker; + + SubscribeToTracker(testExpressionTracker); + SubscribeToTracker(ifTrueExpressionTracker); + SubscribeToTracker(ifFalseExpressionTracker); + + UpdateValueAndChangeTrackers(); + } + + protected override object? ComputeValue() + { + var testValue = _testExpressionTracker.GetValue(); + return testValue switch + { + true => _ifTrueExpressionTracker.GetValue(), + false => _ifFalseExpressionTracker.GetValue(), + _ => throw new NotSupportedException($"Conditional expression must evaluate to a boolean value, but {testValue} ({testValue.GetType().Name}) is not that.") + }; + } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/ExpressionTrackers/ConstantTracker.cs b/src/Library/TerminalUI/ExpressionTrackers/ConstantTracker.cs new file mode 100644 index 0000000..80bfa2a --- /dev/null +++ b/src/Library/TerminalUI/ExpressionTrackers/ConstantTracker.cs @@ -0,0 +1,13 @@ +namespace TerminalUI.ExpressionTrackers; + +public class ConstantTracker : ExpressionTrackerBase +{ + private readonly object? _value; + + public ConstantTracker(object? value) + { + _value = value; + UpdateValueAndChangeTrackers(); + } + protected override object? ComputeValue() => _value; +} \ No newline at end of file diff --git a/src/Library/TerminalUI/ExpressionTrackers/ExpressionParameterTrackerCollection.cs b/src/Library/TerminalUI/ExpressionTrackers/ExpressionParameterTrackerCollection.cs new file mode 100644 index 0000000..0a94a06 --- /dev/null +++ b/src/Library/TerminalUI/ExpressionTrackers/ExpressionParameterTrackerCollection.cs @@ -0,0 +1,19 @@ +using System.Collections.ObjectModel; + +namespace TerminalUI.ExpressionTrackers; + +public class ExpressionParameterTrackerCollection +{ + private readonly Dictionary _values = new(); + + public ReadOnlyDictionary Values => new(_values); + public event Action? ValueChanged; + + public void SetValue(string name, object? value) + { + ArgumentException.ThrowIfNullOrEmpty(name); + + _values[name] = value; + ValueChanged?.Invoke(name, value); + } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/ExpressionTrackers/ExpressionTrackerBase.cs b/src/Library/TerminalUI/ExpressionTrackers/ExpressionTrackerBase.cs new file mode 100644 index 0000000..c40bfab --- /dev/null +++ b/src/Library/TerminalUI/ExpressionTrackers/ExpressionTrackerBase.cs @@ -0,0 +1,82 @@ +using System.Collections.Specialized; +using System.ComponentModel; + +namespace TerminalUI.ExpressionTrackers; + +public abstract class ExpressionTrackerBase : IExpressionTracker +{ + private object? _currentValue; + public List TrackedPropertyNames { get; } = new(); + + protected bool SubscribeToValueChanges { get; set; } = true; + + public event Action? PropertyChanged; + public event Action? Update; + public object? GetValue() => _currentValue; + protected abstract object? ComputeValue(); + + protected void SubscribeToTracker(IExpressionTracker? expressionTracker) + { + if (expressionTracker is null) return; + expressionTracker.Update += UpdateValueAndChangeTrackers; + } + + protected void UpdateValueAndChangeTrackers() => UpdateValueAndChangeTrackers(true); + + private void UpdateValueAndChangeTrackers(bool couldCompute) + { + if (SubscribeToValueChanges) + { + if (_currentValue is INotifyPropertyChanged oldNotifyPropertyChanged) + oldNotifyPropertyChanged.PropertyChanged -= OnPropertyChanged; + if (_currentValue is INotifyCollectionChanged collectionChanged) + collectionChanged.CollectionChanged -= OnCollectionChanged; + } + + var useNull = false; + try + { + if (couldCompute) + { + _currentValue = ComputeValue(); + + if (SubscribeToValueChanges) + { + if (_currentValue is INotifyPropertyChanged notifyPropertyChanged) + notifyPropertyChanged.PropertyChanged += OnPropertyChanged; + if (_currentValue is INotifyCollectionChanged collectionChanged) + collectionChanged.CollectionChanged += OnCollectionChanged; + } + + Update?.Invoke(true); + } + else + { + useNull = true; + } + } + catch (Exception e) + { + useNull = true; + } + + if (useNull) + { + _currentValue = null; + Update?.Invoke(false); + } + } + + private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + => UpdateValueAndChangeTrackers(); + + private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName is null) return; + + if (TrackedPropertyNames.Contains(e.PropertyName)) + { + PropertyChanged?.Invoke(e.PropertyName); + } + } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/ExpressionTrackers/IExpressionTracker.cs b/src/Library/TerminalUI/ExpressionTrackers/IExpressionTracker.cs new file mode 100644 index 0000000..4304350 --- /dev/null +++ b/src/Library/TerminalUI/ExpressionTrackers/IExpressionTracker.cs @@ -0,0 +1,9 @@ +namespace TerminalUI.ExpressionTrackers; + +public interface IExpressionTracker +{ + List TrackedPropertyNames { get; } + event Action? PropertyChanged; + event Action? Update; + object? GetValue(); +} \ No newline at end of file diff --git a/src/Library/TerminalUI/ExpressionTrackers/MemberTracker.cs b/src/Library/TerminalUI/ExpressionTrackers/MemberTracker.cs new file mode 100644 index 0000000..ad2f522 --- /dev/null +++ b/src/Library/TerminalUI/ExpressionTrackers/MemberTracker.cs @@ -0,0 +1,82 @@ +using System.Linq.Expressions; +using System.Reflection; + +namespace TerminalUI.ExpressionTrackers; + +public sealed class MemberTracker : ExpressionTrackerBase +{ + private readonly IExpressionTracker? _parentTracker; + private readonly string _memberName; + private readonly Func _valueProvider; + + public MemberTracker(MemberExpression memberExpression, IExpressionTracker? parentTracker) + { + ArgumentNullException.ThrowIfNull(memberExpression); + _parentTracker = parentTracker; + + if (parentTracker is not null) + { + parentTracker.PropertyChanged += propertyName => + { + if (propertyName == _memberName) + { + UpdateValueAndChangeTrackers(); + } + }; + } + + if (memberExpression.Member is PropertyInfo propertyInfo) + { + _memberName = propertyInfo.Name; + parentTracker?.TrackedPropertyNames.Add(propertyInfo.Name); + + if (propertyInfo.GetMethod is { } getMethod) + { + _valueProvider = () => CallPropertyInfo(propertyInfo); + } + else + { + throw new NotSupportedException( + $"Try to get value of a property without a getter: {propertyInfo.Name} in {propertyInfo.DeclaringType?.Name ?? ""}." + ); + } + } + else if (memberExpression.Member is FieldInfo fieldInfo) + { + _memberName = fieldInfo.Name; + parentTracker?.TrackedPropertyNames.Add(fieldInfo.Name); + + _valueProvider = () => CallFieldInfo(fieldInfo); + } + else + { + throw new NotSupportedException($"Could not determine source type of expression {memberExpression} with parent {parentTracker}"); + } + + SubscribeToTracker(parentTracker); + UpdateValueAndChangeTrackers(); + } + + private object? CallPropertyInfo(PropertyInfo propertyInfo) + { + var obj = _parentTracker?.GetValue(); + if (obj is null && !propertyInfo.GetMethod!.IsStatic) return null; + + return propertyInfo.GetValue(_parentTracker?.GetValue()); + } + + private object? CallFieldInfo(FieldInfo fieldInfo) + { + var obj = _parentTracker?.GetValue(); + if (obj is null && !fieldInfo.IsStatic) return null; + + return fieldInfo.GetValue(_parentTracker?.GetValue()); + } + + protected override object? ComputeValue() + { + var v = _valueProvider(); + + return v; + } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/ExpressionTrackers/MethodCallTracker.cs b/src/Library/TerminalUI/ExpressionTrackers/MethodCallTracker.cs new file mode 100644 index 0000000..ff1741c --- /dev/null +++ b/src/Library/TerminalUI/ExpressionTrackers/MethodCallTracker.cs @@ -0,0 +1,40 @@ +using System.Linq.Expressions; +using System.Reflection; + +namespace TerminalUI.ExpressionTrackers; + +public sealed class MethodCallTracker : ExpressionTrackerBase +{ + private readonly MethodInfo _method; + private readonly IExpressionTracker? _objectTracker; + private readonly List _argumentTrackers; + + public MethodCallTracker( + MethodCallExpression methodCallExpression, + IExpressionTracker? objectTracker, + IEnumerable argumentTrackers) + { + _method = methodCallExpression.Method; + _objectTracker = objectTracker; + _argumentTrackers = argumentTrackers.ToList(); + + if (objectTracker is not null) + { + SubscribeToTracker(objectTracker); + } + + foreach (var expressionTracker in _argumentTrackers) + { + SubscribeToTracker(expressionTracker); + } + + UpdateValueAndChangeTrackers(); + } + + protected override object? ComputeValue() + { + var obj = _objectTracker?.GetValue(); + if (obj is null && !_method.IsStatic) return null; + return _method.Invoke(obj, _argumentTrackers.Select(t => t.GetValue()).ToArray()); + } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/ExpressionTrackers/ParameterTracker.cs b/src/Library/TerminalUI/ExpressionTrackers/ParameterTracker.cs new file mode 100644 index 0000000..09ca9d1 --- /dev/null +++ b/src/Library/TerminalUI/ExpressionTrackers/ParameterTracker.cs @@ -0,0 +1,31 @@ +using System.Linq.Expressions; + +namespace TerminalUI.ExpressionTrackers; + +public sealed class ParameterTracker : ExpressionTrackerBase +{ + private readonly ExpressionParameterTrackerCollection _trackerCollection; + private readonly string _parameterName; + + public ParameterTracker( + ParameterExpression parameterExpression, + ExpressionParameterTrackerCollection trackerCollection, + string parameterName) + { + _trackerCollection = trackerCollection; + _parameterName = parameterName; + + trackerCollection.ValueChanged += TrackerCollectionOnValueChanged; + + UpdateValueAndChangeTrackers(); + } + + private void TrackerCollectionOnValueChanged(string parameterName, object? newValue) + => UpdateValueAndChangeTrackers(); + + protected override object? ComputeValue() + { + _trackerCollection.Values.TryGetValue(_parameterName, out var v); + return v; + } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/ExpressionTrackers/UnaryTracker.cs b/src/Library/TerminalUI/ExpressionTrackers/UnaryTracker.cs new file mode 100644 index 0000000..43c8b1c --- /dev/null +++ b/src/Library/TerminalUI/ExpressionTrackers/UnaryTracker.cs @@ -0,0 +1,46 @@ +using System.Linq.Expressions; + +namespace TerminalUI.ExpressionTrackers; + +public class UnaryTracker : ExpressionTrackerBase +{ + private readonly IExpressionTracker _operandTracker; + private readonly Func _operator; + + public UnaryTracker(UnaryExpression unaryExpression, IExpressionTracker operandTracker) + { + _operandTracker = operandTracker; + ArgumentNullException.ThrowIfNull(unaryExpression); + ArgumentNullException.ThrowIfNull(operandTracker); + + SubscribeToValueChanges = false; + + _operator = unaryExpression.NodeType switch + { + ExpressionType.Negate => Negate, + ExpressionType.Not => o => o is bool b ? !b : null, + ExpressionType.Convert => o => o, + _ => throw new NotSupportedException($"Unary expression of type {unaryExpression.NodeType} is not supported.") + }; + + SubscribeToTracker(operandTracker); + + UpdateValueAndChangeTrackers(); + } + + private static object? Negate(object? source) + { + if (source is null) return null; + return source switch + { + int i => -i, + long l => -l, + float f => -f, + double d => -d, + decimal d => -d, + _ => throw new NotSupportedException($"Unary negation is not supported for type {source.GetType().Name}.") + }; + } + + protected override object? ComputeValue() => _operator(_operandTracker.GetValue()); +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Extensions/Binding.cs b/src/Library/TerminalUI/Extensions/BindingExtensions.cs similarity index 97% rename from src/Library/TerminalUI/Extensions/Binding.cs rename to src/Library/TerminalUI/Extensions/BindingExtensions.cs index 0061d84..c122623 100644 --- a/src/Library/TerminalUI/Extensions/Binding.cs +++ b/src/Library/TerminalUI/Extensions/BindingExtensions.cs @@ -4,7 +4,7 @@ using TerminalUI.Controls; namespace TerminalUI.Extensions; -public static class Binding +public static class BindingExtensions { public static Binding Bind( this TView targetView, diff --git a/src/Library/TerminalUI/Extensions/ViewExtensions.cs b/src/Library/TerminalUI/Extensions/ViewExtensions.cs index 69078cb..118fedf 100644 --- a/src/Library/TerminalUI/Extensions/ViewExtensions.cs +++ b/src/Library/TerminalUI/Extensions/ViewExtensions.cs @@ -28,7 +28,7 @@ public static class ViewExtensions public static TItem WithPropertyChangedHandler( this TItem dataSource, Expression> dataSourceExpression, - Action handler) + Action handler) { new PropertyChangedHandler ( diff --git a/src/Library/TerminalUI/PropertyChangeTracker.cs b/src/Library/TerminalUI/PropertyChangeTracker.cs deleted file mode 100644 index 08d04aa..0000000 --- a/src/Library/TerminalUI/PropertyChangeTracker.cs +++ /dev/null @@ -1,149 +0,0 @@ -using System.ComponentModel; - -namespace TerminalUI; - -public interface IPropertyChangeTracker : IDisposable -{ - string Name { get; } - string Path { get; } - Dictionary Children { get; } -} - -public abstract class PropertyChangeTrackerBase : IPropertyChangeTracker -{ - public string Name { get; } - public string Path { get; } - public Dictionary Children { get; } = new(); - - protected PropertyChangeTrackerBase(string name, string path) - { - Name = name; - Path = path; - } - - public virtual void Dispose() - { - foreach (var propertyChangeTracker in Children.Values) - { - propertyChangeTracker.Dispose(); - } - } -} - -public class PropertyChangeTracker : PropertyChangeTrackerBase -{ - private readonly PropertyTrackTreeItem _propertyTrackTreeItem; - private readonly INotifyPropertyChanged _target; - private readonly IEnumerable _propertiesToListen; - private readonly Action _updateBinding; - - public PropertyChangeTracker( - string name, - string path, - PropertyTrackTreeItem propertyTrackTreeItem, - INotifyPropertyChanged target, - IEnumerable propertiesToListen, - Action updateBinding) : base(name, path) - { - _propertyTrackTreeItem = propertyTrackTreeItem; - _target = target; - _propertiesToListen = propertiesToListen; - _updateBinding = updateBinding; - target.PropertyChanged += Target_PropertyChanged; - } - - private void Target_PropertyChanged(object? sender, PropertyChangedEventArgs e) - { - var propertyName = e.PropertyName; - if (propertyName is null || !_propertiesToListen.Contains(propertyName)) - { - return; - } - - Children.Remove(propertyName); - - var newChild = PropertyChangeHelper.CreatePropertyTracker( - Path, - _propertyTrackTreeItem.Children[propertyName], - _target.GetType().GetProperty(propertyName)?.GetValue(_target), - _updateBinding - ); - - if (newChild is not null) - { - Children.Add(propertyName, newChild); - } - - _updateBinding(propertyName); - } - - public override void Dispose() - { - _target.PropertyChanged -= Target_PropertyChanged; - - base.Dispose(); - } -} - -public class NonSubscriberPropertyChangeTracker : PropertyChangeTrackerBase -{ - public NonSubscriberPropertyChangeTracker(string name, string path) : base(name, path) - { - } -} - -public class PropertyTrackTreeItem -{ - public string Name { get; } - public Dictionary Children { get; } = new(); - - public PropertyTrackTreeItem(string name) - { - Name = name; - } -} - -public static class PropertyChangeHelper -{ - internal static IPropertyChangeTracker? CreatePropertyTracker( - string? path, - PropertyTrackTreeItem propertyTrackTreeItem, - object? obj, - Action updateBinding - ) - { - if (obj is null) return null; - - path = path is null ? propertyTrackTreeItem.Name : path + "." + propertyTrackTreeItem.Name; - - IPropertyChangeTracker tracker = obj is INotifyPropertyChanged notifyPropertyChanged - ? new PropertyChangeTracker( - propertyTrackTreeItem.Name, - path, - propertyTrackTreeItem, - notifyPropertyChanged, - propertyTrackTreeItem.Children.Keys, - updateBinding - ) - : new NonSubscriberPropertyChangeTracker( - propertyTrackTreeItem.Name, - path); - - foreach (var (propertyName, trackerTreeItem) in propertyTrackTreeItem.Children) - { - var childTracker = CreatePropertyTracker( - path, - trackerTreeItem, - obj.GetType().GetProperty(propertyName)?.GetValue(obj), - updateBinding - ); - - if (childTracker is not null) - { - tracker.Children.Add(propertyName, childTracker); - } - } - - return tracker; - } -} \ No newline at end of file diff --git a/src/Library/TerminalUI/PropertyChangedHandler.cs b/src/Library/TerminalUI/PropertyChangedHandler.cs index c639f8c..4768334 100644 --- a/src/Library/TerminalUI/PropertyChangedHandler.cs +++ b/src/Library/TerminalUI/PropertyChangedHandler.cs @@ -2,44 +2,48 @@ namespace TerminalUI; -public sealed class PropertyChangedHandler : PropertyTrackerBase, IDisposable +public sealed class PropertyChangedHandler : PropertyTrackerBase { private readonly TItem _dataSource; - private readonly Action _handler; - private readonly PropertyTrackTreeItem? _propertyTrackTreeItem; + private readonly Action _handler; private readonly Func _propertyValueGenerator; public PropertyChangedHandler( TItem dataSource, Expression> dataSourceExpression, - Action handler - ) : base(() => dataSource, dataSourceExpression) + Action handler + ) : base(dataSourceExpression!) { - _dataSource = dataSource; - _handler = handler; ArgumentNullException.ThrowIfNull(dataSource); ArgumentNullException.ThrowIfNull(dataSourceExpression); ArgumentNullException.ThrowIfNull(handler); + + _dataSource = dataSource; + _handler = handler; + + Parameters.SetValue(dataSourceExpression.Parameters[0].Name!, dataSource); - _propertyTrackTreeItem = CreateTrackingTree(dataSourceExpression); _propertyValueGenerator = dataSourceExpression.Compile(); - UpdateTrackers(); + Update(true); } - protected override void Update(string propertyPath) + protected override void Update(bool couldCompute) { TExpressionResult? value = default; var parsed = false; try { - value = _propertyValueGenerator(_dataSource); - parsed = true; + if (couldCompute) + { + value = _propertyValueGenerator(_dataSource); + parsed = true; + } } catch { } - _handler(propertyPath, parsed, value); + _handler(parsed, value); } } \ No newline at end of file diff --git a/src/Library/TerminalUI/PropertyTrackerBase.cs b/src/Library/TerminalUI/PropertyTrackerBase.cs index 54aa344..eb20281 100644 --- a/src/Library/TerminalUI/PropertyTrackerBase.cs +++ b/src/Library/TerminalUI/PropertyTrackerBase.cs @@ -1,145 +1,92 @@ using System.Linq.Expressions; -using System.Reflection; +using TerminalUI.ExpressionTrackers; namespace TerminalUI; -public abstract class PropertyTrackerBase : IDisposable +public abstract class PropertyTrackerBase { - private readonly Func _source; - protected PropertyTrackTreeItem? PropertyTrackTreeItem { get; } - protected IPropertyChangeTracker? PropertyChangeTracker { get; private set; } + private readonly IExpressionTracker _tracker; + protected ExpressionParameterTrackerCollection Parameters { get; } = new(); - protected PropertyTrackerBase( - Func source, - Expression> dataSourceExpression) + protected PropertyTrackerBase(Expression> dataSourceExpression) { ArgumentNullException.ThrowIfNull(dataSourceExpression); - _source = source; - PropertyTrackTreeItem = CreateTrackingTree(dataSourceExpression); + _tracker = FindReactiveProperties(dataSourceExpression.Body, Parameters); + _tracker.Update += Update; } - protected PropertyTrackTreeItem? CreateTrackingTree(Expression> dataContextExpression) + private IExpressionTracker FindReactiveProperties(Expression? expression, ExpressionParameterTrackerCollection parameters) { - var properties = new List(); - FindReactiveProperties(dataContextExpression, properties); - - if (properties.Count > 0) + if (expression is ConditionalExpression conditionalExpression) { - var rootItem = new PropertyTrackTreeItem(null!); - foreach (var property in properties) - { - var pathParts = property.Split('.'); - var currentItem = rootItem; - for (var i = 0; i < pathParts.Length; i++) - { - if (!currentItem.Children.TryGetValue(pathParts[i], out var child)) - { - child = new PropertyTrackTreeItem(pathParts[i]); - currentItem.Children.Add(pathParts[i], child); - } + var testTracker = FindReactiveProperties(conditionalExpression.Test, parameters); + var trueTracker = FindReactiveProperties(conditionalExpression.IfTrue, parameters); + var falseTracker = FindReactiveProperties(conditionalExpression.IfFalse, parameters); - currentItem = child; - } - } - - return rootItem; - } - - return null; - } - - private string? FindReactiveProperties(Expression? expression, List properties) - { - if (expression is null) return ""; - - if (expression is LambdaExpression lambdaExpression) - { - SavePropertyPath(FindReactiveProperties(lambdaExpression.Body, properties)); - } - else if (expression is ConditionalExpression conditionalExpression) - { - SavePropertyPath(FindReactiveProperties(conditionalExpression.Test, properties)); - SavePropertyPath(FindReactiveProperties(conditionalExpression.IfTrue, properties)); - SavePropertyPath(FindReactiveProperties(conditionalExpression.IfFalse, properties)); + return new ConditionalTracker( + conditionalExpression, + testTracker, + trueTracker, + falseTracker); } else if (expression is MemberExpression memberExpression) { + IExpressionTracker? parentExpressionTracker = null; if (memberExpression.Expression is not null) { - FindReactiveProperties(memberExpression.Expression, properties); - - if (FindReactiveProperties(memberExpression.Expression, properties) is { } path - && memberExpression.Member is PropertyInfo dataContextPropertyInfo) - { - path += "." + memberExpression.Member.Name; - return path; - } + parentExpressionTracker = FindReactiveProperties(memberExpression.Expression, parameters); } + + return new MemberTracker(memberExpression, parentExpressionTracker); } else if (expression is MethodCallExpression methodCallExpression) { - if (methodCallExpression.Object is - { - NodeType: - not ExpressionType.Parameter - and not ExpressionType.Constant - } methodObject) + IExpressionTracker? objectTracker = null; + if (methodCallExpression.Object is { } methodObject) { - SavePropertyPath(FindReactiveProperties(methodObject, properties)); + objectTracker = FindReactiveProperties(methodObject, parameters); } + var argumentTrackers = new List(methodCallExpression.Arguments.Count); foreach (var argument in methodCallExpression.Arguments) { - SavePropertyPath(FindReactiveProperties(argument, properties)); + var argumentTracker = FindReactiveProperties(argument, parameters); + argumentTrackers.Add(argumentTracker); } + + return new MethodCallTracker(methodCallExpression, objectTracker, argumentTrackers); } else if (expression is BinaryExpression binaryExpression) { - SavePropertyPath(FindReactiveProperties(binaryExpression.Left, properties)); - SavePropertyPath(FindReactiveProperties(binaryExpression.Right, properties)); + var leftTracker = FindReactiveProperties(binaryExpression.Left, parameters); + var rightTracker = FindReactiveProperties(binaryExpression.Right, parameters); + + return new BinaryTracker(binaryExpression, leftTracker, rightTracker); } else if (expression is UnaryExpression unaryExpression) { - return FindReactiveProperties(unaryExpression.Operand, properties); + var operandTracker = FindReactiveProperties(unaryExpression.Operand, parameters); + return new UnaryTracker(unaryExpression, operandTracker); } else if (expression is ParameterExpression parameterExpression) { - if (parameterExpression.Type == typeof(TSource)) + if (parameterExpression.Name is { } name) { - return ""; + return new ParameterTracker(parameterExpression, parameters, name); } } - - return null; - - void SavePropertyPath(string? path) + else if (expression is ConstantExpression constantExpression) { - if (path is null) return; - path = path.TrimStart('.'); - properties.Add(path); + return new ConstantTracker(constantExpression.Value); } + /*else if (expression is not ConstantExpression) + { + Debug.Assert(false, "Unknown expression type " + expression.GetType()); + }*/ + + throw new NotSupportedException(); } - protected void UpdateTrackers() - { - if (PropertyChangeTracker is not null) - { - PropertyChangeTracker.Dispose(); - } - - if (PropertyTrackTreeItem is not null) - { - PropertyChangeTracker = PropertyChangeHelper.CreatePropertyTracker( - null, - PropertyTrackTreeItem, - _source(), - Update - ); - } - } - - protected abstract void Update(string propertyPath); - - public virtual void Dispose() => PropertyChangeTracker?.Dispose(); + protected abstract void Update(bool couldCompute); } \ No newline at end of file