Initial commit
This commit is contained in:
9
ParentWpf/App.xaml
Normal file
9
ParentWpf/App.xaml
Normal file
@@ -0,0 +1,9 @@
|
||||
<Application x:Class="ParentWpf.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:ParentWpf"
|
||||
StartupUri="MainWindow.xaml">
|
||||
<Application.Resources>
|
||||
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
13
ParentWpf/App.xaml.cs
Normal file
13
ParentWpf/App.xaml.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System.Configuration;
|
||||
using System.Data;
|
||||
using System.Windows;
|
||||
|
||||
namespace ParentWpf;
|
||||
|
||||
/// <summary>
|
||||
/// Interaction logic for App.xaml
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
{
|
||||
}
|
||||
|
||||
10
ParentWpf/AssemblyInfo.cs
Normal file
10
ParentWpf/AssemblyInfo.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Windows;
|
||||
|
||||
[assembly:ThemeInfo(
|
||||
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
||||
//(used if a resource is not found in the page,
|
||||
// or application resource dictionaries)
|
||||
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
||||
//(used if a resource is not found in the page,
|
||||
// app, or any theme specific resource dictionaries)
|
||||
)]
|
||||
129
ParentWpf/ChildSession.cs
Normal file
129
ParentWpf/ChildSession.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO.Pipes;
|
||||
using System.Text.Json;
|
||||
using CommIpc;
|
||||
|
||||
namespace ParentWpf;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
297
ParentWpf/MainViewModel.cs
Normal file
297
ParentWpf/MainViewModel.cs
Normal file
@@ -0,0 +1,297 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Pipes;
|
||||
using System.Text.Json;
|
||||
using System.Windows;
|
||||
using CommIpc;
|
||||
|
||||
namespace ParentWpf;
|
||||
|
||||
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)
|
||||
{
|
||||
// Prefer spawning the built exe. If not found, fall back to running the dll via dotnet.
|
||||
string? solutionRoot = TryFindSolutionRoot(AppContext.BaseDirectory);
|
||||
if (solutionRoot is null)
|
||||
{
|
||||
throw new InvalidOperationException("Could not find solution root (CommTester.slnx).");
|
||||
}
|
||||
|
||||
string debugExe = Path.Combine(solutionRoot, "ChildWorker", "bin", "Debug", "net10.0", "ChildWorker.exe");
|
||||
string releaseExe = Path.Combine(solutionRoot, "ChildWorker", "bin", "Release", "net10.0", "ChildWorker.exe");
|
||||
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 fileName;
|
||||
string arguments;
|
||||
if (File.Exists(debugExe))
|
||||
{
|
||||
fileName = debugExe;
|
||||
arguments = $"--pipe \"{pipeName}\" --id {childId}";
|
||||
}
|
||||
else if (File.Exists(releaseExe))
|
||||
{
|
||||
fileName = releaseExe;
|
||||
arguments = $"--pipe \"{pipeName}\" --id {childId}";
|
||||
}
|
||||
else if (File.Exists(debugDll))
|
||||
{
|
||||
fileName = "dotnet";
|
||||
arguments = $"\"{debugDll}\" --pipe \"{pipeName}\" --id {childId}";
|
||||
}
|
||||
else if (File.Exists(releaseDll))
|
||||
{
|
||||
fileName = "dotnet";
|
||||
arguments = $"\"{releaseDll}\" --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 Application.Current.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
HandleIncomingFrame(session, frame);
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await Application.Current.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
session.Status = "Error";
|
||||
session.AddLog($"[parent] Receive loop error: {ex.Message}");
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
await Application.Current.Dispatcher.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);
|
||||
}
|
||||
92
ParentWpf/MainWindow.xaml
Normal file
92
ParentWpf/MainWindow.xaml
Normal file
@@ -0,0 +1,92 @@
|
||||
<Window x:Class="ParentWpf.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
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"
|
||||
xmlns:local="clr-namespace:ParentWpf"
|
||||
mc:Ignorable="d"
|
||||
Title="CommTester (Parent)"
|
||||
Height="600"
|
||||
Width="1000">
|
||||
<Grid Margin="12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="260"/>
|
||||
<ColumnDefinition Width="12"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="12"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel Grid.Row="0"
|
||||
Grid.ColumnSpan="3"
|
||||
Orientation="Horizontal">
|
||||
<Button Content="Start 3 Children"
|
||||
Padding="12,6"
|
||||
Margin="0,0,8,0"
|
||||
Click="StartChildren_Click"/>
|
||||
<Button Content="Ping Selected"
|
||||
Padding="12,6"
|
||||
Margin="0,0,8,0"
|
||||
Click="PingSelected_Click"/>
|
||||
<Button Content="Start Work"
|
||||
Padding="12,6"
|
||||
Margin="0,0,8,0"
|
||||
Click="StartWork_Click"/>
|
||||
<Button Content="Cancel Work"
|
||||
Padding="12,6"
|
||||
Click="CancelWork_Click"/>
|
||||
</StackPanel>
|
||||
|
||||
<GroupBox Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
Header="Children">
|
||||
<DockPanel>
|
||||
<TextBlock DockPanel.Dock="Top"
|
||||
Margin="8,6,8,0"
|
||||
Foreground="Gray"
|
||||
FontSize="11"
|
||||
Text="Select a child to inspect its stream."/>
|
||||
<ListBox Margin="8"
|
||||
ItemsSource="{Binding Children}"
|
||||
SelectedItem="{Binding SelectedChild}"
|
||||
DisplayMemberPath="DisplayName"/>
|
||||
</DockPanel>
|
||||
</GroupBox>
|
||||
|
||||
<Grid Grid.Row="2"
|
||||
Grid.Column="2">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="12"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Grid.Row="0"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Text="{Binding SelectedChild.StatusLine}"/>
|
||||
|
||||
<ProgressBar Grid.Row="1"
|
||||
Height="18"
|
||||
Minimum="0"
|
||||
Maximum="100"
|
||||
Value="{Binding SelectedChild.ProgressPercent}"/>
|
||||
|
||||
<GroupBox Grid.Row="3"
|
||||
Header="Stream (Log/Progress/Result)">
|
||||
<ListBox Margin="8"
|
||||
ItemsSource="{Binding SelectedChild.Logs}"/>
|
||||
</GroupBox>
|
||||
|
||||
<TextBlock Grid.Row="4"
|
||||
Foreground="Gray"
|
||||
FontSize="11"
|
||||
Text="{Binding SelectedChild.PipePath}"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
48
ParentWpf/MainWindow.xaml.cs
Normal file
48
ParentWpf/MainWindow.xaml.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace ParentWpf;
|
||||
|
||||
/// <summary>
|
||||
/// Interaction logic for MainWindow.xaml
|
||||
/// </summary>
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private readonly MainViewModel _vm;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
_vm = new MainViewModel();
|
||||
DataContext = _vm;
|
||||
}
|
||||
|
||||
protected override async void OnClosed(EventArgs e)
|
||||
{
|
||||
base.OnClosed(e);
|
||||
try { await _vm.DisposeAsync(); } catch { }
|
||||
}
|
||||
|
||||
private async void StartChildren_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try { await _vm.StartChildrenAsync(count: 3); }
|
||||
catch (Exception ex) { MessageBox.Show(this, ex.Message, "Error"); }
|
||||
}
|
||||
|
||||
private async void PingSelected_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try { await _vm.PingSelectedAsync(); }
|
||||
catch (Exception ex) { MessageBox.Show(this, ex.Message, "Error"); }
|
||||
}
|
||||
|
||||
private async void StartWork_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try { await _vm.StartWorkSelectedAsync(); }
|
||||
catch (Exception ex) { MessageBox.Show(this, ex.Message, "Error"); }
|
||||
}
|
||||
|
||||
private async void CancelWork_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try { await _vm.CancelWorkSelectedAsync(); }
|
||||
catch (Exception ex) { MessageBox.Show(this, ex.Message, "Error"); }
|
||||
}
|
||||
}
|
||||
23
ParentWpf/NotifyBase.cs
Normal file
23
ParentWpf/NotifyBase.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace ParentWpf;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
15
ParentWpf/ParentWpf.csproj
Normal file
15
ParentWpf/ParentWpf.csproj
Normal file
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CommIpc\CommIpc.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net10.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseWPF>true</UseWPF>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user