From 1c4ec6e057268df234f1797938e99d2b4c8b05fc Mon Sep 17 00:00:00 2001 From: "Gordon Lam (SH)" Date: Mon, 13 Apr 2026 16:03:55 +0800 Subject: [PATCH 1/6] Move WPF inference to background thread via Task.Run RunInference was blocking the UI thread, risking freeze on larger models. Fixes ADO #61791035 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Samples/WindowsML/cs-wpf/MainWindow.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Samples/WindowsML/cs-wpf/MainWindow.xaml.cs b/Samples/WindowsML/cs-wpf/MainWindow.xaml.cs index dbd720ef1..a3ceb908d 100644 --- a/Samples/WindowsML/cs-wpf/MainWindow.xaml.cs +++ b/Samples/WindowsML/cs-wpf/MainWindow.xaml.cs @@ -198,7 +198,7 @@ private async void RunInferenceButton_Click(object sender, RoutedEventArgs e) var videoFrame = await ImageProcessor.LoadImageFileAsync(_selectedImagePath); var inputTensor = await ImageProcessor.PreprocessImageAsync(videoFrame); - using var results = InferenceEngine.RunInference(_session, inputTensor); + using var results = await Task.Run(() => InferenceEngine.RunInference(_session, inputTensor)); var resultTensor = InferenceEngine.ExtractResults(_session, results); var topPredictions = ResultProcessor.GetTopPredictions(resultTensor, _labels, 5); From b4dbf242c5c78b6b3a88b5af17c0e63f1aad3035 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 08:47:26 +0800 Subject: [PATCH 2/6] Add indeterminate ProgressBar busy indicator during inference and model loading Show a visual progress bar and disable interactive buttons while async operations (inference, model reload) are in progress. Replaces the Dispatcher.Invoke render-flush workaround with proper busy-state management. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Samples/WindowsML/cs-wpf/MainWindow.xaml | 4 +++ Samples/WindowsML/cs-wpf/MainWindow.xaml.cs | 34 ++++++++++++++++++--- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/Samples/WindowsML/cs-wpf/MainWindow.xaml b/Samples/WindowsML/cs-wpf/MainWindow.xaml index 66aac58fa..37564f094 100644 --- a/Samples/WindowsML/cs-wpf/MainWindow.xaml +++ b/Samples/WindowsML/cs-wpf/MainWindow.xaml @@ -62,6 +62,10 @@ + + + diff --git a/Samples/WindowsML/cs-wpf/MainWindow.xaml.cs b/Samples/WindowsML/cs-wpf/MainWindow.xaml.cs index a3ceb908d..4d937d5c3 100644 --- a/Samples/WindowsML/cs-wpf/MainWindow.xaml.cs +++ b/Samples/WindowsML/cs-wpf/MainWindow.xaml.cs @@ -182,6 +182,14 @@ private void SelectImageButton_Click(object sender, RoutedEventArgs e) } } + private void SetBusy(bool busy) + { + BusyIndicator.Visibility = busy ? Visibility.Visible : Visibility.Collapsed; + RunInferenceButton.IsEnabled = !busy && _session != null && !string.IsNullOrEmpty(_selectedImagePath); + ReloadSessionButton.IsEnabled = !busy; + SelectImageButton.IsEnabled = !busy; + } + private async void RunInferenceButton_Click(object sender, RoutedEventArgs e) { if (string.IsNullOrEmpty(_selectedImagePath) || _session == null) @@ -192,8 +200,8 @@ private async void RunInferenceButton_Click(object sender, RoutedEventArgs e) try { + SetBusy(true); ResultsTextBox.Text = "Running inference..."; - Dispatcher.Invoke(() => { }, System.Windows.Threading.DispatcherPriority.Render); var videoFrame = await ImageProcessor.LoadImageFileAsync(_selectedImagePath); var inputTensor = await ImageProcessor.PreprocessImageAsync(videoFrame); @@ -208,6 +216,10 @@ private async void RunInferenceButton_Click(object sender, RoutedEventArgs e) { ResultsTextBox.Text = $"Error during inference: {ex.Message}"; } + finally + { + SetBusy(false); + } } private void PopulateDeviceCombo(OrtEnv env) @@ -258,11 +270,23 @@ private void PopulateDeviceCombo(OrtEnv env) private async void ReloadSessionButton_Click(object sender, RoutedEventArgs e) { - ResultsTextBox.Text = "Loading / reloading model..."; - await LoadModelAndLabelsAsync(); - if (_session != null) + try + { + SetBusy(true); + ResultsTextBox.Text = "Loading / reloading model..."; + await LoadModelAndLabelsAsync(); + if (_session != null) + { + ResultsTextBox.Text += "\nModel loaded. Select an image and click 'Run Inference'."; + } + } + catch (Exception ex) + { + ResultsTextBox.Text = $"Error loading model: {ex.Message}"; + } + finally { - ResultsTextBox.Text += "\nModel loaded. Select an image and click 'Run Inference'."; + SetBusy(false); } } From 181695a2a769d0914f8d9c5121dba3bd2d9789dd Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:08:13 +0800 Subject: [PATCH 3/6] Address Copilot review: disable all controls during busy, fix enable state, capture session local Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Samples/WindowsML/cs-wpf/MainWindow.xaml.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Samples/WindowsML/cs-wpf/MainWindow.xaml.cs b/Samples/WindowsML/cs-wpf/MainWindow.xaml.cs index 4d937d5c3..4566668f1 100644 --- a/Samples/WindowsML/cs-wpf/MainWindow.xaml.cs +++ b/Samples/WindowsML/cs-wpf/MainWindow.xaml.cs @@ -172,7 +172,7 @@ private void SelectImageButton_Click(object sender, RoutedEventArgs e) bitmap.EndInit(); SelectedImage.Source = bitmap; - RunInferenceButton.IsEnabled = true; + RunInferenceButton.IsEnabled = _session != null; ResultsTextBox.Text = "Image selected. Click 'Run Inference' to classify the image."; } catch (Exception ex) @@ -188,6 +188,9 @@ private void SetBusy(bool busy) RunInferenceButton.IsEnabled = !busy && _session != null && !string.IsNullOrEmpty(_selectedImagePath); ReloadSessionButton.IsEnabled = !busy; SelectImageButton.IsEnabled = !busy; + EpCombo.IsEnabled = !busy; + DeviceCombo.IsEnabled = !busy; + AllowProviderDownloadCheckBox.IsEnabled = !busy; } private async void RunInferenceButton_Click(object sender, RoutedEventArgs e) @@ -206,7 +209,8 @@ private async void RunInferenceButton_Click(object sender, RoutedEventArgs e) var videoFrame = await ImageProcessor.LoadImageFileAsync(_selectedImagePath); var inputTensor = await ImageProcessor.PreprocessImageAsync(videoFrame); - using var results = await Task.Run(() => InferenceEngine.RunInference(_session, inputTensor)); + var session = _session; + using var results = await Task.Run(() => InferenceEngine.RunInference(session, inputTensor)); var resultTensor = InferenceEngine.ExtractResults(_session, results); var topPredictions = ResultProcessor.GetTopPredictions(resultTensor, _labels, 5); From 2d48d1f8d0f1f746ddec0ae263cd874bd2336ec3 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:20:27 +0800 Subject: [PATCH 4/6] Address Copilot round 2: conditional status text, disable perf radios, use captured session Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Samples/WindowsML/cs-wpf/MainWindow.xaml.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Samples/WindowsML/cs-wpf/MainWindow.xaml.cs b/Samples/WindowsML/cs-wpf/MainWindow.xaml.cs index 4566668f1..e06587b50 100644 --- a/Samples/WindowsML/cs-wpf/MainWindow.xaml.cs +++ b/Samples/WindowsML/cs-wpf/MainWindow.xaml.cs @@ -173,7 +173,9 @@ private void SelectImageButton_Click(object sender, RoutedEventArgs e) SelectedImage.Source = bitmap; RunInferenceButton.IsEnabled = _session != null; - ResultsTextBox.Text = "Image selected. Click 'Run Inference' to classify the image."; + ResultsTextBox.Text = _session != null + ? "Image selected. Click 'Run Inference' to classify the image." + : "Image selected. Load or reload the model first to enable inference."; } catch (Exception ex) { @@ -191,6 +193,9 @@ private void SetBusy(bool busy) EpCombo.IsEnabled = !busy; DeviceCombo.IsEnabled = !busy; AllowProviderDownloadCheckBox.IsEnabled = !busy; + PerfModeDefaultRadio.IsEnabled = !busy; + PerfModeMaxPerfRadio.IsEnabled = !busy; + PerfModeMaxEffRadio.IsEnabled = !busy; } private async void RunInferenceButton_Click(object sender, RoutedEventArgs e) @@ -211,7 +216,7 @@ private async void RunInferenceButton_Click(object sender, RoutedEventArgs e) var session = _session; using var results = await Task.Run(() => InferenceEngine.RunInference(session, inputTensor)); - var resultTensor = InferenceEngine.ExtractResults(_session, results); + var resultTensor = InferenceEngine.ExtractResults(session, results); var topPredictions = ResultProcessor.GetTopPredictions(resultTensor, _labels, 5); ResultsTextBox.Text = FormatResultsForUI(topPredictions); From 1d931fafcfb497708d8fe39ae3daf5cdf1ab8c5e Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:32:24 +0800 Subject: [PATCH 5/6] Address Copilot round 3: save/restore combo states in SetBusy, prevent close during inference Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Samples/WindowsML/cs-wpf/MainWindow.xaml.cs | 31 +++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/Samples/WindowsML/cs-wpf/MainWindow.xaml.cs b/Samples/WindowsML/cs-wpf/MainWindow.xaml.cs index e06587b50..5189c489b 100644 --- a/Samples/WindowsML/cs-wpf/MainWindow.xaml.cs +++ b/Samples/WindowsML/cs-wpf/MainWindow.xaml.cs @@ -26,14 +26,27 @@ public partial class MainWindow : Window, IDisposable private OrtEnv? _ortEnv; private List _labels = new(); private bool _disposed; + private bool _isBusy; + private bool _deviceComboWasEnabled; + private bool _epComboWasEnabled; public MainWindow() { InitializeComponent(); // Must match x:Class in XAML Loaded += MainWindow_Loaded; + Closing += MainWindow_Closing; Closed += MainWindow_Closed; } + private void MainWindow_Closing(object? sender, System.ComponentModel.CancelEventArgs e) + { + if (_isBusy) + { + e.Cancel = true; + ResultsTextBox.Text = "Please wait for the current operation to complete before closing."; + } + } + private void MainWindow_Closed(object? sender, EventArgs e) { Dispose(); @@ -186,16 +199,30 @@ private void SelectImageButton_Click(object sender, RoutedEventArgs e) private void SetBusy(bool busy) { + _isBusy = busy; BusyIndicator.Visibility = busy ? Visibility.Visible : Visibility.Collapsed; RunInferenceButton.IsEnabled = !busy && _session != null && !string.IsNullOrEmpty(_selectedImagePath); ReloadSessionButton.IsEnabled = !busy; SelectImageButton.IsEnabled = !busy; - EpCombo.IsEnabled = !busy; - DeviceCombo.IsEnabled = !busy; AllowProviderDownloadCheckBox.IsEnabled = !busy; PerfModeDefaultRadio.IsEnabled = !busy; PerfModeMaxPerfRadio.IsEnabled = !busy; PerfModeMaxEffRadio.IsEnabled = !busy; + + if (busy) + { + // Save combo states before disabling so we can restore them later + _epComboWasEnabled = EpCombo.IsEnabled; + _deviceComboWasEnabled = DeviceCombo.IsEnabled; + EpCombo.IsEnabled = false; + DeviceCombo.IsEnabled = false; + } + else + { + // Restore the combo enabled states that PopulateDeviceCombo computed + EpCombo.IsEnabled = _epComboWasEnabled; + DeviceCombo.IsEnabled = _deviceComboWasEnabled; + } } private async void RunInferenceButton_Click(object sender, RoutedEventArgs e) From 40da52405da3ef49834fedaa0d601774d35871dc Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:47:30 +0800 Subject: [PATCH 6/6] Address Copilot round 4: dispose VideoFrame, decouple device selection from IsEnabled Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Samples/WindowsML/cs-wpf/MainWindow.xaml.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Samples/WindowsML/cs-wpf/MainWindow.xaml.cs b/Samples/WindowsML/cs-wpf/MainWindow.xaml.cs index 5189c489b..7d797fbf9 100644 --- a/Samples/WindowsML/cs-wpf/MainWindow.xaml.cs +++ b/Samples/WindowsML/cs-wpf/MainWindow.xaml.cs @@ -146,11 +146,11 @@ private async Task LoadModelAndLabelsAsync() { ModelPath = "SqueezeNet.onnx", EpName = EpCombo.SelectedItem?.ToString(), - DeviceType = (DeviceCombo.IsEnabled ? DeviceCombo.SelectedItem?.ToString() : null), + DeviceType = (DeviceCombo.Items.Count > 1 ? DeviceCombo.SelectedItem?.ToString() : null), PerfMode = GetSelectedPerformanceMode() }; - if (DeviceCombo.IsEnabled && DeviceCombo.SelectedItem == null) + if (DeviceCombo.Items.Count > 1 && DeviceCombo.SelectedItem == null) { ResultsTextBox.Text = "Select a device type for the selected execution provider."; return; @@ -238,7 +238,7 @@ private async void RunInferenceButton_Click(object sender, RoutedEventArgs e) SetBusy(true); ResultsTextBox.Text = "Running inference..."; - var videoFrame = await ImageProcessor.LoadImageFileAsync(_selectedImagePath); + using var videoFrame = await ImageProcessor.LoadImageFileAsync(_selectedImagePath); var inputTensor = await ImageProcessor.PreprocessImageAsync(videoFrame); var session = _session;