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
5 changes: 5 additions & 0 deletions src/demo/XBase.Demo.App/App.axaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="XBase.Demo.App.App">
</Application>
29 changes: 29 additions & 0 deletions src/demo/XBase.Demo.App/App.axaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Microsoft.Extensions.DependencyInjection;
using XBase.Demo.App.Views;

namespace XBase.Demo.App;

public partial class App : Application
{
public static IServiceProvider? Services { get; set; }

public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}

public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
var services = Services ?? throw new InvalidOperationException("Service provider is not initialized.");
desktop.MainWindow = services.GetRequiredService<MainWindow>();
}

base.OnFrameworkInitializationCompleted();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using XBase.Demo.App.ViewModels;
using XBase.Demo.App.Views;
using XBase.Demo.Diagnostics;
using XBase.Demo.Infrastructure;

namespace XBase.Demo.App.DependencyInjection;

public static class ServiceCollectionExtensions
{
public static IServiceCollection AddDemoApp(this IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);

services.AddXBaseDemoInfrastructure();
services.AddXBaseDemoDiagnostics();

services.AddSingleton<ShellViewModel>();
services.AddSingleton<MainWindow>();

return services;
}
}
44 changes: 44 additions & 0 deletions src/demo/XBase.Demo.App/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System;
using Avalonia;
using Avalonia.ReactiveUI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using XBase.Demo.App.DependencyInjection;

namespace XBase.Demo.App;

public static class Program
{
[STAThread]
public static int Main(string[] args)
{
using var host = CreateHost(args);
host.Start();

App.Services = host.Services;
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

App.Services is being set twice - once at line 18 and again in the AfterSetup callback at line 23. The first assignment appears redundant since the AfterSetup callback will override it.

Suggested change
App.Services = host.Services;

Copilot uses AI. Check for mistakes.

try
{
return BuildAvaloniaApp()
.AfterSetup(_ => App.Services = host.Services)
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

App.Services is being set twice - once at line 18 and again in the AfterSetup callback at line 23. The first assignment appears redundant since the AfterSetup callback will override it.

Copilot uses AI. Check for mistakes.
.StartWithClassicDesktopLifetime(args);
}
finally
{
host.StopAsync().GetAwaiter().GetResult();
}
}

private static IHost CreateHost(string[] args)
{
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddDemoApp();
return builder.Build();
}

private static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToTrace()
.UseReactiveUI();
}
107 changes: 107 additions & 0 deletions src/demo/XBase.Demo.App/ViewModels/ShellViewModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using System;
using System.Collections.Generic;
using System.Reactive;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ReactiveUI;
using XBase.Demo.Domain.Catalog;
using XBase.Demo.Domain.Diagnostics;
using XBase.Demo.Domain.Services;

namespace XBase.Demo.App.ViewModels;

public class ShellViewModel : ReactiveObject
{
private readonly ITableCatalogService _catalogService;
private readonly ITablePageService _pageService;
private readonly IDemoTelemetrySink _telemetrySink;
private readonly ILogger<ShellViewModel> _logger;

private string? _catalogRoot;
private string? _tableSummary;
private bool _isBusy;

public ShellViewModel(
ITableCatalogService catalogService,
ITablePageService pageService,
IDemoTelemetrySink telemetrySink,
ILogger<ShellViewModel> logger)
{
_catalogService = catalogService;
_pageService = pageService;
_telemetrySink = telemetrySink;
_logger = logger;

OpenCatalogCommand = ReactiveCommand.CreateFromTask<string>(ExecuteOpenCatalogAsync);
OpenCatalogCommand.ThrownExceptions.Subscribe(OnOpenCatalogFault);
}

public ReactiveCommand<string, Unit> OpenCatalogCommand { get; }

public string? CatalogRoot
{
get => _catalogRoot;
private set => this.RaiseAndSetIfChanged(ref _catalogRoot, value);
}

public string? TableSummary
{
get => _tableSummary;
private set => this.RaiseAndSetIfChanged(ref _tableSummary, value);
}

public bool IsBusy
{
get => _isBusy;
private set => this.RaiseAndSetIfChanged(ref _isBusy, value);
}

private async Task ExecuteOpenCatalogAsync(string rootPath)
{
if (string.IsNullOrWhiteSpace(rootPath))
{
return;
}

try
{
IsBusy = true;
CatalogRoot = rootPath;

var catalog = await _catalogService.LoadCatalogAsync(rootPath);
var payload = new Dictionary<string, object?>
{
["root"] = catalog.RootPath,
["tableCount"] = catalog.Tables.Count
};
_telemetrySink.Publish(new DemoTelemetryEvent("CatalogLoaded", DateTimeOffset.UtcNow, payload));

if (catalog.Tables.Count > 0)
{
var firstTable = catalog.Tables[0];
var page = await _pageService.LoadPageAsync(firstTable, new TablePageRequest(0, 25));
TableSummary = $"{catalog.Tables.Count} tables discovered. Preview rows: {page.Rows.Count} from {firstTable.Name}.";
}
else
{
TableSummary = "Catalog scanned successfully with no tables detected.";
}
}
finally
{
IsBusy = false;
}
}

private void OnOpenCatalogFault(Exception exception)
{
_logger.LogError(exception, "Catalog load failed");
var payload = new Dictionary<string, object?>
{
["message"] = exception.Message
};
_telemetrySink.Publish(new DemoTelemetryEvent("CatalogLoadFailed", DateTimeOffset.UtcNow, payload));
TableSummary = "Catalog load failed. Review diagnostics for details.";
}
}
22 changes: 22 additions & 0 deletions src/demo/XBase.Demo.App/Views/MainWindow.axaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:Class="XBase.Demo.App.Views.MainWindow"
Width="1024"
Height="640"
Title="xBase Demo Shell"
mc:Ignorable="d">
<Grid RowDefinitions="Auto,*" ColumnDefinitions="*">
<StackPanel Orientation="Horizontal" Margin="12" Spacing="12">
<TextBlock Text="Catalog Root:" VerticalAlignment="Center" FontWeight="Medium" />
<TextBlock Text="{Binding CatalogRoot, FallbackValue=Select a directory}" VerticalAlignment="Center" />
<TextBlock Text="{Binding TableSummary}" VerticalAlignment="Center" Margin="12,0,0,0" />
</StackPanel>

