Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 6 additions & 20 deletions src/SharpClient.App/MauiProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using SharpClient.Core.Sessions;
using SharpClient.Core.Triggers;
using SharpClient.Data;
using SharpClient.UI;

namespace SharpClient.App;

Expand Down Expand Up @@ -90,8 +91,6 @@ public static MauiApp CreateMauiApp()
builder.Services.AddSingleton<ISecretStore, MauiSecretStore>();
builder.Services.AddSingleton<INotifier, MauiNotifier>();
builder.Services.AddSingleton<SharpClient.Core.Platform.IPreferences, MauiPreferences>();
builder.Services.AddSingleton<SettingsViewModel>(sp =>
new SettingsViewModel(sp.GetRequiredService<SharpClient.Core.Platform.IPreferences>()));

// ── Data / persistence ────────────────────────────────────────────
// AppDbContext is transient so each call gets a fresh context; this
Expand All @@ -106,27 +105,14 @@ public static MauiApp CreateMauiApp()
builder.Services.AddSingleton<ISessionManager>(sp =>
sp.GetRequiredService<SessionManager>());

// ── View models ───────────────────────────────────────────────────
builder.Services.AddSingleton<SessionsViewModel>(sp =>
new SessionsViewModel(sp.GetRequiredService<ISessionManager>()));

builder.Services.AddSingleton<ProtocolPanelViewModel>(sp =>
new ProtocolPanelViewModel(sp.GetRequiredService<ISessionManager>()));

builder.Services.AddTransient<WorldManagerViewModel>(sp =>
new WorldManagerViewModel(
sp.GetRequiredService<IWorldStore>(),
sp.GetRequiredService<ISecretStore>(),
sp.GetRequiredService<ISessionManager>(),
sp.GetRequiredService<ISessionLauncher>()));
builder.Services.AddTransient<HistorySearchViewModel>(sp =>
new HistorySearchViewModel(
sp.GetRequiredService<ISessionHistory>(),
sp.GetRequiredService<IWorldStore>()));

// ── Session launcher (real telnet) ────────────────────────────────
builder.Services.AddTransient<ISessionLauncher, SharpClient.Core.Sessions.TelnetSessionLauncher>();

// ── View models ───────────────────────────────────────────────────
// Registered via the shared extension so MAUI and Web stay in lockstep (no host drift).
// Per-view view models are Transient here to match MAUI's per-request-less lifetime model.
builder.Services.AddSharpClientViewModels(ServiceLifetime.Transient);

// ── Trigger / alias engines (stateless) ──────────────────────────
builder.Services.AddSingleton<ITriggerEngine, TriggerEngine>();
builder.Services.AddSingleton<IAliasEngine, AliasEngine>();
Expand Down
58 changes: 58 additions & 0 deletions src/SharpClient.UI/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using Microsoft.Extensions.DependencyInjection;
using SharpClient.Core.Persistence;
using SharpClient.Core.Platform;
using SharpClient.Core.Presentation;
using SharpClient.Core.Sessions;

namespace SharpClient.UI;

