New binding mechanism: Expression tracking

This commit is contained in:
2023-08-15 13:17:42 +02:00
parent 335433562a
commit b792639635
35 changed files with 971 additions and 311 deletions

View File

@@ -1,5 +1,6 @@
using FileTime.App.CommandPalette.ViewModels; using FileTime.App.CommandPalette.ViewModels;
using FileTime.App.Core.ViewModels; using FileTime.App.Core.ViewModels;
using FileTime.App.Core.ViewModels.Timeline;
using FileTime.ConsoleUI.App.Services; using FileTime.ConsoleUI.App.Services;
using FileTime.Core.Interactions; using FileTime.Core.Interactions;
@@ -13,5 +14,6 @@ public interface IRootViewModel
string MachineName { get; } string MachineName { get; }
ICommandPaletteViewModel CommandPalette { get; } ICommandPaletteViewModel CommandPalette { get; }
IDialogService DialogService { get; } IDialogService DialogService { get; }
ITimelineViewModel TimelineViewModel { get; }
event Action<IInputElement>? FocusReadInputElement; event Action<IInputElement>? FocusReadInputElement;
} }

View File

@@ -2,7 +2,11 @@
using FileTime.App.Core.Services; using FileTime.App.Core.Services;
using FileTime.App.Core.ViewModels; using FileTime.App.Core.ViewModels;
using FileTime.ConsoleUI.App.KeyInputHandling; using FileTime.ConsoleUI.App.KeyInputHandling;
using FileTime.Core.Command.CreateContainer;
using FileTime.Core.Models;
using FileTime.Core.Timeline;
using GeneralInputKey; using GeneralInputKey;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using TerminalUI; using TerminalUI;
using TerminalUI.ConsoleDrivers; using TerminalUI.ConsoleDrivers;
@@ -27,6 +31,7 @@ public class App : IApplication
private readonly IConsoleDriver _consoleDriver; private readonly IConsoleDriver _consoleDriver;
private readonly IAppState _appState; private readonly IAppState _appState;
private readonly ILogger<App> _logger; private readonly ILogger<App> _logger;
private readonly IServiceProvider _serviceProvider;
private readonly IKeyInputHandlerService _keyInputHandlerService; private readonly IKeyInputHandlerService _keyInputHandlerService;
private readonly Thread _renderThread; private readonly Thread _renderThread;
@@ -39,7 +44,8 @@ public class App : IApplication
IApplicationContext applicationContext, IApplicationContext applicationContext,
IConsoleDriver consoleDriver, IConsoleDriver consoleDriver,
IAppState appState, IAppState appState,
ILogger<App> logger) ILogger<App> logger,
IServiceProvider serviceProvider)
{ {
_lifecycleService = lifecycleService; _lifecycleService = lifecycleService;
_keyInputHandlerService = keyInputHandlerService; _keyInputHandlerService = keyInputHandlerService;
@@ -50,6 +56,7 @@ public class App : IApplication
_consoleDriver = consoleDriver; _consoleDriver = consoleDriver;
_appState = appState; _appState = appState;
_logger = logger; _logger = logger;
_serviceProvider = serviceProvider;
_renderThread = new Thread(Render); _renderThread = new Thread(Render);
} }
@@ -74,6 +81,12 @@ public class App : IApplication
var focusManager = _applicationContext.FocusManager; var focusManager = _applicationContext.FocusManager;
var command = _serviceProvider.GetRequiredService<CreateContainerCommand>();
command.Init(new FullName("local/C:/Test3"), "container1");
var scheduler = _serviceProvider.GetRequiredService<ICommandScheduler>();
scheduler.AddCommand(command);
while (_applicationContext.IsRunning) while (_applicationContext.IsRunning)
{ {
try try

View File

@@ -129,7 +129,7 @@ public class CommandPalette
}; };
root.WithPropertyChangedHandler(r => r.IsVisible, root.WithPropertyChangedHandler(r => r.IsVisible,
(_, _, isVisible) => (_, isVisible) =>
{ {
if (isVisible) if (isVisible)
{ {

View File

@@ -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<IRootViewModel> View()
{
var root = new Grid<IRootViewModel>
{
ChildInitializer =
{
new ItemsControl<IRootViewModel, ICommandTimeStateViewModel>()
{
ItemTemplate = () =>
{
var grid = new Grid<ICommandTimeStateViewModel>()
{
ChildInitializer =
{
new TextBlock<ICommandTimeStateViewModel>()
{
}.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;
}
}

View File

@@ -1,17 +1,13 @@
using System.Collections.Specialized; using FileTime.App.Core.Models.Enums;
using System.ComponentModel;
using FileTime.App.Core.Models.Enums;
using FileTime.App.Core.ViewModels; using FileTime.App.Core.ViewModels;
using FileTime.ConsoleUI.App.Controls; using FileTime.ConsoleUI.App.Controls;
using FileTime.ConsoleUI.App.Styling; using FileTime.ConsoleUI.App.Styling;
using FileTime.Core.Enums; using FileTime.Core.Enums;
using FileTime.Core.Interactions;
using TerminalUI; using TerminalUI;
using TerminalUI.Color; using TerminalUI.Color;
using TerminalUI.Controls; using TerminalUI.Controls;
using TerminalUI.Extensions; using TerminalUI.Extensions;
using TerminalUI.Models; using TerminalUI.Models;
using TerminalUI.Traits;
using TerminalUI.ViewExtensions; using TerminalUI.ViewExtensions;
namespace FileTime.ConsoleUI.App; namespace FileTime.ConsoleUI.App;
@@ -23,6 +19,7 @@ public class MainWindow
private readonly ITheme _theme; private readonly ITheme _theme;
private readonly CommandPalette _commandPalette; private readonly CommandPalette _commandPalette;
private readonly Dialogs _dialogs; private readonly Dialogs _dialogs;
private readonly Timeline _timeline;
private readonly Lazy<IView> _root; private readonly Lazy<IView> _root;
@@ -31,13 +28,15 @@ public class MainWindow
IApplicationContext applicationContext, IApplicationContext applicationContext,
ITheme theme, ITheme theme,
CommandPalette commandPalette, CommandPalette commandPalette,
Dialogs dialogs) Dialogs dialogs,
Timeline timeline)
{ {
_rootViewModel = rootViewModel; _rootViewModel = rootViewModel;
_applicationContext = applicationContext; _applicationContext = applicationContext;
_theme = theme; _theme = theme;
_commandPalette = commandPalette; _commandPalette = commandPalette;
_dialogs = dialogs; _dialogs = dialogs;
_timeline = timeline;
_root = new Lazy<IView>(Initialize); _root = new Lazy<IView>(Initialize);
} }
@@ -67,7 +66,7 @@ public class MainWindow
private Grid<IRootViewModel> MainContent() => private Grid<IRootViewModel> MainContent() =>
new() new()
{ {
RowDefinitionsObject = "Auto * Auto Auto", RowDefinitionsObject = "Auto * Auto Auto Auto",
ChildInitializer = ChildInitializer =
{ {
new Grid<IRootViewModel> new Grid<IRootViewModel>
@@ -128,27 +127,19 @@ public class MainWindow
SelectedsItemsView().WithExtension(new GridPositionExtension(2, 0)), SelectedsItemsView().WithExtension(new GridPositionExtension(2, 0)),
} }
}, },
new Grid<IRootViewModel>
{
Extensions =
{
new GridPositionExtension(0, 2)
},
ChildInitializer =
{
PossibleCommands()
}
},
new ItemsControl<IRootViewModel, string> new ItemsControl<IRootViewModel, string>
{ {
MaxHeight = 5, MaxHeight = 5,
Extensions = Extensions =
{ {
new GridPositionExtension(0, 3) new GridPositionExtension(0, 2)
}, },
ItemTemplate = () => ItemTemplate = () =>
{ {
return new TextBlock<string>() return new TextBlock<string>
{
Foreground = _theme.WarningForegroundColor
}
.Setup(t => t.Bind( .Setup(t => t.Bind(
t, t,
dc => dc, dc => dc,
@@ -159,7 +150,19 @@ public class MainWindow
i, i,
root => root.AppState.PopupTexts, root => root.AppState.PopupTexts,
c => c.ItemsSource c => c.ItemsSource
)) )),
new Grid<IRootViewModel>
{
Extensions =
{
new GridPositionExtension(0, 3)
},
ChildInitializer =
{
PossibleCommands()
}
},
_timeline.View().WithExtension(new GridPositionExtension(0, 4)),
} }
}; };
@@ -341,7 +344,8 @@ public class MainWindow
textBlock.Bind( textBlock.Bind(
textBlock, textBlock,
dc => dc == null ? _theme.DefaultForegroundColor : ToForegroundColor(dc.ViewMode.Value, dc.BaseItem.Type), dc => dc == null ? _theme.DefaultForegroundColor : ToForegroundColor(dc.ViewMode.Value, dc.BaseItem.Type),
tb => tb.Foreground tb => tb.Foreground,
v => v
); );
textBlock.Bind( textBlock.Bind(
textBlock, textBlock,

View File

@@ -1,5 +1,6 @@
using FileTime.App.CommandPalette.ViewModels; using FileTime.App.CommandPalette.ViewModels;
using FileTime.App.Core.ViewModels; using FileTime.App.Core.ViewModels;
using FileTime.App.Core.ViewModels.Timeline;
using FileTime.ConsoleUI.App.Services; using FileTime.ConsoleUI.App.Services;
using FileTime.Core.Interactions; using FileTime.Core.Interactions;
@@ -13,18 +14,21 @@ public class RootViewModel : IRootViewModel
public IConsoleAppState AppState { get; } public IConsoleAppState AppState { get; }
public ICommandPaletteViewModel CommandPalette { get; } public ICommandPaletteViewModel CommandPalette { get; }
public IDialogService DialogService { get; } public IDialogService DialogService { get; }
public ITimelineViewModel TimelineViewModel { get; }
public event Action<IInputElement>? FocusReadInputElement; public event Action<IInputElement>? FocusReadInputElement;
public RootViewModel( public RootViewModel(
IConsoleAppState appState, IConsoleAppState appState,
IPossibleCommandsViewModel possibleCommands, IPossibleCommandsViewModel possibleCommands,
ICommandPaletteViewModel commandPalette, ICommandPaletteViewModel commandPalette,
IDialogService dialogService) IDialogService dialogService,
ITimelineViewModel timelineViewModel)
{ {
AppState = appState; AppState = appState;
PossibleCommands = possibleCommands; PossibleCommands = possibleCommands;
CommandPalette = commandPalette; CommandPalette = commandPalette;
DialogService = dialogService; DialogService = dialogService;
TimelineViewModel = timelineViewModel;
DialogService.ReadInput.PropertyChanged += (o, e) => DialogService.ReadInput.PropertyChanged += (o, e) =>
{ {

View File

@@ -37,6 +37,7 @@ public static class Startup
services.TryAddSingleton<MainWindow>(); services.TryAddSingleton<MainWindow>();
services.TryAddSingleton<CommandPalette>(); services.TryAddSingleton<CommandPalette>();
services.TryAddSingleton<Dialogs>(); services.TryAddSingleton<Dialogs>();
services.TryAddSingleton<Timeline>();
return services; return services;
} }
} }

View File

@@ -16,6 +16,8 @@ public record Theme(
IColor? MarkedSelectedItemBackgroundColor, IColor? MarkedSelectedItemBackgroundColor,
IColor? SelectedItemColor, IColor? SelectedItemColor,
IColor? SelectedTabBackgroundColor, IColor? SelectedTabBackgroundColor,
IColor? WarningForegroundColor,
IColor? ErrorForegroundColor,
ListViewItemTheme ListViewItemTheme, ListViewItemTheme ListViewItemTheme,
Type? ForegroundColors, Type? ForegroundColors,
Type? BackgroundColors) : ITheme, IColorSampleProvider; Type? BackgroundColors) : ITheme, IColorSampleProvider;
@@ -34,6 +36,8 @@ public static class DefaultThemes
MarkedSelectedItemBackgroundColor: Color256Colors.Foregrounds.Yellow, MarkedSelectedItemBackgroundColor: Color256Colors.Foregrounds.Yellow,
SelectedItemColor: Color256Colors.Foregrounds.Black, SelectedItemColor: Color256Colors.Foregrounds.Black,
SelectedTabBackgroundColor: Color256Colors.Backgrounds.Green, SelectedTabBackgroundColor: Color256Colors.Backgrounds.Green,
WarningForegroundColor: Color256Colors.Foregrounds.Yellow,
ErrorForegroundColor: Color256Colors.Foregrounds.Red,
ListViewItemTheme: new( ListViewItemTheme: new(
SelectedBackgroundColor: Color256Colors.Backgrounds.Gray, SelectedBackgroundColor: Color256Colors.Backgrounds.Gray,
SelectedForegroundColor: Color256Colors.Foregrounds.Black SelectedForegroundColor: Color256Colors.Foregrounds.Black
@@ -54,6 +58,8 @@ public static class DefaultThemes
MarkedSelectedItemBackgroundColor: ConsoleColors.Foregrounds.Yellow, MarkedSelectedItemBackgroundColor: ConsoleColors.Foregrounds.Yellow,
SelectedItemColor: ConsoleColors.Foregrounds.Black, SelectedItemColor: ConsoleColors.Foregrounds.Black,
SelectedTabBackgroundColor: ConsoleColors.Backgrounds.Green, SelectedTabBackgroundColor: ConsoleColors.Backgrounds.Green,
WarningForegroundColor: ConsoleColors.Foregrounds.Yellow,
ErrorForegroundColor: ConsoleColors.Foregrounds.Red,
ListViewItemTheme: new( ListViewItemTheme: new(
SelectedBackgroundColor: ConsoleColors.Backgrounds.Gray, SelectedBackgroundColor: ConsoleColors.Backgrounds.Gray,
SelectedForegroundColor: ConsoleColors.Foregrounds.Black SelectedForegroundColor: ConsoleColors.Foregrounds.Black

View File

@@ -13,7 +13,7 @@ public class CommandScheduler : ICommandScheduler
private readonly Subject<FullName> _containerToRefresh = new(); private readonly Subject<FullName> _containerToRefresh = new();
private readonly object _guard = new(); private readonly object _guard = new();
private bool _isRunningEnabled = true; private bool _isRunningEnabled = /*true*/ false;
private bool _resourceIsInUse; private bool _resourceIsInUse;
public IObservable<FullName> ContainerToRefresh { get; } public IObservable<FullName> ContainerToRefresh { get; }

View File

@@ -129,6 +129,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GeneralInputKey", "Library\
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalUI.DependencyInjection", "Library\TerminalUI.DependencyInjection\TerminalUI.DependencyInjection.csproj", "{E72F6430-0E6E-4818-BD5F-114893ACB18E}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalUI.DependencyInjection", "Library\TerminalUI.DependencyInjection\TerminalUI.DependencyInjection.csproj", "{E72F6430-0E6E-4818-BD5F-114893ACB18E}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalUI.Tests", "Library\TerminalUI.Tests\TerminalUI.Tests.csproj", "{30B6E288-F314-494B-8550-1329BFF664D2}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{E72F6430-0E6E-4818-BD5F-114893ACB18E}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -411,6 +417,7 @@ Global
{6C3C3151-9341-4792-9B0B-A11C0658524E} = {07CA18AA-B85D-4DEE-BB86-F569F6029853} {6C3C3151-9341-4792-9B0B-A11C0658524E} = {07CA18AA-B85D-4DEE-BB86-F569F6029853}
{91AE5B64-042B-4660-A8E8-D247E6E14A1E} = {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} {E72F6430-0E6E-4818-BD5F-114893ACB18E} = {07CA18AA-B85D-4DEE-BB86-F569F6029853}
{30B6E288-F314-494B-8550-1329BFF664D2} = {07CA18AA-B85D-4DEE-BB86-F569F6029853}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF} SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF}

View File

@@ -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<TestViewModel>
{
DataContext = testViewModel
};
var txtb1 = grid.CreateChild<TextBlock<TestViewModel>>();
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<TestViewModel>
{
DataContext = testViewModel
};
var txtb1 = grid.CreateChild<TextBlock<TestViewModel>>();
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<TestViewModel>
{
DataContext = testViewModel
};
var txtb1 = grid.CreateChild<TextBlock<TestViewModel>>();
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<TestViewModel>
{
DataContext = testViewModel
};
var txtb1 = grid.CreateChild<TextBlock<TestViewModel>>();
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<TestViewModel>();
var txtb1 = grid.CreateChild<TextBlock<TestViewModel>>();
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<TestViewModel>();
var txtb1 = grid.CreateChild<TextBlock<TestViewModel>>();
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<TestViewModel>
{
DataContext = testViewModel
};
var txtb1 = grid.CreateChild<TextBlock<TestViewModel>>();
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<TestViewModel>
{
DataContext = testViewModel
};
var txtb1 = grid.CreateChild<TextBlock<TestViewModel>>();
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<TestViewModel>
{
DataContext = testViewModel
};
var txtb1 = grid.CreateChild<TextBlock<TestViewModel>>();
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<TestViewModel>
{
DataContext = testViewModel
};
var txtb1 = grid.CreateChild<TextBlock<TestViewModel>>();
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<TestViewModel>
{
DataContext = testViewModel
};
var txtb1 = grid.CreateChild<TextBlock<TestViewModel>>();
txtb1.Bind(
txtb1,
vm => vm.Items.Count,
t => t.Text,
v => v.ToString());
testViewModel.Items = new List<TestNestedCollectionItem>
{
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);
}
}

View File

@@ -0,0 +1 @@
global using Xunit;

View File

@@ -0,0 +1,10 @@
namespace TerminalUI.Tests.Models;
public class TestCollectionItem
{
public List<TestItem>? Items1 { get; set; } = new()
{
new TestItem() {Name = "Name1"},
new TestItem() {Name = "Name2"},
};
}

View File

@@ -0,0 +1,6 @@
namespace TerminalUI.Tests.Models;
public class TestItem
{
public string? Name { get; set; }
}

View File

@@ -0,0 +1,22 @@
namespace TerminalUI.Tests.Models;
public class TestNestedCollectionItem
{
public List<TestNestedCollectionItem> Items { get; set; } = new();
public List<TestCollectionItem> 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;
}
}

View File

@@ -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<TestNestedCollectionItem> _items;
[Notify] private string _text;
public TestViewModel()
{
_text = "Initial text";
_items = new List<TestNestedCollectionItem>
{
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));
}

View File

@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="PropertyChanged.SourceGenerator" Version="1.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.2.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\TerminalUI\TerminalUI.csproj" />
</ItemGroup>
</Project>

View File

@@ -2,11 +2,12 @@
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
using TerminalUI.Controls; using TerminalUI.Controls;
using TerminalUI.ExpressionTrackers;
using TerminalUI.Traits; using TerminalUI.Traits;
namespace TerminalUI; namespace TerminalUI;
public sealed class Binding<TDataContext, TExpressionResult, TResult> : PropertyTrackerBase<TDataContext, TExpressionResult> public sealed class Binding<TDataContext, TExpressionResult, TResult> : PropertyTrackerBase<TDataContext, TExpressionResult>, IDisposable
{ {
private readonly Func<TDataContext, TExpressionResult> _dataContextMapper; private readonly Func<TDataContext, TExpressionResult> _dataContextMapper;
private IView<TDataContext> _dataSourceView; private IView<TDataContext> _dataSourceView;
@@ -15,6 +16,7 @@ public sealed class Binding<TDataContext, TExpressionResult, TResult> : Property
private readonly Func<TExpressionResult, TResult> _converter; private readonly Func<TExpressionResult, TResult> _converter;
private readonly TResult? _fallbackValue; private readonly TResult? _fallbackValue;
private IDisposableCollection? _propertySourceDisposableCollection; private IDisposableCollection? _propertySourceDisposableCollection;
private readonly string _parameterName;
public Binding( public Binding(
IView<TDataContext> dataSourceView, IView<TDataContext> dataSourceView,
@@ -23,7 +25,7 @@ public sealed class Binding<TDataContext, TExpressionResult, TResult> : Property
PropertyInfo targetProperty, PropertyInfo targetProperty,
Func<TExpressionResult, TResult> converter, Func<TExpressionResult, TResult> converter,
TResult? fallbackValue = default TResult? fallbackValue = default
) : base(() => dataSourceView.DataContext, dataSourceExpression) ) : base(dataSourceExpression)
{ {
ArgumentNullException.ThrowIfNull(dataSourceView); ArgumentNullException.ThrowIfNull(dataSourceView);
ArgumentNullException.ThrowIfNull(dataSourceExpression); ArgumentNullException.ThrowIfNull(dataSourceExpression);
@@ -37,14 +39,14 @@ public sealed class Binding<TDataContext, TExpressionResult, TResult> : Property
_converter = converter; _converter = converter;
_fallbackValue = fallbackValue; _fallbackValue = fallbackValue;
UpdateTrackers(); _parameterName = dataSourceExpression.Parameters[0].Name!;
Parameters.SetValue(_parameterName, dataSourceView.DataContext);
dataSourceView.PropertyChanged += View_PropertyChanged; dataSourceView.PropertyChanged += View_PropertyChanged;
UpdateTargetProperty(); Update(true);
AddToSourceDisposables(propertySource); AddToSourceDisposables(propertySource);
dataSourceView.AddDisposable(this);
} }
private void AddToSourceDisposables(object? propertySource) private void AddToSourceDisposables(object? propertySource)
@@ -60,19 +62,24 @@ public sealed class Binding<TDataContext, TExpressionResult, TResult> : Property
{ {
if (e.PropertyName != nameof(IView<TDataContext>.DataContext)) return; if (e.PropertyName != nameof(IView<TDataContext>.DataContext)) return;
UpdateTrackers(); Parameters.SetValue(_parameterName, _dataSourceView.DataContext);
UpdateTargetProperty(); Update(true);
} }
protected override void Update(string propertyPath) => UpdateTargetProperty(); protected override void Update(bool couldCompute)
private void UpdateTargetProperty()
{ {
TResult value; TResult? value;
try try
{
if (couldCompute)
{ {
value = _converter(_dataContextMapper(_dataSourceView.DataContext)); value = _converter(_dataContextMapper(_dataSourceView.DataContext));
} }
else
{
value = _fallbackValue;
}
}
catch catch
{ {
value = _fallbackValue; value = _fallbackValue;
@@ -87,9 +94,9 @@ public sealed class Binding<TDataContext, TExpressionResult, TResult> : Property
} }
} }
public override void Dispose() public void Dispose()
{ {
base.Dispose(); //base.Dispose();
_propertySourceDisposableCollection?.RemoveDisposable(this); _propertySourceDisposableCollection?.RemoveDisposable(this);
_dataSourceView.RemoveDisposable(this); _dataSourceView.RemoveDisposable(this);
_dataSourceView.PropertyChanged -= View_PropertyChanged; _dataSourceView.PropertyChanged -= View_PropertyChanged;

View File

@@ -27,11 +27,11 @@ public sealed partial class TextBlock<T> : View<TextBlock<T>, T>, IDisplayView
public TextBlock() public TextBlock()
{ {
this.Bind( /*this.Bind(
this, this,
dc => dc == null ? string.Empty : dc.ToString(), dc => dc == null ? string.Empty : dc.ToString(),
tb => tb.Text tb => tb.Text
); );*/
RerenderProperties.Add(nameof(Text)); RerenderProperties.Add(nameof(Text));
RerenderProperties.Add(nameof(TextAlignment)); RerenderProperties.Add(nameof(TextAlignment));

View File

@@ -1,4 +1,5 @@
using Microsoft.Extensions.Logging; using System.Diagnostics;
using Microsoft.Extensions.Logging;
namespace TerminalUI; namespace TerminalUI;
@@ -32,14 +33,15 @@ public class EventLoop : IEventLoop
{ {
foreach (var action in _permanentQueue) foreach (var action in _permanentQueue)
{ {
try /*try
{ {*/
action(); action();
} /*}
catch (Exception e) catch (Exception e)
{ {
Debug.Fail(e.Message);
_logger.LogError(e, "Error while processing action in permanent queue"); _logger.LogError(e, "Error while processing action in permanent queue");
} }*/
} }
} }
} }

View File

@@ -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<object?, object?, object?> _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());
}

View File

@@ -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.")
};
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,19 @@
using System.Collections.ObjectModel;
namespace TerminalUI.ExpressionTrackers;
public class ExpressionParameterTrackerCollection
{
private readonly Dictionary<string, object?> _values = new();
public ReadOnlyDictionary<string, object?> Values => new(_values);
public event Action<string, object?>? ValueChanged;
public void SetValue(string name, object? value)
{
ArgumentException.ThrowIfNullOrEmpty(name);
_values[name] = value;
ValueChanged?.Invoke(name, value);
}
}

View File

@@ -0,0 +1,82 @@
using System.Collections.Specialized;
using System.ComponentModel;
namespace TerminalUI.ExpressionTrackers;
public abstract class ExpressionTrackerBase : IExpressionTracker
{
private object? _currentValue;
public List<string> TrackedPropertyNames { get; } = new();
protected bool SubscribeToValueChanges { get; set; } = true;
public event Action<string>? PropertyChanged;
public event Action<bool>? 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);
}
}
}

View File

@@ -0,0 +1,9 @@
namespace TerminalUI.ExpressionTrackers;
public interface IExpressionTracker
{
List<string> TrackedPropertyNames { get; }
event Action<string>? PropertyChanged;
event Action<bool>? Update;
object? GetValue();
}

View File

@@ -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<object?> _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 ?? "<null>"}."
);
}
}
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;
}
}

