Switched to Avalonia
This commit is contained in:
10
ParentAvalonia/App.axaml
Normal file
10
ParentAvalonia/App.axaml
Normal file
@@ -0,0 +1,10 @@
|
||||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="ParentAvalonia.App"
|
||||
RequestedThemeVariant="Default">
|
||||
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
|
||||
|
||||
<Application.Styles>
|
||||
<FluentTheme />
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
23
ParentAvalonia/App.axaml.cs
Normal file
23
ParentAvalonia/App.axaml.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace ParentAvalonia;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
desktop.MainWindow = new MainWindow();
|
||||
}
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
}
|
||||
128
ParentAvalonia/ChildSession.cs
Normal file
128
ParentAvalonia/ChildSession.cs
Normal file
@@ -0,0 +1,128 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO.Pipes;
|
||||
using CommIpc;
|
||||
|
||||
namespace ParentAvalonia;
|
||||
|
||||
public sealed class ChildSession : NotifyBase, IAsyncDisposable
|
||||
{
|
||||
private readonly SemaphoreSlim _writeLock = new(1, 1);
|
||||
private string _status = "Starting";
|
||||
private double _progressPercent;
|
||||
private string? _currentWorkId;
|
||||
private bool _isConnected;
|
||||
|
||||
public int Id { get; }
|
||||
public string PipeName { get; }
|
||||
public string PipePath => CommIpc.PipeName.Describe(PipeName);
|
||||
|
||||
public NamedPipeServerStream Pipe { get; }
|
||||
public Process Process { get; }
|
||||
public CancellationTokenSource LifetimeCts { get; } = new();
|
||||
|
||||
public ObservableCollection<string> Logs { get; } = new();
|
||||
|
||||
public bool IsConnected
|
||||
{
|
||||
get => _isConnected;
|
||||
set
|
||||
{
|
||||
if (SetField(ref _isConnected, value))
|
||||
{
|
||||
OnPropertyChanged(nameof(DisplayName));
|
||||
OnPropertyChanged(nameof(StatusLine));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string Status
|
||||
{
|
||||
get => _status;
|
||||
set
|
||||
{
|
||||
if (SetField(ref _status, value))
|
||||
{
|
||||
OnPropertyChanged(nameof(DisplayName));
|
||||
OnPropertyChanged(nameof(StatusLine));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string DisplayName => $"Child {Id} ({(IsConnected ? "Connected" : "Disconnected")})";
|
||||
public string StatusLine => $"{Status} | Work: {(_currentWorkId ?? "-")}";
|
||||
|
||||
public double ProgressPercent
|
||||
{
|
||||
get => _progressPercent;
|
||||
set => SetField(ref _progressPercent, value);
|
||||
}
|
||||
|
||||
public string? CurrentWorkId
|
||||
{
|
||||
get => _currentWorkId;
|
||||
set
|
||||
{
|
||||
if (SetField(ref _currentWorkId, value))
|
||||
{
|
||||
OnPropertyChanged(nameof(StatusLine));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ChildSession(int id, string pipeName, NamedPipeServerStream pipe, Process process)
|
||||
{
|
||||
Id = id;
|
||||
PipeName = pipeName;
|
||||
Pipe = pipe;
|
||||
Process = process;
|
||||
}
|
||||
|
||||
public async Task SendAsync(IpcFrame frame, CancellationToken cancellationToken)
|
||||
{
|
||||
await _writeLock.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
await IpcProtocol.WriteFrameAsync(Pipe, frame, cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_writeLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public void AddLog(string line)
|
||||
{
|
||||
// keep it from growing without bounds during prototyping
|
||||
if (Logs.Count > 2000)
|
||||
{
|
||||
Logs.RemoveAt(0);
|
||||
}
|
||||
Logs.Add(line);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
LifetimeCts.Cancel();
|
||||
}
|
||||
catch { }
|
||||
|
||||
try
|
||||
{
|
||||
if (!Process.HasExited)
|
||||
{
|
||||
Process.Kill(entireProcessTree: true);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
try { Pipe.Dispose(); } catch { }
|
||||
try { Process.Dispose(); } catch { }
|
||||
try { LifetimeCts.Dispose(); } catch { }
|
||||
try { _writeLock.Dispose(); } catch { }
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
282
ParentAvalonia/MainViewModel.cs
Normal file
282
ParentAvalonia/MainViewModel.cs
Normal file
@@ -0,0 +1,282 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Pipes;
|
||||
using Avalonia.Threading;
|
||||
using CommIpc;
|
||||
|
||||
namespace ParentAvalonia;
|
||||
|
||||
public sealed class MainViewModel : NotifyBase, IAsyncDisposable
|
||||
{
|
||||
private ChildSession? _selectedChild;
|
||||
private bool _childrenStarted;
|
||||
private readonly CancellationTokenSource _shutdownCts = new();
|
||||
|
||||
public ObservableCollection<ChildSession> Children { get; } = new();
|
||||
|
||||
public ChildSession? SelectedChild
|
||||
{
|
||||
get => _selectedChild;
|
||||
set => SetField(ref _selectedChild, value);
|
||||
}
|
||||
|
||||
public async Task StartChildrenAsync(int count)
|
||||
{
|
||||
if (_childrenStarted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_childrenStarted = true;
|
||||
|
||||
for (int i = 1; i <= count; i++)
|
||||
{
|
||||
await StartChildAsync(i, _shutdownCts.Token);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PingSelectedAsync()
|
||||
{
|
||||
if (SelectedChild is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string corr = Guid.NewGuid().ToString("N");
|
||||
await SelectedChild.SendAsync(
|
||||
new IpcFrame(IpcKinds.Ping, CorrelationId: corr, Payload: IpcProtocol.ToJsonElement(new { from = "parent" })),
|
||||
SelectedChild.LifetimeCts.Token);
|
||||
|
||||
SelectedChild.AddLog($"[parent] -> ping ({corr})");
|
||||
}
|
||||
|
||||
public async Task StartWorkSelectedAsync(int steps = 30, int delayMs = 120)
|
||||
{
|
||||
if (SelectedChild is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string corr = Guid.NewGuid().ToString("N");
|
||||
SelectedChild.CurrentWorkId = corr;
|
||||
SelectedChild.ProgressPercent = 0;
|
||||
|
||||
await SelectedChild.SendAsync(
|
||||
new IpcFrame(
|
||||
IpcKinds.StartWork,
|
||||
CorrelationId: corr,
|
||||
Payload: IpcProtocol.ToJsonElement(new { steps, delayMs })),
|
||||
SelectedChild.LifetimeCts.Token);
|
||||
|
||||
SelectedChild.AddLog($"[parent] -> startWork (corr={corr}, steps={steps}, delayMs={delayMs})");
|
||||
}
|
||||
|
||||
public async Task CancelWorkSelectedAsync()
|
||||
{
|
||||
if (SelectedChild is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string corr = SelectedChild.CurrentWorkId ?? Guid.NewGuid().ToString("N");
|
||||
await SelectedChild.SendAsync(
|
||||
new IpcFrame(IpcKinds.CancelWork, CorrelationId: corr),
|
||||
SelectedChild.LifetimeCts.Token);
|
||||
|
||||
SelectedChild.AddLog($"[parent] -> cancelWork (corr={corr})");
|
||||
}
|
||||
|
||||
private async Task StartChildAsync(int childId, CancellationToken cancellationToken)
|
||||
{
|
||||
string pipeName = PipeName.ForChild(childId, Environment.ProcessId);
|
||||
var server = new NamedPipeServerStream(
|
||||
pipeName: pipeName,
|
||||
direction: PipeDirection.InOut,
|
||||
maxNumberOfServerInstances: 1,
|
||||
transmissionMode: PipeTransmissionMode.Byte,
|
||||
options: PipeOptions.Asynchronous);
|
||||
|
||||
Task waitForConnection = server.WaitForConnectionAsync(cancellationToken);
|
||||
|
||||
Process process = StartChildProcess(childId, pipeName);
|
||||
var session = new ChildSession(childId, pipeName, server, process);
|
||||
Children.Add(session);
|
||||
SelectedChild ??= session;
|
||||
session.AddLog($"[parent] Starting {process.StartInfo.FileName} {process.StartInfo.Arguments}");
|
||||
|
||||
await waitForConnection;
|
||||
session.IsConnected = true;
|
||||
session.Status = "Connected";
|
||||
session.AddLog($"[parent] Connected to {PipeName.Describe(pipeName)}");
|
||||
|
||||
_ = Task.Run(() => ReceiveLoopAsync(session), _shutdownCts.Token);
|
||||
}
|
||||
|
||||
private static Process StartChildProcess(int childId, string pipeName)
|
||||
{
|
||||
// Cross-platform friendly: prefer running the built dll via 'dotnet'.
|
||||
string? solutionRoot = TryFindSolutionRoot(AppContext.BaseDirectory);
|
||||
if (solutionRoot is null)
|
||||
{
|
||||
throw new InvalidOperationException("Could not find solution root (CommTester.slnx).");
|
||||
}
|
||||
|
||||
string debugDll = Path.Combine(solutionRoot, "ChildWorker", "bin", "Debug", "net10.0", "ChildWorker.dll");
|
||||
string releaseDll = Path.Combine(solutionRoot, "ChildWorker", "bin", "Release", "net10.0", "ChildWorker.dll");
|
||||
|
||||
string childDll = File.Exists(debugDll)
|
||||
? debugDll
|
||||
: File.Exists(releaseDll)
|
||||
? releaseDll
|
||||
: string.Empty;
|
||||
|
||||
string fileName;
|
||||
string arguments;
|
||||
if (!string.IsNullOrWhiteSpace(childDll))
|
||||
{
|
||||
fileName = "dotnet";
|
||||
arguments = $"\"{childDll}\" --pipe \"{pipeName}\" --id {childId}";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Last-resort: run via project (slower but robust in fresh checkouts).
|
||||
string csproj = Path.Combine(solutionRoot, "ChildWorker", "ChildWorker.csproj");
|
||||
fileName = "dotnet";
|
||||
arguments = $"run --project \"{csproj}\" -- --pipe \"{pipeName}\" --id {childId}";
|
||||
}
|
||||
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = fileName,
|
||||
Arguments = arguments,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
var p = new Process { StartInfo = psi, EnableRaisingEvents = true };
|
||||
p.Start();
|
||||
return p;
|
||||
}
|
||||
|
||||
private static string? TryFindSolutionRoot(string baseDirectory)
|
||||
{
|
||||
var dir = new DirectoryInfo(baseDirectory);
|
||||
for (int i = 0; i < 10 && dir is not null; i++)
|
||||
{
|
||||
string slnx = Path.Combine(dir.FullName, "CommTester.slnx");
|
||||
if (File.Exists(slnx))
|
||||
{
|
||||
return dir.FullName;
|
||||
}
|
||||
dir = dir.Parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task ReceiveLoopAsync(ChildSession session)
|
||||
{
|
||||
try
|
||||
{
|
||||
while (!session.LifetimeCts.IsCancellationRequested)
|
||||
{
|
||||
IpcFrame? frame = await IpcProtocol.ReadFrameAsync(session.Pipe, cancellationToken: session.LifetimeCts.Token);
|
||||
if (frame is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
await Dispatcher.UIThread.InvokeAsync(() => HandleIncomingFrame(session, frame));
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
session.Status = "Error";
|
||||
session.AddLog($"[parent] Receive loop error: {ex.Message}");
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
session.IsConnected = false;
|
||||
session.Status = "Disconnected";
|
||||
session.AddLog("[parent] Disconnected.");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleIncomingFrame(ChildSession session, IpcFrame frame)
|
||||
{
|
||||
switch (frame.Kind)
|
||||
{
|
||||
case IpcKinds.Hello:
|
||||
{
|
||||
var hello = IpcProtocol.FromJsonElement<HelloPayload>(frame.Payload);
|
||||
session.AddLog($"[child] hello: id={hello?.ChildId}, pid={hello?.Pid}");
|
||||
break;
|
||||
}
|
||||
case IpcKinds.Pong:
|
||||
session.AddLog($"[child] pong ({frame.CorrelationId})");
|
||||
break;
|
||||
|
||||
case IpcKinds.Log:
|
||||
{
|
||||
var log = IpcProtocol.FromJsonElement<LogPayload>(frame.Payload);
|
||||
session.AddLog($"[child:{log?.Level ?? "info"}] {log?.Message}");
|
||||
break;
|
||||
}
|
||||
case IpcKinds.Progress:
|
||||
{
|
||||
var prog = IpcProtocol.FromJsonElement<ProgressPayload>(frame.Payload);
|
||||
if (prog is not null)
|
||||
{
|
||||
session.ProgressPercent = prog.Percent;
|
||||
session.Status = $"Working ({prog.Step}/{prog.Total})";
|
||||
}
|
||||
break;
|
||||
}
|
||||
case IpcKinds.Result:
|
||||
{
|
||||
var res = IpcProtocol.FromJsonElement<ResultPayload>(frame.Payload);
|
||||
session.ProgressPercent = 100;
|
||||
session.Status = "Idle";
|
||||
session.AddLog($"[child] result: {res?.Message}");
|
||||
break;
|
||||
}
|
||||
case IpcKinds.Error:
|
||||
{
|
||||
var err = IpcProtocol.FromJsonElement<ErrorPayload>(frame.Payload);
|
||||
session.Status = "Error";
|
||||
session.AddLog($"[child] error: {err?.Message}");
|
||||
break;
|
||||
}
|
||||
default:
|
||||
session.AddLog($"[child] {frame.Kind} ({frame.CorrelationId})");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
_shutdownCts.Cancel();
|
||||
|
||||
foreach (var child in Children.ToArray())
|
||||
{
|
||||
try { await child.DisposeAsync(); } catch { }
|
||||
}
|
||||
|
||||
_shutdownCts.Dispose();
|
||||
}
|
||||
|
||||
private sealed record HelloPayload(int ChildId, int Pid);
|
||||
private sealed record LogPayload(string Level, string Message);
|
||||
private sealed record ProgressPayload(int Step, int Total, double Percent);
|
||||
private sealed record ResultPayload(string Message);
|
||||
private sealed record ErrorPayload(string Message);
|
||||
}
|
||||
52
ParentAvalonia/MainWindow.axaml
Normal file
52
ParentAvalonia/MainWindow.axaml
Normal file
@@ -0,0 +1,52 @@
|
||||
<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"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="ParentAvalonia.MainWindow"
|
||||
Title="CommTester (Parent - Avalonia)" Width="1000" Height="650">
|
||||
<Grid Margin="12" RowDefinitions="Auto,12,*" ColumnDefinitions="260,12,*">
|
||||
|
||||
<StackPanel Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3" Orientation="Horizontal" Spacing="8">
|
||||
<Button Content="Start 3 Children" Padding="12,6" Click="StartChildren_Click" />
|
||||
<Button Content="Ping Selected" Padding="12,6" Click="PingSelected_Click" />
|
||||
<Button Content="Start Work" Padding="12,6" Click="StartWork_Click" />
|
||||
<Button Content="Cancel Work" Padding="12,6" Click="CancelWork_Click" />
|
||||
</StackPanel>
|
||||
|
||||
<Border Grid.Row="2" Grid.Column="0" BorderBrush="#444" BorderThickness="1" CornerRadius="6">
|
||||
<DockPanel Margin="10">
|
||||
<TextBlock DockPanel.Dock="Top" FontSize="14" FontWeight="SemiBold" Text="Children" />
|
||||
<TextBlock DockPanel.Dock="Top" Margin="0,6,0,0" Foreground="Gray" FontSize="11"
|
||||
Text="Select a child to inspect its stream." />
|
||||
<ListBox Margin="0,8,0,0"
|
||||
ItemsSource="{Binding Children}"
|
||||
SelectedItem="{Binding SelectedChild}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding DisplayName}" />
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<Grid Grid.Row="2" Grid.Column="2" RowDefinitions="Auto,Auto,12,*,Auto">
|
||||
<TextBlock Grid.Row="0" FontSize="14" FontWeight="SemiBold"
|
||||
Text="{Binding SelectedChild.StatusLine, FallbackValue=No child selected}" />
|
||||
|
||||
<ProgressBar Grid.Row="1" Height="18" Minimum="0" Maximum="100"
|
||||
Value="{Binding SelectedChild.ProgressPercent, FallbackValue=0}" />
|
||||
|
||||
<Border Grid.Row="3" BorderBrush="#444" BorderThickness="1" CornerRadius="6">
|
||||
<DockPanel Margin="10">
|
||||
<TextBlock DockPanel.Dock="Top" FontSize="14" FontWeight="SemiBold" Text="Stream (Log/Progress/Result)" />
|
||||
<ListBox Margin="0,8,0,0" ItemsSource="{Binding SelectedChild.Logs}" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<TextBlock Grid.Row="4" Foreground="Gray" FontSize="11"
|
||||
Text="{Binding SelectedChild.PipePath, FallbackValue=-}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
77
ParentAvalonia/MainWindow.axaml.cs
Normal file
77
ParentAvalonia/MainWindow.axaml.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
|
||||
namespace ParentAvalonia;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private readonly MainViewModel _vm;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
_vm = new MainViewModel();
|
||||
DataContext = _vm;
|
||||
|
||||
Closed += async (_, __) =>
|
||||
{
|
||||
try { await _vm.DisposeAsync(); } catch { }
|
||||
};
|
||||
}
|
||||
|
||||
private async void StartChildren_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
try { await _vm.StartChildrenAsync(count: 3); }
|
||||
catch (Exception ex) { await MessageBoxAsync(ex.Message); }
|
||||
}
|
||||
|
||||
private async void PingSelected_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
try { await _vm.PingSelectedAsync(); }
|
||||
catch (Exception ex) { await MessageBoxAsync(ex.Message); }
|
||||
}
|
||||
|
||||
private async void StartWork_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
try { await _vm.StartWorkSelectedAsync(); }
|
||||
catch (Exception ex) { await MessageBoxAsync(ex.Message); }
|
||||
}
|
||||
|
||||
private async void CancelWork_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
try { await _vm.CancelWorkSelectedAsync(); }
|
||||
catch (Exception ex) { await MessageBoxAsync(ex.Message); }
|
||||
}
|
||||
|
||||
private async Task MessageBoxAsync(string message)
|
||||
{
|
||||
var dlg = new Window
|
||||
{
|
||||
Title = "Error",
|
||||
Width = 500,
|
||||
Height = 160,
|
||||
Content = new StackPanel
|
||||
{
|
||||
Margin = new Avalonia.Thickness(12),
|
||||
Spacing = 12,
|
||||
Children =
|
||||
{
|
||||
new TextBlock { Text = message, TextWrapping = Avalonia.Media.TextWrapping.Wrap },
|
||||
new Button
|
||||
{
|
||||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right,
|
||||
Content = "OK",
|
||||
IsDefault = true
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (dlg.Content is StackPanel sp && sp.Children.LastOrDefault() is Button ok)
|
||||
{
|
||||
ok.Click += (_, __) => dlg.Close();
|
||||
}
|
||||
|
||||
await dlg.ShowDialog(this);
|
||||
}
|
||||
}
|
||||
23
ParentAvalonia/NotifyBase.cs
Normal file
23
ParentAvalonia/NotifyBase.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace ParentAvalonia;
|
||||
|
||||
public abstract class NotifyBase : INotifyPropertyChanged
|
||||
{
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
|
||||
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
field = value;
|
||||
OnPropertyChanged(propertyName);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
27
ParentAvalonia/ParentAvalonia.csproj
Normal file
27
ParentAvalonia/ParentAvalonia.csproj
Normal file
@@ -0,0 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<AvaloniaUseCompiledBindingsByDefault>false</AvaloniaUseCompiledBindingsByDefault>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="11.3.11" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.3.11" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.11" />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.11" />
|
||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.11">
|
||||
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
|
||||
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CommIpc\CommIpc.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
21
ParentAvalonia/Program.cs
Normal file
21
ParentAvalonia/Program.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Avalonia;
|
||||
using System;
|
||||
|
||||
namespace ParentAvalonia;
|
||||
|
||||
class Program
|
||||
{
|
||||
// Initialization code. Don't use any Avalonia, third-party APIs or any
|
||||
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
|
||||
// yet and stuff might break.
|
||||
[STAThread]
|
||||
public static void Main(string[] args) => BuildAvaloniaApp()
|
||||
.StartWithClassicDesktopLifetime(args);
|
||||
|
||||
// Avalonia configuration, don't remove; also used by visual designer.
|
||||
public static AppBuilder BuildAvaloniaApp()
|
||||
=> AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
.WithInterFont()
|
||||
.LogToTrace();
|
||||
}
|
||||
18
ParentAvalonia/app.manifest
Normal file
18
ParentAvalonia/app.manifest
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<!-- This manifest is used on Windows only.
|
||||
Don't remove it as it might cause problems with window transparency and embedded controls.
|
||||
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
|
||||
<assemblyIdentity version="1.0.0.0" name="ParentAvalonia.Desktop"/>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- A list of the Windows versions that this application has been tested on
|
||||
and is designed to work with. Uncomment the appropriate elements
|
||||
and Windows will automatically select the most compatible environment. -->
|
||||
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
Reference in New Issue
Block a user