<Border Grid.Row="1" Margin="12" Padding="12" BorderBrush="#FF4D596A" BorderThickness="1" CornerRadius="6">
<TextBlock Text="Demo grid placeholder – wiring ready for implementation." TextWrapping="Wrap" />
</Border>
</Grid>
</Window>
32 changes: 32 additions & 0 deletions src/demo/XBase.Demo.App/Views/MainWindow.axaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
using Microsoft.Extensions.DependencyInjection;
using XBase.Demo.App.ViewModels;

namespace XBase.Demo.App.Views;

public partial class MainWindow : ReactiveWindow<ShellViewModel>
{
public MainWindow()
: this(ResolveViewModel())
{
}

public MainWindow(ShellViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
}

private static ShellViewModel ResolveViewModel()
{
var services = App.Services ?? throw new InvalidOperationException("Service provider is not available for MainWindow.");
return services.GetRequiredService<ShellViewModel>();
}

private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
20 changes: 20 additions & 0 deletions src/demo/XBase.Demo.App/XBase.Demo.App.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<AssemblyName>XBase.Demo.App</AssemblyName>
<RootNamespace>XBase.Demo.App</RootNamespace>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../XBase.Demo.Domain/XBase.Demo.Domain.csproj" />
<ProjectReference Include="../XBase.Demo.Infrastructure/XBase.Demo.Infrastructure.csproj" />
<ProjectReference Include="../XBase.Demo.Diagnostics/XBase.Demo.Diagnostics.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.1.3" />
<PackageReference Include="Avalonia.Desktop" Version="11.1.3" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.1.3" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
</ItemGroup>
</Project>
39 changes: 39 additions & 0 deletions src/demo/XBase.Demo.Diagnostics/InMemoryTelemetrySink.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using XBase.Demo.Domain.Diagnostics;

namespace XBase.Demo.Diagnostics;

/// <summary>
/// Captures telemetry events in-memory for UI consumption.
/// </summary>
public sealed class InMemoryTelemetrySink : IDemoTelemetrySink
{
private readonly ConcurrentQueue<DemoTelemetryEvent> _events = new();
private readonly ILogger<InMemoryTelemetrySink> _logger;

public InMemoryTelemetrySink(ILogger<InMemoryTelemetrySink> logger)
{
_logger = logger;
}

public void Publish(DemoTelemetryEvent telemetryEvent)
{
ArgumentNullException.ThrowIfNull(telemetryEvent);

_events.Enqueue(telemetryEvent);
while (_events.Count > 128 && _events.TryDequeue(out _))
{
}

_logger.LogInformation("Telemetry event {Name} captured", telemetryEvent.Name);
}

/// <summary>
/// Returns a snapshot of the buffered events for visualization.
/// </summary>
public IReadOnlyCollection<DemoTelemetryEvent> GetSnapshot()
=> _events.ToArray();
}
21 changes: 21 additions & 0 deletions src/demo/XBase.Demo.Diagnostics/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using XBase.Demo.Domain.Diagnostics;

namespace XBase.Demo.Diagnostics;

/// <summary>
/// Registers diagnostics sinks for the demo experience.
/// </summary>
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddXBaseDemoDiagnostics(this IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);

services.AddSingleton<InMemoryTelemetrySink>();
services.AddSingleton<IDemoTelemetrySink>(sp => sp.GetRequiredService<InMemoryTelemetrySink>());

return services;
}
}
11 changes: 11 additions & 0 deletions src/demo/XBase.Demo.Diagnostics/XBase.Demo.Diagnostics.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>XBase.Demo.Diagnostics</AssemblyName>
<RootNamespace>XBase.Demo.Diagnostics</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../XBase.Demo.Domain/XBase.Demo.Domain.csproj" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
</ItemGroup>
</Project>
26 changes: 26 additions & 0 deletions src/demo/XBase.Demo.Domain/Catalog/CatalogModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.Collections.Generic;

namespace XBase.Demo.Domain.Catalog;

/// <summary>
/// Represents a catalog of xBase tables discovered under a root directory.
/// </summary>
/// <param name="RootPath">The file system root the catalog was scanned from.</param>
/// <param name="Tables">The set of tables available for browsing.</param>
public sealed record CatalogModel(string RootPath, IReadOnlyList<TableModel> Tables);

/// <summary>
/// Represents a single table, including metadata necessary for browsing.
/// </summary>
/// <param name="Name">Logical table name.</param>
/// <param name="Path">Absolute path to the DBF file.</param>
/// <param name="Indexes">Associated indexes discovered for the table.</param>
public sealed record TableModel(string Name, string Path, IReadOnlyList<IndexModel> Indexes);

/// <summary>
/// Represents an index that can be applied while browsing.
/// </summary>
/// <param name="Name">Index identifier.</param>
/// <param name="Expression">The expression used to compute the index key.</param>
/// <param name="Order">Optional ordering hint for display.</param>
public sealed record IndexModel(string Name, string Expression, int Order = 0);
Loading