NFoundation.Photino.NET.Extensions 1.0.5

dotnet add package NFoundation.Photino.NET.Extensions --version 1.0.5
                    
NuGet\Install-Package NFoundation.Photino.NET.Extensions -Version 1.0.5
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="NFoundation.Photino.NET.Extensions" Version="1.0.5" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="NFoundation.Photino.NET.Extensions" Version="1.0.5" />
                    
Directory.Packages.props
<PackageReference Include="NFoundation.Photino.NET.Extensions" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add NFoundation.Photino.NET.Extensions --version 1.0.5
                    
#r "nuget: NFoundation.Photino.NET.Extensions, 1.0.5"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package NFoundation.Photino.NET.Extensions@1.0.5
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=NFoundation.Photino.NET.Extensions&version=1.0.5
                    
Install as a Cake Addin
#tool nuget:?package=NFoundation.Photino.NET.Extensions&version=1.0.5
                    
Install as a Cake Tool

NFoundation.Photino.NET.Extensions

An extension library for Photino.NET that adds typed messaging, automatic script injection, and enhanced logging capabilities.

Overview

NFoundation.Photino.NET.Extensions enhances the Photino.NET framework with a comprehensive set of utilities for building desktop applications with web UI. It provides a fluent API for setting up typed communication between .NET and JavaScript, automatic script injection, and seamless console logging integration.

Key Features

  • 🚀 Typed Messaging System - Type-safe communication between .NET and JavaScript
  • 📡 Request-Response Patterns - Async request handling with automatic response routing
  • 📜 Automatic Script Injection - Embedded JavaScript library with auto-initialization
  • 🪵 Console Logging Bridge - Forward JavaScript console messages to .NET ILogger
  • 🔥 Hot Reload for Development - Automatic page refresh when source files change (DEBUG builds only)
  • 🔍 Enhanced Photino Logging - Routes Photino log messages to ILogger instance, rather than the Console (not supported for AOT compiled apps)

Disclaimer

This project is an independent extension library for Photino.NET.
It is not affiliated with, endorsed by, or sponsored by the Photino.NET maintainers.

Photino.NET is a separate open-source project licensed under the Apache License 2.0.

Installation

Install the NuGet package:

dotnet add package NFoundation.Photino.NET.Extensions

Or via Package Manager Console:

Install-Package NFoundation.Photino.NET.Extensions

Prerequisites

  • .NET 8.0 or later
  • Photino.NET 3.2.3 or compatible version

Quick Start

Here's a minimal example to get you started:

using Microsoft.Extensions.Logging;
using Photino.NET;
using NFoundation.Photino.NET.Extensions;

// Set up logging
using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
var logger = loggerFactory.CreateLogger("MainWindow");

// Initialize Photino log patcher (optional but recommended - does not work for AOT compiled apps)
PhotinoWindowLogPatcher.Initialize();

var window = new PhotinoWindow()
    .SetLogger(logger)
    .SetTitle("My App")
    .SetSize(new System.Drawing.Size(1200, 800))

    // Register message handlers
    .RegisterMessageHandler<string>("say-hello", (name) =>
    {
        logger.LogInformation("Hello from {Name}!", name);
    })

    // Register request handlers
    .RegisterRequestHandler<UserRequest, UserResponse>("get-user", async (request) =>
    {
        // Your async logic here
        return new UserResponse { Name = "John Doe" };
    })

    // Enable automatic script injection with console log messages bridged to host
    .RegisterPhotinoScript()

    // Load your HTML with hot reload support (automatically enabled in DEBUG builds)
    .Load("wwwroot", "index.html");

window.WaitForClose();

In your HTML file:

<!DOCTYPE html>
<html>
<head>
    <title>My App</title>
</head>
<body>
    
    <script src="photino://photinoWindow.js"></script>

    <script>
        // Send a one-way message
        function sayHello() {
            PhotinoWindow.sendMessage('say-hello', 'JavaScript');
        }

        // Send a request and handle the response
        async function getUser() {
            try {
                const user = await PhotinoWindow.sendRequest('get-user', { id: 1 });
                console.log('User:', user.name);
            } catch (error) {
                console.error('Error:', error.message);
            }
        }
    </script>
</body>
</html>

Features in Detail

Typed Messaging System

The library provides a robust typed messaging system for communication between .NET and JavaScript.

One-Way Messages

Perfect for fire-and-forget scenarios like button clicks, notifications, or status updates.

.NET Side:

// Register a handler
window.RegisterMessageHandler<UserAction>("user-action", (action) =>
{
    logger.LogInformation("User {UserId} performed {ActionType}", action.UserId, action.Type);
});

JavaScript Side:

// Send message
PhotinoWindow.sendMessage('user-action', {
    userId: 123,
    type: 'login',
    timestamp: new Date().toISOString()
});
Request-Response Pattern

Ideal for data fetching, form validation, or any scenario requiring a response.

.NET Side:

// Register an async request handler
window.RegisterRequestHandler<GetUserRequest, UserResponse>("get-user", async (request) =>
{
    var user = await userService.GetUserAsync(request.UserId);
    return new UserResponse
    {
        Name = user.Name,
        Email = user.Email
    };
});

// Handle validation requests
window.RegisterRequestHandler<ValidateFormRequest, ValidationResult>("validate-form", async (request) =>
{
    var result = await formValidator.ValidateAsync(request);
    return result;
});

JavaScript Side:

// Make a request and handle the response
async function loadUser(userId) {
    try {
        const user = await PhotinoWindow.sendRequest('get-user', { userId: userId });
        document.getElementById('userName').textContent = user.name;
        document.getElementById('userEmail').textContent = user.email;
    } catch (error) {
        console.error('Failed to load user:', error.message);
    }
}

// Validate a form with timeout
async function validateForm(formData) {
    try {
        const result = await PhotinoWindow.sendRequest('validate-form', formData, 5000); // 5s timeout
        if (result.isValid) {
            showSuccess('Form is valid!');
        } else {
            showErrors(result.errors);
        }
    } catch (error) {
        showError('Validation failed: ' + error.message);
    }
}

JavaScript Integration

Automatic Script Injection

The library includes an embedded JavaScript client (photinoWindow.js) that's automatically served via a custom scheme handler.

Configuration Options:

  • enablePhotinoDebugLogging: When true, enables debug logging from the Photino JavaScript framework itself in the browser console. This is useful for debugging message passing issues between JavaScript and .NET.
  • forwardConsoleMessagesToLogger: When true (default), automatically forwards JavaScript console.log/warn/error messages to your .NET logger, allowing you to capture client-side logging in your server-side logs.
// Basic setup with auto-initialization
window.RegisterPhotinoScript();

// Advanced setup with options
window.RegisterPhotinoScript(
    scheme: "photino",                           // Custom scheme name (default: "photino")
    enablePhotinoDebugLogging: true,             // Enable debug output for the Photino JavaScript framework (default: false)
    forwardConsoleMessagesToLogger: true         // Forward JS console messages to .NET logger (default: true)
);

In your HTML:


<script src="photino://photinoWindow.js"></script>
PhotinoWindow JavaScript API

Once loaded, the PhotinoWindow object provides a clean API:

// Check status
const status = PhotinoWindow.getStatus()
console.log(`Initialized ${status.initialized}, Handler Count: ${status.messageHandlers}, Pending Requests: ${status.pendingRequests}`);

// Send message
PhotinoWindow.sendMessage(type, payload);

// Send request with optional timeout
PhotinoWindow.sendRequest(type, payload, timeout = 30000);

// Register message handler (from .NET to JS)
PhotinoWindow.onMessage(type, handler);

// Remove message handler
PhotinoWindow.offMessage(type);

// Clear all handlers
PhotinoWindow.clearHandlers();

Console Logging Bridge

Forward JavaScript console output to your .NET logger for unified logging.

Setup
// Enable console logging bridge (enabled by default, therefore forwardConsoleMessagesToLogger can be omitted if preferred)
window.RegisterPhotinoScript(forwardConsoleMessagesToLogger: true);
Usage

All JavaScript console methods are automatically forwarded:

console.log('This appears in .NET logger as LogDebug');
console.info('This appears as LogInformation');
console.warn('This appears as LogWarning');
console.error('This appears as LogError');
console.debug('This appears as LogTrace');

// Complex objects are automatically serialized
console.log('User data:', { id: 1, name: 'John' });

.NET Output:

[12:34:56] DEBUG [JS Console] This appears in .NET logger as LogDebug
[12:34:56] INFO  [JS Console] This appears as LogInformation
[12:34:56] WARN  [JS Console] This appears as LogWarning
[12:34:56] ERROR [JS Console] This appears as LogError
[12:34:56] TRACE [JS Console] This appears as LogTrace
[12:34:56] DEBUG [JS Console] User data: {"id":1,"name":"John"}

Hot Reload for Development