View File

@@ -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<IExpressionTracker> _argumentTrackers;
public MethodCallTracker(
MethodCallExpression methodCallExpression,
IExpressionTracker? objectTracker,
IEnumerable<IExpressionTracker> 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());
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,46 @@
using System.Linq.Expressions;
namespace TerminalUI.ExpressionTrackers;
public class UnaryTracker : ExpressionTrackerBase
{
private readonly IExpressionTracker _operandTracker;
private readonly Func<object?, object?> _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());
}

View File

@@ -4,7 +4,7 @@ using TerminalUI.Controls;
namespace TerminalUI.Extensions; namespace TerminalUI.Extensions;
public static class Binding public static class BindingExtensions
{ {
public static Binding<TDataContext, TResult, TResult> Bind<TView, TDataContext, TResult>( public static Binding<TDataContext, TResult, TResult> Bind<TView, TDataContext, TResult>(
this TView targetView, this TView targetView,

View File

@@ -28,7 +28,7 @@ public static class ViewExtensions
public static TItem WithPropertyChangedHandler<TItem, TExpressionResult>( public static TItem WithPropertyChangedHandler<TItem, TExpressionResult>(
this TItem dataSource, this TItem dataSource,
Expression<Func<TItem, TExpressionResult>> dataSourceExpression, Expression<Func<TItem, TExpressionResult>> dataSourceExpression,
Action<string, bool, TExpressionResult> handler) Action<bool, TExpressionResult> handler)
{ {
new PropertyChangedHandler<TItem, TExpressionResult> new PropertyChangedHandler<TItem, TExpressionResult>
( (

View File

@@ -1,149 +0,0 @@
using System.ComponentModel;
namespace TerminalUI;
public interface IPropertyChangeTracker : IDisposable
{
string Name { get; }
string Path { get; }
Dictionary<string, IPropertyChangeTracker> Children { get; }
}
public abstract class PropertyChangeTrackerBase : IPropertyChangeTracker
{
public string Name { get; }
public string Path { get; }
public Dictionary<string, IPropertyChangeTracker> 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<string> _propertiesToListen;
private readonly Action<string> _updateBinding;
public PropertyChangeTracker(
string name,
string path,
PropertyTrackTreeItem propertyTrackTreeItem,
INotifyPropertyChanged target,
IEnumerable<string> propertiesToListen,
Action<string> 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<string, PropertyTrackTreeItem> Children { get; } = new();
public PropertyTrackTreeItem(string name)
{
Name = name;
}
}
public static class PropertyChangeHelper
{
internal static IPropertyChangeTracker? CreatePropertyTracker(
string? path,
PropertyTrackTreeItem propertyTrackTreeItem,
object? obj,
Action<string> 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;
}
}

View File

@@ -2,44 +2,48 @@
namespace TerminalUI; namespace TerminalUI;
public sealed class PropertyChangedHandler<TItem, TExpressionResult> : PropertyTrackerBase<TItem, TExpressionResult>, IDisposable public sealed class PropertyChangedHandler<TItem, TExpressionResult> : PropertyTrackerBase<TItem, TExpressionResult>
{ {
private readonly TItem _dataSource; private readonly TItem _dataSource;
private readonly Action<string, bool, TExpressionResult?> _handler; private readonly Action<bool, TExpressionResult?> _handler;
private readonly PropertyTrackTreeItem? _propertyTrackTreeItem;
private readonly Func<TItem, TExpressionResult> _propertyValueGenerator; private readonly Func<TItem, TExpressionResult> _propertyValueGenerator;
public PropertyChangedHandler( public PropertyChangedHandler(
TItem dataSource, TItem dataSource,
Expression<Func<TItem, TExpressionResult>> dataSourceExpression, Expression<Func<TItem, TExpressionResult>> dataSourceExpression,
Action<string, bool, TExpressionResult?> handler Action<bool, TExpressionResult?> handler
) : base(() => dataSource, dataSourceExpression) ) : base(dataSourceExpression!)
{ {
_dataSource = dataSource;
_handler = handler;
ArgumentNullException.ThrowIfNull(dataSource); ArgumentNullException.ThrowIfNull(dataSource);
ArgumentNullException.ThrowIfNull(dataSourceExpression); ArgumentNullException.ThrowIfNull(dataSourceExpression);
ArgumentNullException.ThrowIfNull(handler); ArgumentNullException.ThrowIfNull(handler);
_propertyTrackTreeItem = CreateTrackingTree(dataSourceExpression); _dataSource = dataSource;
_handler = handler;
Parameters.SetValue(dataSourceExpression.Parameters[0].Name!, dataSource);
_propertyValueGenerator = dataSourceExpression.Compile(); _propertyValueGenerator = dataSourceExpression.Compile();
UpdateTrackers(); Update(true);
} }
protected override void Update(string propertyPath) protected override void Update(bool couldCompute)
{ {
TExpressionResult? value = default; TExpressionResult? value = default;
var parsed = false; var parsed = false;
try try
{
if (couldCompute)
{ {
value = _propertyValueGenerator(_dataSource); value = _propertyValueGenerator(_dataSource);
parsed = true; parsed = true;
} }
}
catch catch
{ {
} }
_handler(propertyPath, parsed, value); _handler(parsed, value);
} }
} }

View File

@@ -1,145 +1,92 @@
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection; using TerminalUI.ExpressionTrackers;
namespace TerminalUI; namespace TerminalUI;
public abstract class PropertyTrackerBase<TSource, TExpressionResult> : IDisposable public abstract class PropertyTrackerBase<TSource, TExpressionResult>
{ {
private readonly Func<TSource?> _source; private readonly IExpressionTracker _tracker;
protected PropertyTrackTreeItem? PropertyTrackTreeItem { get; } protected ExpressionParameterTrackerCollection Parameters { get; } = new();
protected IPropertyChangeTracker? PropertyChangeTracker { get; private set; }
protected PropertyTrackerBase( protected PropertyTrackerBase(Expression<Func<TSource?, TExpressionResult>> dataSourceExpression)
Func<TSource?> source,
Expression<Func<TSource?, TExpressionResult>> dataSourceExpression)
{ {
ArgumentNullException.ThrowIfNull(dataSourceExpression); ArgumentNullException.ThrowIfNull(dataSourceExpression);
_source = source; _tracker = FindReactiveProperties(dataSourceExpression.Body, Parameters);
PropertyTrackTreeItem = CreateTrackingTree(dataSourceExpression); _tracker.Update += Update;
} }
protected PropertyTrackTreeItem? CreateTrackingTree(Expression<Func<TSource?, TExpressionResult>> dataContextExpression) private IExpressionTracker FindReactiveProperties(Expression? expression, ExpressionParameterTrackerCollection parameters)
{ {
var properties = new List<string>(); if (expression is ConditionalExpression conditionalExpression)
FindReactiveProperties(dataContextExpression, properties); {
var testTracker = FindReactiveProperties(conditionalExpression.Test, parameters);
var trueTracker = FindReactiveProperties(conditionalExpression.IfTrue, parameters);
var falseTracker = FindReactiveProperties(conditionalExpression.IfFalse, parameters);
if (properties.Count > 0) return new ConditionalTracker(
{ conditionalExpression,
var rootItem = new PropertyTrackTreeItem(null!); testTracker,
foreach (var property in properties) trueTracker,
{ falseTracker);
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);
}
currentItem = child;
}
}
return rootItem;
}
return null;
}
private string? FindReactiveProperties(Expression? expression, List<string> 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));
} }
else if (expression is MemberExpression memberExpression) else if (expression is MemberExpression memberExpression)
{ {
IExpressionTracker? parentExpressionTracker = null;
if (memberExpression.Expression is not null) if (memberExpression.Expression is not null)
{ {
FindReactiveProperties(memberExpression.Expression, properties); parentExpressionTracker = FindReactiveProperties(memberExpression.Expression, parameters);
}
if (FindReactiveProperties(memberExpression.Expression, properties) is { } path return new MemberTracker(memberExpression, parentExpressionTracker);
&& memberExpression.Member is PropertyInfo dataContextPropertyInfo)
{
path += "." + memberExpression.Member.Name;
return path;
}
}
} }
else if (expression is MethodCallExpression methodCallExpression) else if (expression is MethodCallExpression methodCallExpression)
{ {
if (methodCallExpression.Object is IExpressionTracker? objectTracker = null;
if (methodCallExpression.Object is { } methodObject)
{ {
NodeType: objectTracker = FindReactiveProperties(methodObject, parameters);
not ExpressionType.Parameter
and not ExpressionType.Constant
} methodObject)
{
SavePropertyPath(FindReactiveProperties(methodObject, properties));
} }
var argumentTrackers = new List<IExpressionTracker>(methodCallExpression.Arguments.Count);
foreach (var argument in methodCallExpression.Arguments) 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) else if (expression is BinaryExpression binaryExpression)
{ {
SavePropertyPath(FindReactiveProperties(binaryExpression.Left, properties)); var leftTracker = FindReactiveProperties(binaryExpression.Left, parameters);
SavePropertyPath(FindReactiveProperties(binaryExpression.Right, properties)); var rightTracker = FindReactiveProperties(binaryExpression.Right, parameters);
return new BinaryTracker(binaryExpression, leftTracker, rightTracker);
} }
else if (expression is UnaryExpression unaryExpression) 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) else if (expression is ParameterExpression parameterExpression)
{ {
if (parameterExpression.Type == typeof(TSource)) if (parameterExpression.Name is { } name)
{ {
return ""; return new ParameterTracker(parameterExpression, parameters, name);
} }
} }
else if (expression is ConstantExpression constantExpression)
return null;
void SavePropertyPath(string? path)
{ {
if (path is null) return; return new ConstantTracker(constantExpression.Value);
path = path.TrimStart('.');
properties.Add(path);
} }
} /*else if (expression is not ConstantExpression)
protected void UpdateTrackers()
{ {
if (PropertyChangeTracker is not null) Debug.Assert(false, "Unknown expression type " + expression.GetType());
{ }*/
PropertyChangeTracker.Dispose();
throw new NotSupportedException();
} }
if (PropertyTrackTreeItem is not null) protected abstract void Update(bool couldCompute);
{
PropertyChangeTracker = PropertyChangeHelper.CreatePropertyTracker(
null,
PropertyTrackTreeItem,
_source(),
Update
);
}
}
protected abstract void Update(string propertyPath);
public virtual void Dispose() => PropertyChangeTracker?.Dispose();
} }