Skip to content

Commit

Permalink
refactor: Inversion of control for all dependencies between classes i…
Browse files Browse the repository at this point in the history
…n the UI (#770)

* refactor: message-based communication in ViewModels

Signed-off-by: Maximilien Noal <noal.maximilien@gmail.com>

* refactor: DebugViewModel is an injected service now

Signed-off-by: Maximilien Noal <noal.maximilien@gmail.com>

---------

Signed-off-by: Maximilien Noal <noal.maximilien@gmail.com>
  • Loading branch information
maximilien-noal authored Jul 12, 2024
1 parent 9402c48 commit 1b5cb66
Show file tree
Hide file tree
Showing 14 changed files with 166 additions and 123 deletions.
18 changes: 18 additions & 0 deletions src/Spice86/DependencyInjection/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ namespace Spice86.DependencyInjection;
using Avalonia.Platform.Storage;
using Avalonia.Threading;

using CommunityToolkit.Mvvm.Messaging;

using Microsoft.Extensions.DependencyInjection;

using Spice86.Core.CLI;
using Spice86.Core.Emulator;
using Spice86.Infrastructure;
using Spice86.Interfaces;
using Spice86.Logging;
using Spice86.Shared.Interfaces;
using Spice86.ViewModels;

public static class ServiceCollectionExtensions {
public static void AddConfiguration(this IServiceCollection serviceCollection, string[] args) {
Expand Down Expand Up @@ -42,5 +46,19 @@ public static void AddGuiInfrastructure(this IServiceCollection serviceCollectio
serviceCollection.AddSingleton<IHostStorageProvider, HostStorageProvider>();
serviceCollection.AddSingleton<ITextClipboard>(_ => new TextClipboard(mainWindow.Clipboard));
serviceCollection.AddSingleton<IStructureViewModelFactory, StructureViewModelFactory>();
serviceCollection.AddSingleton<IMessenger>(_ => WeakReferenceMessenger.Default);
}

public static void AddEmulatorServices(this IServiceCollection serviceCollection) {
serviceCollection.AddScoped<IProgramExecutor, ProgramExecutor>((serviceProvider) => {
Configuration configuration = serviceProvider.GetRequiredService<Configuration>();
ILoggerService loggerService = serviceProvider.GetRequiredService<ILoggerService>();
return new ProgramExecutor(configuration, loggerService, serviceProvider.GetService<IGui>());
});
}

public static void AddViewModels(this IServiceCollection serviceCollection) {
serviceCollection.AddScoped<DebugWindowViewModel>();
serviceCollection.AddScoped<MainWindowViewModel>();
}
}
7 changes: 0 additions & 7 deletions src/Spice86/Interfaces/IPauseStatus.cs

This file was deleted.

6 changes: 4 additions & 2 deletions src/Spice86/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,11 @@ public static void Main(string[] args) {
ClassicDesktopStyleApplicationLifetime desktop = CreateDesktopApp();
MainWindow mainWindow = new();
serviceCollection.AddGuiInfrastructure(mainWindow);
serviceCollection.AddEmulatorServices();
serviceCollection.AddViewModels();
//We need to rebuild the service provider after adding new services to the collection
using MainWindowViewModel mainWindowViewModel = serviceCollection.BuildServiceProvider().GetRequiredService<MainWindowViewModel>();
serviceProvider = serviceCollection.BuildServiceProvider();
using MainWindowViewModel mainWindowViewModel = serviceProvider.GetRequiredService<MainWindowViewModel>();
StartGraphicalUserInterface(desktop, mainWindowViewModel, mainWindow, args);
}
}
Expand All @@ -78,7 +81,6 @@ private static ServiceCollection InjectCommonServices(string[] args) {
serviceCollection.AddLogging();

serviceCollection.AddScoped<IProgramExecutorFactory, ProgramExecutorFactory>();
serviceCollection.AddScoped<MainWindowViewModel>();
return serviceCollection;
}

Expand Down
24 changes: 14 additions & 10 deletions src/Spice86/ViewModels/CfgCpuViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@
using AvaloniaGraphControl;

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;

using Spice86.Core.Emulator.CPU.CfgCpu.ControlFlowGraph;
using Spice86.Core.Emulator.CPU.CfgCpu.Linker;
using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction;
using Spice86.Core.Emulator.CPU.CfgCpu.ParsedInstruction.SelfModifying;
using Spice86.Core.Emulator.InternalDebugger;
using Spice86.Infrastructure;
using Spice86.Interfaces;
using Spice86.Shared.Diagnostics;
using Spice86.Shared.Emulator.Memory;
using Spice86.Shared.Interfaces;
using Spice86.ViewModels.Messages;

using System.Diagnostics;

Expand All @@ -35,15 +36,18 @@ public partial class CfgCpuViewModel : ViewModelBase, IInternalDebugger {
[ObservableProperty]
private long _averageNodeTime = 0;

private readonly IPauseStatus? _pauseStatus;
private readonly IMessenger _messenger;

public CfgCpuViewModel(IUIDispatcherTimerFactory dispatcherTimerFactory, IPerformanceMeasurer performanceMeasurer, IPauseStatus pauseStatus) {
_pauseStatus = pauseStatus;
_pauseStatus.PropertyChanged += (sender, args) => {
if (args.PropertyName == nameof(IPauseStatus.IsPaused) && !_pauseStatus.IsPaused) {
private bool _isPaused;

public CfgCpuViewModel(IMessenger messenger, IUIDispatcherTimerFactory dispatcherTimerFactory, IPerformanceMeasurer performanceMeasurer) {
_messenger = messenger;
_messenger.Register<PauseStatusChangedMessage>(this, (_, message) => {
_isPaused = message.IsPaused;
if(!message.IsPaused) {
Graph = null;
}
};
});
_performanceMeasurer = performanceMeasurer;
dispatcherTimerFactory.StartNew(TimeSpan.FromMilliseconds(400), DispatcherPriority.Normal, UpdateCurrentGraph);
}
Expand All @@ -53,7 +57,7 @@ partial void OnMaxNodesToDisplayChanging(int value) {
}

private async void UpdateCurrentGraph(object? sender, EventArgs e) {
if (_pauseStatus?.IsPaused is false or null) {
if (!_isPaused) {
return;
}
if (Graph is not null) {
Expand Down Expand Up @@ -81,7 +85,7 @@ await Task.Run(async () => {
visitedNodes.Add(node);
stopwatch.Restart();
foreach (ICfgNode successor in node.Successors) {
var edgeKey = GenerateEdgeKey(node, successor);
(int, int) edgeKey = GenerateEdgeKey(node, successor);
if (!existingEdges.Contains(edgeKey)) {
currentGraph.Edges.Add(CreateEdge(node, successor));
existingEdges.Add(edgeKey);
Expand All @@ -91,7 +95,7 @@ await Task.Run(async () => {
}
}
foreach (ICfgNode predecessor in node.Predecessors) {
var edgeKey = GenerateEdgeKey(predecessor, node);
(int, int) edgeKey = GenerateEdgeKey(predecessor, node);
if (!existingEdges.Contains(edgeKey)) {
currentGraph.Edges.Add(CreateEdge(predecessor, node));
existingEdges.Add(edgeKey);
Expand Down
19 changes: 10 additions & 9 deletions src/Spice86/ViewModels/CpuViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,31 @@ namespace Spice86.ViewModels;
using Avalonia.Threading;

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;

using Spice86.Core.Emulator.CPU;
using Spice86.Core.Emulator.InternalDebugger;
using Spice86.Infrastructure;
using Spice86.Interfaces;
using Spice86.Models.Debugging;
using Spice86.ViewModels.Messages;

using System.ComponentModel;
using System.Reflection;

public partial class CpuViewModel : ViewModelBase, IInternalDebugger {
private readonly IPauseStatus _pauseStatus;
private readonly IMessenger _messenger;
private State? _cpuState;
private bool _isPaused;

[ObservableProperty]
private StateInfo _state = new();

[ObservableProperty]
private CpuFlagsInfo _flags = new();

public CpuViewModel(IUIDispatcherTimerFactory dispatcherTimerFactory, IPauseStatus pauseStatus) {
_pauseStatus = pauseStatus;
public CpuViewModel(IMessenger messenger, IUIDispatcherTimerFactory dispatcherTimerFactory) {
_messenger = messenger;
_messenger.Register<PauseStatusChangedMessage>(this, (_, message) => _isPaused = message.IsPaused);
dispatcherTimerFactory.StartNew(TimeSpan.FromMilliseconds(400), DispatcherPriority.Normal, UpdateValues);
}

Expand All @@ -40,14 +43,12 @@ public void Visit<T>(T component) where T : IDebuggableComponent {
_cpuState ??= component as State;
}

private bool IsPaused => _pauseStatus.IsPaused;

private void VisitCpuState(State state) {
if (!IsPaused) {
if (!_isPaused) {
UpdateCpuState(state);
}

if (IsPaused) {
if (_isPaused) {
State.PropertyChanged -= OnStatePropertyChanged;
State.PropertyChanged += OnStatePropertyChanged;
Flags.PropertyChanged -= OnStatePropertyChanged;
Expand All @@ -60,7 +61,7 @@ private void VisitCpuState(State state) {
return;

void OnStatePropertyChanged(object? sender, PropertyChangedEventArgs e) {
if (sender is null || e.PropertyName == null || !IsPaused) {
if (sender is null || e.PropertyName == null || !_isPaused) {
return;
}
PropertyInfo? originalPropertyInfo = state.GetType().GetProperty(e.PropertyName);
Expand Down
53 changes: 34 additions & 19 deletions src/Spice86/ViewModels/DebugWindowViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,25 @@ namespace Spice86.ViewModels;

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;

using Spice86.Core.Emulator;
using Spice86.Core.Emulator.InternalDebugger;
using Spice86.Core.Emulator.Memory;
using Spice86.Infrastructure;
using Spice86.Interfaces;
using Spice86.Shared.Diagnostics;
using Spice86.ViewModels.Messages;

using System.ComponentModel;

public partial class DebugWindowViewModel : ViewModelBase, IInternalDebugger {
private readonly IPauseStatus _pauseStatus;
private readonly IProgramExecutor _programExecutor;
private readonly IDebuggableComponent _programExecutor;
private readonly IHostStorageProvider _storageProvider;
private readonly IUIDispatcherTimerFactory _uiDispatcherTimerFactory;
private readonly ITextClipboard _textClipboard;
private readonly IStructureViewModelFactory _structureViewModelFactory;
private readonly IMessenger _messenger;

[ObservableProperty]
private DateTime? _lastUpdate;
Expand Down Expand Up @@ -55,28 +58,36 @@ public partial class DebugWindowViewModel : ViewModelBase, IInternalDebugger {
[ObservableProperty]
private CfgCpuViewModel _cfgCpuViewModel;

public DebugWindowViewModel(ITextClipboard textClipboard, IHostStorageProvider storageProvider, IUIDispatcherTimerFactory uiDispatcherTimerFactory, IPauseStatus pauseStatus, IProgramExecutor programExecutor, IStructureViewModelFactory structureViewModelFactory) {
public DebugWindowViewModel(IMessenger messenger, ITextClipboard textClipboard, IHostStorageProvider storageProvider, IUIDispatcherTimerFactory uiDispatcherTimerFactory, IProgramExecutor programExecutor, IStructureViewModelFactory structureViewModelFactory) {
_messenger = messenger;
_messenger.Register<PauseStatusChangedMessage>(this, (_, message) => HandlePauseStatusChanged(message.IsPaused));
_programExecutor = programExecutor;
_structureViewModelFactory = structureViewModelFactory;
_storageProvider = storageProvider;
_textClipboard = textClipboard;
_uiDispatcherTimerFactory = uiDispatcherTimerFactory;
_pauseStatus = pauseStatus;
IsPaused = _programExecutor.IsPaused;
_pauseStatus.PropertyChanged += OnPauseStatusChanged;
IsPaused = programExecutor.IsPaused;
uiDispatcherTimerFactory.StartNew(TimeSpan.FromSeconds(1.0 / 30.0), DispatcherPriority.Normal, UpdateValues);
var disassemblyVm = new DisassemblyViewModel(this, uiDispatcherTimerFactory, pauseStatus);
var disassemblyVm = new DisassemblyViewModel(_messenger, programExecutor.IsPaused, false, uiDispatcherTimerFactory);
DisassemblyViewModels.Add(disassemblyVm);
PaletteViewModel = new(uiDispatcherTimerFactory);
SoftwareMixerViewModel = new(uiDispatcherTimerFactory);
VideoCardViewModel = new(uiDispatcherTimerFactory);
CpuViewModel = new(uiDispatcherTimerFactory, pauseStatus);
CpuViewModel = new(_messenger, uiDispatcherTimerFactory);
MidiViewModel = new(uiDispatcherTimerFactory);
MemoryViewModels.Add(new(this, textClipboard, uiDispatcherTimerFactory, storageProvider, pauseStatus, 0, _structureViewModelFactory));
CfgCpuViewModel = new(uiDispatcherTimerFactory, new PerformanceMeasurer(), pauseStatus);
MemoryViewModels.Add(new(this, _messenger, textClipboard, uiDispatcherTimerFactory, storageProvider, programExecutor.IsPaused, false, 0, A20Gate.EndOfHighMemoryArea, _structureViewModelFactory));
CfgCpuViewModel = new(_messenger, uiDispatcherTimerFactory, new PerformanceMeasurer());
Dispatcher.UIThread.Post(ForceUpdate, DispatcherPriority.Background);
_messenger.Register<AddViewModelMessage<MemoryViewModel>>(this, (_, _) => NewMemoryViewCommand.Execute(null));
_messenger.Register<AddViewModelMessage<DisassemblyViewModel>>(this, (_, _) => NewDisassemblyViewCommand.Execute(null));
_messenger.Register<RemoveViewModelMessage<DisassemblyViewModel>>(this, (_, message) => CloseTab(message.Sender));
_messenger.Register<RemoveViewModelMessage<MemoryViewModel>>(this, (_, message) => CloseTab(message.Sender));
}

private void HandlePauseStatusChanged(bool isPaused) => IsPaused = isPaused;

private void NotifyViaMessageAboutPauseStatus(bool isPaused) => _messenger.Send(new PauseStatusChangedMessage(isPaused));

internal void CloseTab(IInternalDebugger internalDebuggerViewModel) {
switch (internalDebuggerViewModel) {
case MemoryViewModel memoryViewModel:
Expand All @@ -91,20 +102,24 @@ internal void CloseTab(IInternalDebugger internalDebuggerViewModel) {
}

[RelayCommand(CanExecute = nameof(IsPaused))]
public void NewMemoryView() {
MemoryViewModels.Add(new MemoryViewModel(this, _textClipboard, _uiDispatcherTimerFactory, _storageProvider, _pauseStatus, 0, _structureViewModelFactory));
}

public void NewMemoryView() =>
MemoryViewModels.Add(new MemoryViewModel(this, _messenger, _textClipboard, _uiDispatcherTimerFactory, _storageProvider, IsPaused, true, 0, A20Gate.EndOfHighMemoryArea, _structureViewModelFactory));

[RelayCommand(CanExecute = nameof(IsPaused))]
public void NewDisassemblyView() => DisassemblyViewModels.Add(new DisassemblyViewModel(this, _uiDispatcherTimerFactory, _pauseStatus));
public void NewDisassemblyView() =>
DisassemblyViewModels.Add(new DisassemblyViewModel(_messenger, true, IsPaused, _uiDispatcherTimerFactory));

[RelayCommand]
public void Pause() => _pauseStatus.IsPaused = _programExecutor.IsPaused = IsPaused = true;
public void Pause() {
IsPaused = true;
NotifyViaMessageAboutPauseStatus(IsPaused);
}

[RelayCommand(CanExecute = nameof(IsPaused))]
public void Continue() => _pauseStatus.IsPaused = _programExecutor.IsPaused = IsPaused = false;

private void OnPauseStatusChanged(object? sender, PropertyChangedEventArgs e) => IsPaused = _pauseStatus.IsPaused;
public void Continue() {
IsPaused = false;
NotifyViaMessageAboutPauseStatus(IsPaused);
}

[RelayCommand]
public void ForceUpdate() => UpdateValues(this, EventArgs.Empty);
Expand Down
43 changes: 12 additions & 31 deletions src/Spice86/ViewModels/DisassemblyViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ namespace Spice86.ViewModels;

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;

using Iced.Intel;

Expand All @@ -13,17 +14,13 @@ namespace Spice86.ViewModels;
using Spice86.Core.Emulator.InternalDebugger;
using Spice86.Core.Emulator.Memory;
using Spice86.Infrastructure;
using Spice86.Interfaces;
using Spice86.MemoryWrappers;
using Spice86.Models.Debugging;
using Spice86.Shared.Utils;

using System.Collections.Specialized;
using System.ComponentModel;
using Spice86.ViewModels.Messages;

public partial class DisassemblyViewModel : ViewModelBase, IInternalDebugger {
private readonly IPauseStatus _pauseStatus;
private readonly DebugWindowViewModel _debugWindowViewModel;
private readonly IMessenger _messenger;
private bool _needToUpdateDisassembly = true;
private IMemory? _memory;
private State? _state;
Expand Down Expand Up @@ -62,39 +59,25 @@ public uint? StartAddress {
[NotifyCanExecuteChangedFor(nameof(CloseTabCommand))]
private bool _canCloseTab;

public DisassemblyViewModel(DebugWindowViewModel debugWindowViewModel, IUIDispatcherTimerFactory dispatcherTimerFactory, IPauseStatus pauseStatus) {
_debugWindowViewModel = debugWindowViewModel;
_pauseStatus = pauseStatus;
IsPaused = pauseStatus.IsPaused;
_pauseStatus.PropertyChanged += OnPauseStatusChanged;
public DisassemblyViewModel(IMessenger messenger, bool isPaused, bool canCloseTab, IUIDispatcherTimerFactory dispatcherTimerFactory) {
IsPaused = isPaused;
CanCloseTab = canCloseTab;
_messenger = messenger;
_messenger.Register<PauseStatusChangedMessage>(this, HandlePauseStatusMessage);
dispatcherTimerFactory.StartNew(TimeSpan.FromMilliseconds(400), DispatcherPriority.Normal, UpdateValues);
UpdateCanCloseTabProperty();
debugWindowViewModel.DisassemblyViewModels.CollectionChanged += OnDebugViewModelCollectionChanged;
}

private void UpdateCanCloseTabProperty() {
CanCloseTab = _debugWindowViewModel.DisassemblyViewModels.Count > 1;
}

private void OnDebugViewModelCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) {
UpdateCanCloseTabProperty();
}

[RelayCommand(CanExecute = nameof(CanCloseTab))]
private void CloseTab() {
_debugWindowViewModel.CloseTab(this);
UpdateCanCloseTabProperty();
}
private void CloseTab() => _messenger.Send(new RemoveViewModelMessage<DisassemblyViewModel>(this));

private void UpdateValues(object? sender, EventArgs e) {
if (_needToUpdateDisassembly && IsPaused) {
UpdateDisassembly();
}
}

private void OnPauseStatusChanged(object? sender, PropertyChangedEventArgs e) {
IsPaused = _pauseStatus.IsPaused;
UpdateCanCloseTabProperty();
private void HandlePauseStatusMessage(object recipient, PauseStatusChangedMessage message) {
IsPaused = message.IsPaused;
if (!IsPaused) {
return;
}
Expand Down Expand Up @@ -124,9 +107,7 @@ public void Visit<T>(T component) where T : IDebuggableComponent {
}

[RelayCommand(CanExecute = nameof(IsPaused))]
public void NewDisassemblyView() {
_debugWindowViewModel.NewDisassemblyViewCommand.Execute(null);
}
public void NewDisassemblyView() => _messenger.Send(new AddViewModelMessage<DisassemblyViewModel>());

[RelayCommand(CanExecute = nameof(IsPaused))]
public void StepInstruction() => _programExecutor?.StepInstruction();
Expand Down
Loading

0 comments on commit 1b5cb66

Please sign in to comment.