New binding mechanism: Expression tracking
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ public class CommandPalette
|
|||||||
};
|
};
|
||||||
|
|
||||||
root.WithPropertyChangedHandler(r => r.IsVisible,
|
root.WithPropertyChangedHandler(r => r.IsVisible,
|
||||||
(_, _, isVisible) =>
|
(_, isVisible) =>
|
||||||
{
|
{
|
||||||
if (isVisible)
|
if (isVisible)
|
||||||
{
|
{
|
||||||
|
|||||||
46
src/ConsoleApp/FileTime.ConsoleUI.App/Controls/Timeline.cs
Normal file
46
src/ConsoleApp/FileTime.ConsoleUI.App/Controls/Timeline.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
259
src/Library/TerminalUI.Tests/BindingTests.cs
Normal file
259
src/Library/TerminalUI.Tests/BindingTests.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/Library/TerminalUI.Tests/GlobalUsings.cs
Normal file
1
src/Library/TerminalUI.Tests/GlobalUsings.cs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
global using Xunit;
|
||||||
10
src/Library/TerminalUI.Tests/Models/TestCollectionItem.cs
Normal file
10
src/Library/TerminalUI.Tests/Models/TestCollectionItem.cs
Normal 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"},
|
||||||
|
};
|
||||||
|
}
|
||||||
6
src/Library/TerminalUI.Tests/Models/TestItem.cs
Normal file
6
src/Library/TerminalUI.Tests/Models/TestItem.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace TerminalUI.Tests.Models;
|
||||||
|
|
||||||
|
public class TestItem
|
||||||
|
{
|
||||||
|
public string? Name { get; set; }
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/Library/TerminalUI.Tests/Models/TestViewModel.cs
Normal file
32
src/Library/TerminalUI.Tests/Models/TestViewModel.cs
Normal 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));
|
||||||
|
}
|
||||||
33
src/Library/TerminalUI.Tests/TerminalUI.Tests.csproj
Normal file
33
src/Library/TerminalUI.Tests/TerminalUI.Tests.csproj
Normal 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>
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
37
src/Library/TerminalUI/ExpressionTrackers/BinaryTracker.cs
Normal file
37
src/Library/TerminalUI/ExpressionTrackers/BinaryTracker.cs
Normal 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());
|
||||||
|
}
|
||||||
@@ -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.")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/Library/TerminalUI/ExpressionTrackers/ConstantTracker.cs
Normal file
13
src/Library/TerminalUI/ExpressionTrackers/ConstantTracker.cs
Normal 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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace TerminalUI.ExpressionTrackers;
|
||||||
|
|
||||||
|
public interface IExpressionTracker
|
||||||
|
{
|
||||||
|
List<string> TrackedPropertyNames { get; }
|
||||||
|
event Action<string>? PropertyChanged;
|
||||||
|
event Action<bool>? Update;
|
||||||
|
object? GetValue();
|
||||||
|
}
|
||||||
82
src/Library/TerminalUI/ExpressionTrackers/MemberTracker.cs
Normal file
82
src/Library/TerminalUI/ExpressionTrackers/MemberTracker.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/Library/TerminalUI/ExpressionTrackers/UnaryTracker.cs
Normal file
46
src/Library/TerminalUI/ExpressionTrackers/UnaryTracker.cs
Normal 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());
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
@@ -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>
|
||||||
(
|
(
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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();
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user