/// <summary>
/// Shared DI registration for the presentation view models, referenced by BOTH the MAUI host
/// (<c>SharpClient.App</c>) and the Web host (<c>SharpClient.Web</c>). Keeping the registrations in
/// one place prevents host drift — previously each host registered the view models independently and
/// they diverged (e.g. <see cref="TriggerAliasEditorViewModel"/> was registered on Web but not MAUI,
/// crashing the Rules page on Android).
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Registers all six presentation view models. The three session/settings view models are always
/// singletons; the three per-view view models use <paramref name="perViewLifetime"/> (Transient
/// in MAUI, Scoped in Web). All constructor dependencies must already be registered by the host.
/// </summary>
public static IServiceCollection AddSharpClientViewModels(
this IServiceCollection services,
ServiceLifetime perViewLifetime)
{
// ── Shared-lifetime view models (Singleton in both hosts) ────────────
services.AddSingleton<SessionsViewModel>(sp =>
new SessionsViewModel(sp.GetRequiredService<ISessionManager>()));
services.AddSingleton<ProtocolPanelViewModel>(sp =>
new ProtocolPanelViewModel(sp.GetRequiredService<ISessionManager>()));
services.AddSingleton<SettingsViewModel>(sp =>
new SettingsViewModel(sp.GetRequiredService<IPreferences>()));

// ── Per-view view models (Transient in MAUI, Scoped in Web) ──────────
services.Add(new ServiceDescriptor(
typeof(WorldManagerViewModel),
sp => new WorldManagerViewModel(
sp.GetRequiredService<IWorldStore>(),
sp.GetRequiredService<ISecretStore>(),
sp.GetRequiredService<ISessionManager>(),
sp.GetRequiredService<ISessionLauncher>()),
perViewLifetime));
services.Add(new ServiceDescriptor(
typeof(HistorySearchViewModel),
sp => new HistorySearchViewModel(
sp.GetRequiredService<ISessionHistory>(),
sp.GetRequiredService<IWorldStore>()),
perViewLifetime));
services.Add(new ServiceDescriptor(
typeof(TriggerAliasEditorViewModel),
sp => new TriggerAliasEditorViewModel(
sp.GetRequiredService<IWorldStore>()),
perViewLifetime));

return services;
}
}
1 change: 1 addition & 0 deletions src/SharpClient.UI/SharpClient.UI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.8" />
</ItemGroup>

<ItemGroup>
Expand Down
22 changes: 4 additions & 18 deletions src/SharpClient.Web/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using SharpClient.Core.Sessions;
using SharpClient.Core.Triggers;
using SharpClient.Data;
using SharpClient.UI;
using SharpClient.Web;
using SharpClient.Web.Components;

Expand All @@ -20,8 +21,6 @@

// ── Platform services ──────────────────────────────────────────────────────
builder.Services.AddSingleton<IPreferences, WebPreferences>();
builder.Services.AddSingleton<SettingsViewModel>(sp =>
new SettingsViewModel(sp.GetRequiredService<IPreferences>()));

builder.Services.AddSingleton<IAppStorage, WebAppStorage>();
builder.Services.AddSingleton<ISecretStore, WebSecretStore>();
Expand All @@ -41,10 +40,6 @@
// ── Session management ─────────────────────────────────────────────────────
builder.Services.AddSingleton<SessionManager>();
builder.Services.AddSingleton<ISessionManager>(sp => sp.GetRequiredService<SessionManager>());
builder.Services.AddSingleton<SessionsViewModel>(sp =>
new SessionsViewModel(sp.GetRequiredService<ISessionManager>()));
builder.Services.AddSingleton<ProtocolPanelViewModel>(sp =>
new ProtocolPanelViewModel(sp.GetRequiredService<ISessionManager>()));

// ── Data / persistence ─────────────────────────────────────────────────────
builder.Services.AddScoped<AppDbContext>();
Expand All @@ -55,18 +50,9 @@
builder.Services.AddScoped<ISessionLauncher, TelnetSessionLauncher>();

// ── View models ────────────────────────────────────────────────────────────
builder.Services.AddScoped<WorldManagerViewModel>(sp => new WorldManagerViewModel(
sp.GetRequiredService<IWorldStore>(),
sp.GetRequiredService<ISecretStore>(),
sp.GetRequiredService<ISessionManager>(),
sp.GetRequiredService<ISessionLauncher>()));
builder.Services.AddScoped<TriggerAliasEditorViewModel>(sp =>
new TriggerAliasEditorViewModel(
sp.GetRequiredService<IWorldStore>()));
builder.Services.AddScoped<HistorySearchViewModel>(sp =>
new HistorySearchViewModel(
sp.GetRequiredService<ISessionHistory>(),
sp.GetRequiredService<IWorldStore>()));
// Registered via the shared extension so MAUI and Web stay in lockstep (no host drift).
// Per-view view models are Scoped here to match the Web's per-request scope.
builder.Services.AddSharpClientViewModels(ServiceLifetime.Scoped);

var app = builder.Build();

Expand Down
Loading