Some checks failed
CI / build-and-test (push) Failing after 27s
The WinUI 3 host now stands up the full engine pipeline on launch and
discovers participants from the operator's live Teams meeting. Verified
end-to-end against a real call: window opened, NDI runtime preflight
passed, IsoController spun up, and Participants observable yielded
three live entries ((Local), Active Speaker, Brendon Power) with their
TEAMSISO_<name> output names in the redesigned shell.
What this commit lands:
ViewModels/ (slim ports of the WPF host's view-models — engine layer
shared verbatim via ProjectReference):
* ObservableObject.cs — INPC base, mirrors the WPF version
* RelayCommand.cs — sync + typed + async variants; ICommand is the
same shared type across both hosts (System.ObjectModel.dll)
* ParticipantViewModel.cs — DisplayName / Initials / SourceCodec /
IsoStateLabel / DisplayedAudioLevel / IsActiveSpeaker / IsOnline /
OutputName + ToggleIsoCommand. Drops the WPF-specific thumbnail
WriteableBitmap path, clipboard, PreviewWindow, snapshot encoder —
those come back when the WinUI imaging pipeline is wired (Phase 5
of the migration plan).
* MainViewModel.cs — subscribes to IIsoController.Participants on a
DispatcherQueueSynchronizationContext, owns the ObservableCollection,
runs a 1Hz DispatcherQueueTimer for stats + active-speaker
highlight + session-elapsed text. Commands: EnableAllOnline,
StopAllIsos, RefreshDiscovery, DropRecordingMarker, ToggleByIndex.
App.xaml.cs:
* OnLaunched brings up MainWindow first, then fires WireEngineAsync
so the user sees the shell immediately while NDI preflight + engine
setup proceed.
* Full pipeline: EngineLogging → NdiInteropPInvoke (with friendly
fallback message if the NDI runtime isn't installed) → ConfigStore
at %APPDATA%\TeamsISO\config.json → NdiRuntimeProbe + scaler →
IsoPipeline factory → IsoController → MainViewModel →
MainWindow.AttachViewModel → IsoController.StartAsync.
* Logger writes to the same %LOCALAPPDATA%\TeamsISO\Logs as the WPF
host so a mixed-host operator sees a single timeline.
MainWindow.xaml + .xaml.cs:
* x:Name on the section header buttons (RefreshButton, StopAllButton,
EnableAllButton, MarkerButton) and on the status bar text + the
ParticipantsHost grid.
* AttachViewModel wires those buttons to view-model commands; pushes
StatusText + ParticipantCountText through PropertyChanged.
* BuildSimpleRow imperatively constructs each row (Grid with name +
codec + output + ISO toggle pill) instead of going through a
DataTemplate. Rationale: declaring a DataTemplate in
Grid.Resources OR loading one via XamlReader.Load both crash WinUI
3's XAML parser at runtime on this build host (same HR=0x802b000a
we saw with the SettingsDrawer NavigationView). Imperative
construction sidesteps the parser. The rich row template (avatar
circle, audio meter, active-speaker accent) returns in Phase 5
alongside the CommunityToolkit DataGrid swap.
* Per-row PropertyChanged subscriptions refresh DisplayName, codec,
output name, and ISO pill text as the engine pushes updates.
Verified live (PID 4824, 2026-05-13 08:02): three real Teams
participants appeared in the redesigned shell within seconds of launch,
status bar populated with "3 participants · 0 routing", section
header showed "Participants 3", and the title-bar live pills rendered
in their proper places (placeholder values for now; binding to
SessionElapsed / IsRecording lands in the next commit).
86 lines
2.8 KiB
C#
86 lines
2.8 KiB
C#
using System;
|
|
using System.Threading.Tasks;
|
|
using System.Windows.Input;
|
|
|
|
namespace TeamsISO.App.WinUI.ViewModels;
|
|
|
|
/// <summary>
|
|
/// Synchronous command — same shape as the WPF host's RelayCommand.
|
|
/// System.Windows.Input.ICommand is the shared base type across both
|
|
/// hosts (lives in System.ObjectModel.dll on .NET 8), so no rewrite
|
|
/// is needed beyond the namespace.
|
|
/// </summary>
|
|
public sealed class RelayCommand : ICommand
|
|
{
|
|
private readonly Action _execute;
|
|
private readonly Func<bool>? _canExecute;
|
|
|
|
public RelayCommand(Action execute, Func<bool>? canExecute = null)
|
|
{
|
|
_execute = execute;
|
|
_canExecute = canExecute;
|
|
}
|
|
|
|
public bool CanExecute(object? parameter) => _canExecute?.Invoke() ?? true;
|
|
public void Execute(object? parameter) => _execute();
|
|
public event EventHandler? CanExecuteChanged;
|
|
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
|
|
/// <summary>Typed-parameter variant for hotkey-driven commands (NumPad 1-9).</summary>
|
|
public sealed class RelayCommand<T> : ICommand
|
|
{
|
|
private readonly Action<T> _execute;
|
|
private readonly Func<T, bool>? _canExecute;
|
|
|
|
public RelayCommand(Action<T> execute, Func<T, bool>? canExecute = null)
|
|
{
|
|
_execute = execute;
|
|
_canExecute = canExecute;
|
|
}
|
|
|
|
public bool CanExecute(object? parameter) => _canExecute?.Invoke(Convert<T>(parameter)) ?? true;
|
|
public void Execute(object? parameter) => _execute(Convert<T>(parameter));
|
|
public event EventHandler? CanExecuteChanged;
|
|
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
|
|
|
private static TValue Convert<TValue>(object? value)
|
|
{
|
|
if (value is null) return default!;
|
|
if (value is TValue typed) return typed;
|
|
try { return (TValue)System.Convert.ChangeType(value, typeof(TValue)); }
|
|
catch { return default!; }
|
|
}
|
|
}
|
|
|
|
/// <summary>Async command that suppresses re-entrancy while in flight.</summary>
|
|
public sealed class AsyncRelayCommand : ICommand
|
|
{
|
|
private readonly Func<Task> _execute;
|
|
private readonly Func<bool>? _canExecute;
|
|
private bool _isRunning;
|
|
|
|
public AsyncRelayCommand(Func<Task> execute, Func<bool>? canExecute = null)
|
|
{
|
|
_execute = execute;
|
|
_canExecute = canExecute;
|
|
}
|
|
|
|
public bool CanExecute(object? parameter) => !_isRunning && (_canExecute?.Invoke() ?? true);
|
|
|
|
public async void Execute(object? parameter)
|
|
{
|
|
if (_isRunning) return;
|
|
_isRunning = true;
|
|
RaiseCanExecuteChanged();
|
|
try { await _execute(); }
|
|
finally
|
|
{
|
|
_isRunning = false;
|
|
RaiseCanExecuteChanged();
|
|
}
|
|
}
|
|
|
|
public event EventHandler? CanExecuteChanged;
|
|
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
|
}
|