-
Notifications
You must be signed in to change notification settings - Fork 1
desktop interop
The IDesktopInteropService is a bridge that provides Blazor components with direct access to desktop functionality in an Avalonia-based desktop application. This service enables your web-based UI to interact with native window management, file system operations, system paths, clipboard, and browser integration.
Key Characteristics:
- Injected as a dependency in Blazor components via
@inject IDesktopInteropService Desktop - Provides both
Task<T>andValueTask<T>return types for optimized performance - Security-hardened URL validation and file path handling
- Works with Photino's native notification API and JavaScript interop
Opens a native file picker dialog and returns the selected file path.
Signature:
Task<string?> OpenFileDialogAsync(FileDialogOptions? options = null)Parameters:
-
options- Optional configuration for the file dialog
Returns:
-
string?- Full path to the selected file, ornullif cancelled
Example:
@inject IDesktopInteropService Desktop
var filePath = await Desktop.OpenFileDialogAsync(new FileDialogOptions
{
Title = "Select a Document",
MultiSelect = false,
Filters = new List<FileFilter>
{
new FileFilter
{
Name = "Text Files",
Extensions = new[] { "*.txt", "*.md" }
},
new FileFilter
{
Name = "All Files",
Extensions = new[] { "*.*" }
}
}
});
if (!string.IsNullOrEmpty(filePath))
{
// Use filePath
}Notes:
- Returns
nullif the user cancels the dialog - File extensions are automatically normalized to
*.extensionformat - Returns only the first selected file (use OpenFolderDialogAsync for directories)
Opens a native save dialog and returns the selected file path.
Signature:
Task<string?> SaveFileDialogAsync(FileDialogOptions? options = null)Parameters:
-
options- Optional configuration for the save dialog
Returns:
-
string?- Full path where the file should be saved, ornullif cancelled
Example:
var savePath = await Desktop.SaveFileDialogAsync(new FileDialogOptions
{
Title = "Save Document As",
DefaultFileName = "document.txt",
Filters = new List<FileFilter>
{
new FileFilter
{
Name = "Text Documents",
Extensions = new[] { "txt" }
}
}
});
if (!string.IsNullOrEmpty(savePath))
{
var data = Encoding.UTF8.GetBytes("File content");
await Desktop.WriteFileAsync(savePath, data);
}Notes:
- Does not automatically add file extensions - include in
DefaultFileNameif desired - Returns
nullif the user cancels the dialog - Use with
WriteFileAsyncto persist the file
Opens a native folder/directory picker dialog.
Signature:
Task<string?> OpenFolderDialogAsync()Parameters:
- None
Returns:
-
string?- Full path to the selected folder, ornullif cancelled
Example:
var folderPath = await Desktop.OpenFolderDialogAsync();
if (!string.IsNullOrEmpty(folderPath))
{
var files = Directory.GetFiles(folderPath);
// Process files
}Notes:
- Single selection only (non-configurable)
- Returns
nullif the user cancels the dialog
Asynchronously reads the complete contents of a file as bytes.
Signature:
Task<byte[]> ReadFileAsync(string path)Parameters:
-
path- Full file path to read
Returns:
-
byte[]- Complete file contents
Example:
// Read binary file
var imageBytes = await Desktop.ReadFileAsync("image.png");
// Read text file
var textContent = Encoding.UTF8.GetString(
await Desktop.ReadFileAsync("document.txt")
);
// Read JSON file
var jsonData = JsonSerializer.Deserialize<MyModel>(
await Desktop.ReadFileAsync("config.json")
);Notes:
- Runs on a thread pool to avoid blocking
- Returns entire file contents in memory
- Not recommended for very large files
- Throws
FileNotFoundExceptionif file doesn't exist
Asynchronously writes byte data to a file, creating or overwriting it.
Signature:
Task WriteFileAsync(string path, byte[] data)Parameters:
-
path- Full file path to write to -
data- Byte array containing the file contents
Returns:
-
Task- Completes when write is finished
Example:
// Write text file
var content = "Hello, Desktop!";
await Desktop.WriteFileAsync("output.txt", Encoding.UTF8.GetBytes(content));
// Write JSON file
var settings = new { theme = "dark", language = "en" };
var json = JsonSerializer.Serialize(settings);
await Desktop.WriteFileAsync("config.json", Encoding.UTF8.GetBytes(json));
// Copy file contents
var original = await Desktop.ReadFileAsync("source.txt");
await Desktop.WriteFileAsync("backup.txt", original);Notes:
- Runs on a thread pool to avoid blocking
- Creates directories if they don't exist (you must create parent directories yourself)
- Overwrites existing files without warning
- Requires appropriate file system permissions
Checks whether a file exists at the specified path.
Signature:
ValueTask<bool> FileExistsAsync(string path)Parameters:
-
path- Full file path to check
Returns:
-
ValueTask<bool>-trueif file exists,falseotherwise
Example:
if (await Desktop.FileExistsAsync("config.json"))
{
var config = await Desktop.ReadFileAsync("config.json");
// Load configuration
}
else
{
// Initialize default configuration
}Notes:
- Uses
ValueTaskoptimization (no allocation if not awaited) - Synchronous operation with minimal overhead
- Returns
falsefor directories (checks files only)
Minimizes the main application window.
Signature:
ValueTask MinimizeWindowAsync()Parameters:
- None
Returns:
-
ValueTask- Completes when window state changes
Example:
<MudButton OnClick="@(async () => await Desktop.MinimizeWindowAsync())">
Minimize
</MudButton>Notes:
- ValueTask optimization for minimal overhead
- Silently fails if no main window is available
Maximizes the main application window.
Signature:
ValueTask MaximizeWindowAsync()Parameters:
- None
Returns:
-
ValueTask- Completes when window state changes
Example:
<MudButton OnClick="@(async () => await Desktop.MaximizeWindowAsync())">
Maximize
</MudButton>Notes:
- ValueTask optimization for minimal overhead
- Silently fails if no main window is available
Restores the window to normal (non-minimized, non-maximized) state.
Signature:
ValueTask RestoreWindowAsync()Parameters:
- None
Returns:
-
ValueTask- Completes when window state changes
Example:
<MudButton OnClick="@(async () => await Desktop.RestoreWindowAsync())">
Restore
</MudButton>Notes:
- ValueTask optimization for minimal overhead
- Silently fails if no main window is available
Changes the main application window's title.
Signature:
ValueTask SetWindowTitleAsync(string title)Parameters:
-
title- New window title text
Returns:
-
ValueTask- Completes when title is updated
Example:
await Desktop.SetWindowTitleAsync("My App - Editing: document.txt");Notes:
- ValueTask optimization for minimal overhead
- Silently fails if no main window is available
- Useful for displaying document name or status in window title
Retrieves the current state of the main application window.
Signature:
ValueTask<WindowState> GetWindowStateAsync()Parameters:
- None
Returns:
-
ValueTask<WindowState>- Current window state enum
WindowState Values:
-
Normal- Regular window size -
Minimized- Window is minimized -
Maximized- Window is maximized
Example:
var state = await Desktop.GetWindowStateAsync();
switch (state)
{
case WindowState.Normal:
Debug.WriteLine("Window is in normal state");
break;
case WindowState.Minimized:
Debug.WriteLine("Window is minimized");
break;
case WindowState.Maximized:
Debug.WriteLine("Window is maximized");
break;
}Notes:
- ValueTask optimization for minimal overhead
- Returns
WindowState.Normalif no main window is available - Useful for conditional UI rendering based on window state
Retrieves the application data directory, creating it if necessary.
Signature:
ValueTask<string> GetAppDataPathAsync()Parameters:
- None
Returns:
-
ValueTask<string>- Full path to application data directory
Example:
var appDataPath = await Desktop.GetAppDataPathAsync();
var settingsFile = Path.Combine(appDataPath, "settings.json");
// Use for saving user preferences, cached data, etc.
var settings = new { theme = "dark" };
await Desktop.WriteFileAsync(
settingsFile,
Encoding.UTF8.GetBytes(JsonSerializer.Serialize(settings))
);Notes:
- ValueTask optimization for minimal overhead
- Automatically creates directory if it doesn't exist
- Uses
Environment.SpecialFolder.ApplicationData - Platform-specific locations:
- Windows:
C:\Users\[User]\AppData\Roaming\[AppDataFolderName] - Linux:
~/.config/[AppDataFolderName] - macOS:
~/Library/Application Support/[AppDataFolderName]
- Windows:
- Directory name comes from
Constants.Defaults.AppDataFolderName
Retrieves the user's Documents directory.
Signature:
ValueTask<string> GetDocumentsPathAsync()Parameters:
- None
Returns:
-
ValueTask<string>- Full path to Documents directory
Example:
var documentsPath = await Desktop.GetDocumentsPathAsync();
var defaultSaveLocation = Path.Combine(documentsPath, "MyApp");
// Use as default save location
var savePath = await Desktop.SaveFileDialogAsync(new FileDialogOptions
{
Title = "Save Document",
DefaultFileName = Path.Combine(defaultSaveLocation, "document.txt")
});Notes:
- ValueTask optimization for minimal overhead
- Platform-specific locations:
- Windows:
C:\Users\[User]\Documents - Linux:
~/Documents - macOS:
~/Documents
- Windows:
Opens a URL in the system's default web browser.
Signature:
Task OpenUrlInBrowserAsync(string url)Parameters:
-
url- Complete URL to open (http, https, or mailto)
Returns:
-
Task- Completes when browser is opened
Example:
// Open website
await Desktop.OpenUrlInBrowserAsync("https://github.com");
// Open mailto link
await Desktop.OpenUrlInBrowserAsync("mailto:user@example.com?subject=Hello");
// Open with error handling
try
{
await Desktop.OpenUrlInBrowserAsync("https://example.com");
}
catch (ArgumentException ex)
{
// Handle invalid URL or unsupported scheme
Debug.WriteLine($"URL error: {ex.Message}");
}Supported URL Schemes:
http://https://mailto:
Throws:
-
ArgumentException- If URL is null, empty, invalid format, or uses unsupported scheme
Notes:
- Security-hardened: validates URL format and scheme before opening
- Runs on a thread pool
- Uses
UseShellExecute = truefor cross-platform browser launching - Prevents command injection attacks through strict validation
Displays a system notification.
Signature:
Task ShowNotificationAsync(string title, string message)Parameters:
-
title- Notification title -
message- Notification message body
Returns:
-
Task- Completes when notification is shown
Example:
await Desktop.ShowNotificationAsync(
"File Saved",
"Your document has been saved successfully."
);
await Desktop.ShowNotificationAsync(
"Warning",
"This action cannot be undone."
);Notes:
- Uses Photino's native notification API or JavaScript fallback
- Platform-specific notification appearance
- Async call completes immediately; notification may display after
Retrieves the current text content from the system clipboard.
Signature:
Task<string?> GetClipboardTextAsync()Parameters:
- None
Returns:
-
Task<string?>- Clipboard text content, ornullif empty
Example:
var clipboardContent = await Desktop.GetClipboardTextAsync();
if (!string.IsNullOrEmpty(clipboardContent))
{
Debug.WriteLine($"Clipboard: {clipboardContent}");
}Notes:
- Returns
nullif clipboard is empty - Only retrieves text (binary clipboard data not supported)
- Requires appropriate system permissions
Copies text to the system clipboard.
Signature:
Task SetClipboardTextAsync(string text)Parameters:
-
text- Text to copy to clipboard
Returns:
-
Task- Completes when text is copied
Example:
<MudButton OnClick="@(async () => {
await Desktop.SetClipboardTextAsync(\"Copied text!\");
await Desktop.ShowNotificationAsync(\"Copied\", \"Text copied to clipboard\");
})">
Copy to Clipboard
</MudButton>Notes:
- Only supports text content
- Overwrites previous clipboard contents
- Completes immediately; system may buffer the operation
Configuration class for file and folder picker dialogs.
Properties:
| Property | Type | Description |
|---|---|---|
Title |
string? |
Dialog window title. Defaults to "Open File" or "Save File" if not specified. |
MultiSelect |
bool |
Whether multiple files can be selected (OpenFileDialogAsync only). Default: false. Note: Only first selection is returned. |
DefaultFileName |
string? |
Suggested filename for save dialogs. Not added automatically - include extension if needed. |
Filters |
List<FileFilter>? |
List of file type filters to display in the dialog. |
Example:
var options = new FileDialogOptions
{
Title = "Select Image Files",
DefaultFileName = "image.png",
Filters = new List<FileFilter>
{
new FileFilter
{
Name = "Image Files",
Extensions = new[] { "*.jpg", "*.png", "*.gif" }
},
new FileFilter
{
Name = "All Files",
Extensions = new[] { "*.*" }
}
}
};
var selectedFile = await Desktop.OpenFileDialogAsync(options);Represents a file type filter in file dialogs.
Properties:
| Property | Type | Description |
|---|---|---|
Name |
string |
Display name for this filter (e.g., "Image Files", "Text Documents"). |
Extensions |
string[] |
Array of file extensions (e.g., ["*.jpg", "*.png"]). Automatically normalized to *.extension format. |
Example:
var filters = new List<FileFilter>
{
new FileFilter
{
Name = "C# Files",
Extensions = new[] { "cs", "csx" } // Both formats accepted
},
new FileFilter
{
Name = "Text Files",
Extensions = new[] { "*.txt", "*.md" } // Both formats accepted
},
new FileFilter
{
Name = "All Files",
Extensions = new[] { "*.*" }
}
};Notes:
- Extensions are automatically normalized (don't worry about
*.prefix) - Can use either
"txt"or"*.txt"- both are handled identically - Extension comparison is case-insensitive
A complete component demonstrating file operations:
@page "/file-manager"
@inject IDesktopInteropService Desktop
@using System.Text
<MudCard>
<MudCardHeader>
<MudText Typo="Typo.h5">File Manager</MudText>
</MudCardHeader>
<MudCardContent>
<MudStack>
<MudButtonGroup>
<MudButton Variant="Variant.Filled"
OnClick="@OpenFile">
Open File
</MudButton>
<MudButton Variant="Variant.Filled"
OnClick="@SaveFile">
Save File
</MudButton>
<MudButton Variant="Variant.Filled"
OnClick="@SelectFolder">
Select Folder
</MudButton>
</MudButtonGroup>
@if (!string.IsNullOrEmpty(currentFile))
{
<MudPaper Class="pa-4">
<MudText Typo="Typo.body1">
<strong>Current File:</strong> @currentFile
</MudText>
@if (!string.IsNullOrEmpty(fileContent))
{
<MudText Typo="Typo.body2" Class="mt-2">
@fileContent
</MudText>
}
</MudPaper>
}
</MudStack>
</MudCardContent>
</MudCard>
@code {
private string? currentFile;
private string? fileContent;
private async Task OpenFile()
{
var filePath = await Desktop.OpenFileDialogAsync(new FileDialogOptions
{
Title = "Select a Text File",
Filters = new List<FileFilter>
{
new FileFilter
{
Name = "Text Files",
Extensions = new[] { "*.txt", "*.md" }
},
new FileFilter
{
Name = "All Files",
Extensions = new[] { "*.*" }
}
}
});
if (!string.IsNullOrEmpty(filePath))
{
currentFile = filePath;
var data = await Desktop.ReadFileAsync(filePath);
fileContent = Encoding.UTF8.GetString(data);
}
}
private async Task SaveFile()
{
if (string.IsNullOrEmpty(fileContent))
{
await Desktop.ShowNotificationAsync("Error", "No content to save");
return;
}
var savePath = await Desktop.SaveFileDialogAsync(new FileDialogOptions
{
Title = "Save Text File",
DefaultFileName = "document.txt",
Filters = new List<FileFilter>
{
new FileFilter
{
Name = "Text Files",
Extensions = new[] { "txt" }
}
}
});
if (!string.IsNullOrEmpty(savePath))
{
await Desktop.WriteFileAsync(savePath, Encoding.UTF8.GetBytes(fileContent));
await Desktop.ShowNotificationAsync("Success", "File saved successfully");
currentFile = savePath;
}
}
private async Task SelectFolder()
{
var folderPath = await Desktop.OpenFolderDialogAsync();
if (!string.IsNullOrEmpty(folderPath))
{
currentFile = folderPath;
fileContent = null;
}
}
}A component for managing application settings:
@page "/settings"
@inject IDesktopInteropService Desktop
@using System.Text.Json
@using System.Text
<MudCard>
<MudCardHeader>
<MudText Typo="Typo.h5">Application Settings</MudText>
</MudCardHeader>
<MudCardContent>
<MudStack>
<MudTextField @bind-Value="settings.Theme"
Label="Theme" />
<MudTextField @bind-Value="settings.Language"
Label="Language" />
<MudCheckBox @bind-Checked="settings.NotificationsEnabled">
Enable Notifications
</MudCheckBox>
<MudButtonGroup>
<MudButton Variant="Variant.Filled"
OnClick="@LoadSettings">
Load
</MudButton>
<MudButton Variant="Variant.Filled"
OnClick="@SaveSettings">
Save
</MudButton>
</MudButtonGroup>
</MudStack>
</MudCardContent>
</MudCard>
@code {
private Settings settings = new();
private const string SettingsFileName = "settings.json";
protected override async Task OnInitializedAsync()
{
await LoadSettings();
}
private async Task LoadSettings()
{
try
{
var appDataPath = await Desktop.GetAppDataPathAsync();
var settingsPath = Path.Combine(appDataPath, SettingsFileName);
if (await Desktop.FileExistsAsync(settingsPath))
{
var json = Encoding.UTF8.GetString(
await Desktop.ReadFileAsync(settingsPath)
);
settings = JsonSerializer.Deserialize<Settings>(json) ?? new();
}
}
catch (Exception ex)
{
await Desktop.ShowNotificationAsync("Error", $"Failed to load settings: {ex.Message}");
}
}
private async Task SaveSettings()
{
try
{
var appDataPath = await Desktop.GetAppDataPathAsync();
var settingsPath = Path.Combine(appDataPath, SettingsFileName);
var json = JsonSerializer.Serialize(settings, new JsonSerializerOptions
{
WriteIndented = true
});
await Desktop.WriteFileAsync(settingsPath, Encoding.UTF8.GetBytes(json));
await Desktop.ShowNotificationAsync("Success", "Settings saved");
}
catch (Exception ex)
{
await Desktop.ShowNotificationAsync("Error", $"Failed to save settings: {ex.Message}");
}
}
public class Settings
{
public string Theme { get; set; } = "Light";
public string Language { get; set; } = "English";
public bool NotificationsEnabled { get; set; } = true;
}
}Several methods return ValueTask or ValueTask<T> instead of Task or Task<T>:
-
FileExistsAsync-ValueTask<bool> -
MinimizeWindowAsync-ValueTask -
MaximizeWindowAsync-ValueTask -
RestoreWindowAsync-ValueTask -
SetWindowTitleAsync-ValueTask -
GetWindowStateAsync-ValueTask<WindowState> -
GetAppDataPathAsync-ValueTask<string> -
GetDocumentsPathAsync-ValueTask<string>
Why ValueTask?
ValueTask is a value type that avoids heap allocation when the operation completes synchronously. This is beneficial for:
- Window Operations: These complete immediately without asynchronous work
- File Existence Check: Simple synchronous file system check
- System Paths: Computed synchronously from environment variables
- Reduced GC Pressure: No task object allocation on the heap
Usage Guidelines:
Always await ValueTask methods just like regular Tasks:
// Correct - works with both Task and ValueTask
var state = await Desktop.GetWindowStateAsync();
// Avoid - don't call .ConfigureAwait(false) with ValueTask
// This can cause additional allocationsFor library code that needs to handle ValueTask generically, use the Microsoft.Bcl.AsyncInterfaces NuGet package.
The OpenUrlInBrowserAsync method includes strict security validation:
Validation Rules:
- URL cannot be null or empty
- URL must be a valid absolute URI format
- Only
http,https, andmailtoschemes are allowed - Any other scheme throws
ArgumentException
Implementation:
// Built-in validation
if (string.IsNullOrWhiteSpace(url))
throw new ArgumentException("URL cannot be null or empty");
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
throw new ArgumentException("Invalid URL format");
if (!Constants.Security.AllowedUrlSchemes.Contains(uri.Scheme))
throw new ArgumentException($"URL scheme '{uri.Scheme}' is not allowed");Security Benefits:
- Prevents command injection through URL malformation
- Restricts to safe URL schemes only
- Fails fast with clear error messages
- No string concatenation with user input
Usage Pattern:
try
{
await Desktop.OpenUrlInBrowserAsync(userProvidedUrl);
}
catch (ArgumentException ex)
{
// Log and display error to user
Debug.WriteLine($"Invalid URL: {ex.Message}");
}File operations accept user-provided paths. Security best practices:
-
Validate Paths: Check for path traversal attempts
var fullPath = Path.GetFullPath(userProvidedPath); if (!fullPath.StartsWith(allowedDirectory)) throw new ArgumentException("Path outside allowed directory");
-
Use Sandboxed Directories: Restrict to:
-
GetAppDataPathAsync()for application data -
GetDocumentsPathAsync()for user documents - Dialog-selected paths only for arbitrary locations
-
-
Avoid Direct File Paths: Use dialogs when possible
// Safer - user selects the location var path = await Desktop.OpenFileDialogAsync(options); // Riskier - direct path from input var path = userProvidedPath; // Validate carefully
-
Example Safe Pattern:
private async Task<string?> SafeReadFile(string userPath) { try { var fullPath = Path.GetFullPath(userPath); var appDataPath = await Desktop.GetAppDataPathAsync(); // Verify path is within app data if (!fullPath.StartsWith(appDataPath)) throw new UnauthorizedAccessException("Access denied"); if (!await Desktop.FileExistsAsync(fullPath)) throw new FileNotFoundException("File not found"); var data = await Desktop.ReadFileAsync(fullPath); return Encoding.UTF8.GetString(data); } catch (Exception ex) { Debug.WriteLine($"File access error: {ex.Message}"); return null; } }
Register IDesktopInteropService in your application startup:
// Program.cs
builder.Services.AddScoped<IDesktopInteropService, DesktopInteropService>();Inject in Blazor components:
@inject IDesktopInteropService Desktop
@code {
private async Task DoSomething()
{
await Desktop.OpenUrlInBrowserAsync("https://example.com");
}
}