The library provides automatic hot reload functionality that monitors your web files for changes and refreshes the application automatically during development.

Basic Usage
var window = new PhotinoWindow()
    .SetLogger(logger)
    .SetTitle("My App")

    // Load with hot reload support - automatically enabled in DEBUG builds
    .Load("wwwroot", "index.html");

window.WaitForClose();
How It Works
  • Automatic Detection: Hot reload is enabled automatically in DEBUG builds and disabled in RELEASE builds
  • Path Resolution: Always loads from source directory (not bin/output) to ensure hot reload works properly
  • Multi-Window Support: Multiple windows can watch the same directory efficiently using shared file watchers
  • URL Support: Also works with development servers while still monitoring local files
  • Automatic Cleanup: File watchers are reference-counted and automatically disposed when the last window using them is closed or garbage collected
Supported Scenarios

Local Files:

// Watch wwwroot directory, load index.html from source
.Load("wwwroot", "index.html")

// Watch Resources/wwwroot, load admin.html
.Load("Resources/wwwroot", "admin.html")

Development Servers:

// Watch wwwroot for file changes, but load from development server
.Load("wwwroot", "http://localhost:3000")

// Watch Resources/wwwroot, load from HTTPS development server
.Load("Resources/wwwroot", "https://localhost:5001")
Advanced Configuration
// Custom hot reload configuration
.Load("wwwroot", "index.html", options =>
{
    options.DebounceDelay = 500;                    // Wait 500ms after changes stop
    options.FileFilter = "*.html,*.css,*.js";      // Only watch specific file types
    options.IncludeSubdirectories = true;           // Monitor subdirectories (default)
    options.EnableOnlyInDebug = false;              // Force enable in RELEASE builds
})
Path Resolution Logic

The hot reload system intelligently finds your source files:

  1. Project Root Detection: Searches up the directory tree for .csproj files
  2. Source Priority: Always prefers source directories over bin/output copies
  3. Embedded Resources: Supports Resources/wwwroot pattern for embedded resource projects
  4. Fallback Handling: Gracefully falls back to regular loading if source detection fails
Multi-Window Efficiency

When multiple windows watch the same directory:

// Both windows share a single file watcher for "wwwroot"
var window1 = new PhotinoWindow().Load("wwwroot", "index.html");
var window2 = new PhotinoWindow().Load("wwwroot", "admin.html");

// Automatic cleanup when windows are closed
window1.Close(); // Watcher continues for window2
window2.Close(); // Watcher is automatically disposed when last window closes
// Note: Hot reload watchers are also cleaned up automatically when windows
// are garbage collected, even without explicit Close() calls
Debugging Hot Reload

Enable debug logging to troubleshoot hot reload issues:

var window = new PhotinoWindow()
    .SetLogger(logger) // Hot reload uses this logger for debug output
    .Load("wwwroot", "index.html");

Debug Output:

[12:34:56] DEBUG Hot reload monitoring source path: C:\MyProject\wwwroot
[12:34:57] INFO  Hot reload triggered for path: C:\MyProject\wwwroot
[12:34:57] DEBUG Sent hot reload message to window
JavaScript Integration

The hot reload system works seamlessly with the included JavaScript library. When files change, a __reload message is sent to all affected windows, triggering a page refresh:

// This happens automatically - no JavaScript code needed
// But you can listen for the reload event if desired
PhotinoWindow.onMessage('__reload', () => {
    console.log('Page reload triggered');
    // Custom cleanup logic before reload if needed
});
Manual Reload

You can also trigger a reload manually from .NET:

// Trigger a reload of the current page
window.Reload();

Advanced Features

Enhanced Photino Logging

The library includes a Harmony-based patcher that intercepts Photino.NET's internal logging and routes it through your ILogger.

// Initialize the log patcher at application startup
PhotinoWindowLogPatcher.Initialize();

// Now all Photino internal logs will use your configured logger
var window = new PhotinoWindow()
    .SetLogger(logger)  // This logger will receive both extension and Photino logs
    // ... rest of configuration

Memory Management

The library uses ConditionalWeakTable<PhotinoWindow, PhotinoWindowData> for storing window-specific data, ensuring that:

  • Window data is automatically garbage collected when windows go out of scope
  • No memory leaks from long-running applications
  • Thread-safe access to window-specific configuration
  • Hot reload watchers are automatically cleaned up when their associated windows are collected

Note: The base PhotinoWindow class does not implement IDisposable. The library handles cleanup automatically through weak references and finalizers. Use the Close() method to explicitly close a window, or let the garbage collector handle cleanup when windows go out of scope. The custom Window wrapper class (used with dependency injection) does implement IDisposable for additional control.

JSON Serialization Configuration

Configure JSON serialization for your message payloads. The library starts with sensible defaults from JsonUtilities.GetSerializerOptions() and allows you to customize them:

// Optional
window.ConfigureJsonSerializerOptions(options =>
{
    // Default options are: camelCase, not indented
    // Included converters: JsonStringEnumConverter and JsonDateTimeConverter (microsecond precision)

    // Add JSON source generators for AOT/trimming support
    options.TypeInfoResolverChain.Add(MyJsonContext.Default);

    // Add custom converters
    options.Converters.Add(new JsonCustomConverter());
});

For AOT and trimming scenarios, create a JSON source generator context:

[JsonSerializable(typeof(MyRequestType))]
[JsonSerializable(typeof(MyResponseType))]
internal partial class MyJsonContext : JsonSerializerContext
{
}

Photino Window with Dependency Injection

The library provides a robust integration with Microsoft's dependency injection container and hosting infrastructure through the Window base class and service collection extensions.

Window Base Class

Create custom window classes by inheriting from Window and implementing the Configure method:

using Microsoft.Extensions.Logging;
using Photino.NET;
using NFoundation.Photino.NET.Extensions;

public class MainWindow : Window
{
    private readonly IMyService _myService;

    public MainWindow(ILogger<MainWindow> logger, IMyService myService)
        : base(logger)
    {
        _myService = myService;
    }

    protected override void Configure(PhotinoWindow window)
    {
        window
            .SetTitle("My Application")
            .SetSize(new Size(1200, 800))
            .Center()

            // Register handlers using injected services
            .RegisterRequestHandler<DataRequest, DataResponse>("get-data", async (request) =>
            {
                return await _myService.GetDataAsync(request);
            })

            // Load with hot reload support
            .Load("wwwroot", "index.html");
    }
}

Service Registration

The library provides two extension methods for registering windows with the DI container:

AddHostedWindow - Integrated with Host Lifetime

Use AddHostedWindow to register a window that integrates with the application host lifetime. The window will:

  • Open automatically when the host starts
  • Stop the application when the window is closed
  • Close automatically when the application stops
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = Host.CreateApplicationBuilder(args);

// Register services
builder.Services.AddSingleton<IMyService, MyService>();

// Register window as hosted service
builder.Services.AddHostedWindow<MainWindow>();

var host = builder.Build();
await host.RunAsync();
AddWindow - Manual Control

Use AddWindow to register a window for manual control. This is useful when you need to:

  • Open windows on demand
  • Manage multiple windows independently
  • Control window lifecycle programmatically
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = Host.CreateApplicationBuilder(args);

// Register services
builder.Services.AddSingleton<IMyService, MyService>();

// Register windows for manual control
builder.Services.AddWindow<MainWindow>();
builder.Services.AddWindow<SettingsWindow>();

var host = builder.Build();

// Open settings window when needed
void OpenSettings()
{
    var settingsWindow = host.Services.GetRequiredService<SettingsWindow>();
    settingsWindow.Open(parent: mainWindow);
}

// Runs hosted services and hosted windows
await host.RunAsync(); // App closes when MainWindow closes

Complete Example with DI

Here's a complete example showing dependency injection integration:

// Program.cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using NFoundation.Photino.NET.Extensions;

var builder = Host.CreateApplicationBuilder(args);

// Configure logging
builder.Logging.ClearProviders();
builder.Logging.AddConsole();

// Register application services
builder.Services.AddSingleton<IDataService, DataService>();
builder.Services.AddSingleton<ISettingsService, SettingsService>();

// Register main window as hosted service
builder.Services.AddHostedWindow<MainWindow>();

// Register additional windows for manual control
builder.Services.AddWindow<SettingsWindow>();
builder.Services.AddWindow<AboutWindow>();

var host = builder.Build();
await host.RunAsync();

// MainWindow.cs
public class MainWindow : Window
{
    private readonly IDataService _dataService;
    private readonly IServiceProvider _serviceProvider;

    public MainWindow(
        ILogger<MainWindow> logger,
        IDataService dataService,
        IServiceProvider serviceProvider)
        : base(logger)
    {
        _dataService = dataService;
        _serviceProvider = serviceProvider;
    }

    protected override void Configure(PhotinoWindow window)
    {
        window
            .SetTitle("My Application")
            .SetSize(new Size(1200, 800))
            .Center()
            .SetDevToolsEnabled(true)

            // Handle data requests
            .RegisterRequestHandler<GetDataRequest, DataResponse>("get-data",
                async (request) => await _dataService.GetDataAsync(request))

            // Handle window management
            .RegisterMessageHandler<string>("open-settings", (message) =>
            {
                var settingsWindow = _serviceProvider.GetRequiredService<SettingsWindow>();
                settingsWindow.Open(parent: this);
            })

            .Load("wwwroot", "index.html");
    }
}

// SettingsWindow.cs
public class SettingsWindow : Window
{
    private readonly ISettingsService _settingsService;

    public SettingsWindow(
        ILogger<SettingsWindow> logger,
        ISettingsService settingsService)
        : base(logger)
    {
        _settingsService = settingsService;
    }

    protected override void Configure(PhotinoWindow window)
    {
        window
            .SetTitle("Settings")
            .SetSize(new Size(600, 400))
            .Center()

            .RegisterRequestHandler<GetSettingsRequest, SettingsResponse>("get-settings",
                async (request) => await _settingsService.GetSettingsAsync())

            .RegisterRequestHandler<SaveSettingsRequest, bool>("save-settings",
                async (request) => await _settingsService.SaveSettingsAsync(request))

            .Load("wwwroot", "settings.html");
    }
}

Key Benefits

  • Full DI Support: Windows can have constructor dependencies injected
  • Lifetime Management: Automatic integration with IHostApplicationLifetime
  • Thread Safety: Windows run on proper STA threads (Windows) automatically
  • Resource Cleanup: Implements IDisposable for proper resource management
  • Service Scoping: Access to IServiceProvider for resolving additional services
  • Multiple Windows: Support for parent-child window relationships

Window Lifecycle

The Window base class provides these lifecycle features:

  • Open(): Opens the window on a dedicated thread
  • Close(): Explicitly closes the window
  • Reload(): Triggers a page reload
  • WaitForCloseAsync(): Asynchronously waits for window closure
  • Dispose(): Proper cleanup of resources

Windows can only be opened once. Attempting to open an already-opened window will throw an InvalidOperationException.

API Reference

Extension Methods

Method Description
SetLogger(ILogger) Configure logger for the window
ConfigureJsonSerializerOptions(Action<JsonSerializerOptions>) Configure JSON serialization options
RegisterMessageHandler<T>(string, Action<T>) Register one-way message handler
UnregisterMessageHandler(string) Remove message handler
RegisterRequestHandler<TReq, TRes>(string, Func<TReq, Task<TRes>>) Register async request handler
UnregisterRequestHandler(string) Remove request handler
SendMessage<T>(string, T) Send one-way message to JavaScript
Reload() Trigger a page reload in the browser window
Load(string, string) Load content with automatic hot reload support (watchPath, htmlPath)
Load(string, string, Action<HotReloadOptions>?) Load content with configurable hot reload options
RegisterPhotinoScript(string, bool, bool) Enable script injection with options (scheme, enablePhotinoDebugLogging, forwardConsoleMessagesToLogger)
ClearHandlers() Remove all registered handlers

Static Classes

Class Purpose
PhotinoWindowLogPatcher Harmony-based logging integration
PhotinoWindowExtensions Main extension methods

Example Project

The library includes a complete example application demonstrating all features:

Location: NFoundation.Templates.Photino.NET.App

To run the example:

cd src/NFoundation.Templates.Photino.NET.App
dotnet run

The example demonstrates:

  • Typed messaging patterns
  • Request-response handling
  • Console logging bridge
  • Error handling
  • UI interactions

Dependencies

Package Version Purpose
Photino.NET 3.2.3+ Core desktop framework
Microsoft.Extensions.Logging 8.0.0+ Logging abstraction
NFoundation.Json 1.0.0+ JSON utilities
Lib.Harmony 2.3.3+ Runtime method patching

Requirements

  • .NET 8.0 or later
  • Windows, macOS, or Linux (Photino.NET requirements)
  • Modern web browser engine (embedded in application)

License

This library is licensed under the Apache License 2.0.

It depends on:

  • Photino.NET, which is also licensed under Apache License 2.0.
  • Harmony, which is also licensed under MIT License.

See the NOTICE file for details.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.5 203 9/29/2025
1.0.4 189 9/23/2025
1.0.3 191 9/23/2025
1.0.2 194 9/22/2025
1.0.1 247 9/19/2025
1.0.0 251 9/19/2025