-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMainWindow.xaml.cs
More file actions
502 lines (428 loc) · 19.8 KB
/
MainWindow.xaml.cs
File metadata and controls
502 lines (428 loc) · 19.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
//////////////////////////////////////////////////////////////////////
/// MainWindow.cs
/// BatchOptimization WPF Application
///
/// MainWindow class: UI window for managing batch plan optimization and dose calculation.
/// Provides functionality to add/remove/check plans, and run batch jobs with status logging.
///
/// Features:
/// - Connects to Aria via ESAPI (VMS Treatment Planning System API)
/// - Supports dose calculation only or iterative optimization runs per plan
/// - Maintains plan list with patient/course/plan identifiers and iteration counts
/// - Updates UI efficiently with INotifyPropertyChanged on plan input rows
/// - Runs long batch processes asynchronously with UI thread yielding for responsiveness
/// - Automatically closes ESAPI warning popups in background during optimization
/// - Logs progress and results to disk and UI log view
///
/// Author: Becket Hui
/// Date: August 2025
///
/// Version History:
/// - v3.0.0.1 (2025/08) Upgrade to ESAPI version 18.1, improved async batch processing and UI responsiveness, add calculate with MU option, modify optimization to handle RAD plan type
/// - v2.0.0.1 (2024/07) Refactor using INotifyPropertyChanged and Dispatcher.Yield for smoother UI updates
/// - v1.0.1.1 (2022/09) Initial release with basic batch optimization UI and ESAPI integration
///
//////////////////////////////////////////////////////////////////////
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Threading;
using VMS.TPS.Common.Model.API;
[assembly: AssemblyVersion("3.0.0.7")]
[assembly: AssemblyFileVersion("0.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0")]
[assembly: ESAPIScript(IsWriteable = true)]
namespace batchOptimization
{
/// <summary>
/// Main window class for batch optimization UI.
/// Handles batch plan optimization and dose calc executions.
/// </summary>
public partial class MainWindow : Window
{
// Instance of your optimization manager class wrapping ESAPI logic
private EsapiPlanOptimization plnOpt = new EsapiPlanOptimization();
// Logger to write logs to file
private LogFile logger = new LogFile();
// Helper class to close warning popups automatically in background
private CloseWindow closeWarning = new CloseWindow();
// Indicates connection status to Aria database
private bool connection = false;
// Flag indicating if only dose calculation (no optimization) is requested
private bool dsCalcOnly = false;
/// <summary>
/// MainWindow constructor initializes components and connects to Aria.
/// Loads configuration and prepares UI.
/// </summary>
public MainWindow()
{
RunTask tskApp, tskLogLoc;
InitializeComponent();
// Connect to Aria application via plnOpt helper
tskApp = plnOpt.ConnectApp();
if (tskApp.success)
{
ShowMessage(tskApp.message); // Show connection message
// Setup file directories for logs and config
string fileDir = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
// Ensure Logs folder exist or create it
tskLogLoc = logger.CheckLogFolder(fileDir);
if (!tskLogLoc.success) ShowMessage(tskLogLoc.message);
// Load batch optimization parameters from config file
tskLogLoc = plnOpt.LoadParameters(Path.Combine(fileDir, "BatchOptimization.cfg"));
if (!tskLogLoc.success) ShowMessage(tskLogLoc.message);
// Initialize the DataGrid with a blank PlanInput
dataGridPlns.ItemsSource = new ObservableCollection<PlanInput> { new PlanInput() };
txtbStat.Text = "Ready.";
connection = true;
}
else
{
// If connection failed, disable functionality and show error
ShowMessage(tskApp.message, Colors.Red);
txtbStat.Text = "Failed to connect to Aria.";
connection = false;
}
}
/// <summary>
/// Adds a new plan row to the DataGrid.
/// Copies the currently selected plan if any; otherwise adds a blank row.
/// </summary>
private void btnAddPln_Click(object sender, RoutedEventArgs e)
{
PlanInput selPln = dataGridPlns.SelectedItem as PlanInput;
ObservableCollection<PlanInput> dataGridColl = dataGridPlns.ItemsSource as ObservableCollection<PlanInput>;
// If null (no plans yet), initialize the collection and bind it
if (dataGridColl == null)
{
dataGridColl = new ObservableCollection<PlanInput>();
dataGridPlns.ItemsSource = dataGridColl;
}
if (selPln != null)
{
// Copy the selected plan's properties into a new plan and add
PlanInput newPln = new PlanInput();
newPln.Pat = selPln.Pat;
newPln.Crs = selPln.Crs;
newPln.Pln = selPln.Pln;
newPln.Iter = selPln.Iter;
newPln.Stat = "⨁"; // Default status symbol
dataGridColl.Add(newPln);
}
else
{
// No selection: add a new blank plan
dataGridColl.Add(new PlanInput());
}
}
/// <summary>
/// Removes the currently selected plan row from the DataGrid.
/// </summary>
private void btnRmvPln_Click(object sender, RoutedEventArgs e)
{
if (dataGridPlns.SelectedItem != null)
{
ObservableCollection<PlanInput> dataGridColl = dataGridPlns.ItemsSource as ObservableCollection<PlanInput>;
if (dataGridColl != null)
{
dataGridColl.Remove((PlanInput)dataGridPlns.SelectedItem);
}
}
}
/// <summary>
/// Validates each plan in the DataGrid by invoking CheckPlan method.
/// Updates each plan's status and logs accordingly.
/// </summary>
private async void btnChckPln_Click(object sender, RoutedEventArgs e)
{
RunTask tskChckPln;
if (!connection)
{
txtbStat.Text = "Cannot connect to database.";
return;
}
ObservableCollection<PlanInput> dataGridColl = dataGridPlns.ItemsSource as ObservableCollection<PlanInput>;
if (dataGridColl == null || dataGridColl.Count == 0)
{
txtbStat.Text = "No plan added to list.";
return;
}
int rowNumber = 0;
ShowMessage(string.Format("Checking {0} plans.", dataGridColl.Count));
txtbStat.Text = "Checking plans.";
foreach (PlanInput inPln in dataGridColl)
{
rowNumber++;
if (inPln == null)
{
continue;
}
try
{
// Convert PlanInput to Extended plan info for ESAPI calls
ExtPlan extPln = new ExtPlan(inPln);
tskChckPln = plnOpt.CheckPlan(extPln);
// Yield UI thread so window stays responsive to input (minimize, updates, etc.)
await Dispatcher.Yield(DispatcherPriority.Background);
// Update status and message based on check result
if (tskChckPln.success)
{
ShowMessage(string.Format("For row {0}, {1}", rowNumber, tskChckPln.message));
inPln.Stat = "⨁"; // Indicate valid
}
else
{
ShowMessage(string.Format("For row {0}, {1}", rowNumber, tskChckPln.message), Colors.Red);
inPln.Stat = "✕"; // Indicate invalid
}
}
catch
{
ShowMessage(string.Format("For row {0}, input is invalid.", rowNumber), Colors.Red);
inPln.Stat = "✕";
}
}
txtbStat.Text = "Ready.";
}
/// <summary>
/// Runs batch optimization or dose calculation asynchronously.
/// Disables UI controls during processing, yields control frequently to keep UI responsive.
/// Monitors a background popup-closer thread for faults.
/// </summary>
private async void btnRunOpt_Click(object sender, RoutedEventArgs e)
{
RunTask tskOpt, tskCalcDs;
txtbStat.Text = "Running batch processing.";
dsCalcOnly = chkBxDoseCalcOnly.IsChecked.HasValue ? chkBxDoseCalcOnly.IsChecked.Value : false;
// Disable UI controls to prevent user changes mid-processing
EnableControls(false);
if (!connection)
{
txtbStat.Text = "Cannot connect to database.";
EnableControls(true);
return;
}
var cts = new CancellationTokenSource();
try
{
// Start background task that closes warning popups during ESAPI calls
Task closeWarningTask = closeWarning.CloseWarningThread(cts.Token);
// ContinueWith monitors and reports exceptions on UI thread context
_ = closeWarningTask.ContinueWith((t) =>
{
if (t.IsFaulted)
{
Exception ex = t.Exception.InnerException ?? t.Exception;
ShowMessage(string.Format("Something wrong with routine to close pop-up windows:\n{0}", ex.Message), Colors.Red);
}
}, TaskScheduler.FromCurrentSynchronizationContext());
// Start log file and write initial message
logger.StartLog(plnOpt.GetUserId());
logger.WriteLog(string.Format("Start batch processing."));
ObservableCollection<PlanInput> dataGridColl = dataGridPlns.ItemsSource as ObservableCollection<PlanInput>;
int rowNumber = 0;
foreach (PlanInput inPln in dataGridColl)
{
rowNumber++;
if (inPln == null)
{
continue;
}
try
{
ExtPlan extPln = new ExtPlan(inPln);
if (dsCalcOnly)
{
// If in Dose Calculation Only mode, just compute dose
ShowMessage(string.Format("For row {0}, plan {1}, computing dose.", rowNumber, extPln.PlanId));
logger.WriteLog(string.Format("For patient {0}, course {1}, plan {2}, start dose computation.", extPln.PatientId, extPln.CourseId, extPln.PlanId));
// Yield for responsiveness
await Dispatcher.Yield(DispatcherPriority.Background);
tskCalcDs = plnOpt.ComputeDose(extPln);
// Update results and status accordingly
if (tskCalcDs.success)
{
ShowMessage(tskCalcDs.message);
logger.WriteLog(tskCalcDs.message);
inPln.Stat = "✓";
}
else
{
ShowMessage(tskCalcDs.message, Colors.Red);
logger.WriteLog(tskCalcDs.message);
inPln.Stat = "✕";
}
}
else
{
// Optimization mode: run multiple optimization iterations per plan
ShowMessage(string.Format("For row {0}, plan {1}, start optimization.", rowNumber, extPln.PlanId));
logger.WriteLog(string.Format("For patient {0}, course {1}, plan {2}, start optimization.", extPln.PatientId, extPln.CourseId, extPln.PlanId));
for (int idxRun = 1; idxRun <= extPln.N_Runs; idxRun++)
{
ShowMessage(string.Format("Running optimization iteration no. {0}.", idxRun));
logger.WriteLog(string.Format("Start optimization run no. {0} in plan \"{1}\".", idxRun, extPln.PlanId));
// Yield for responsiveness
await Dispatcher.Yield(DispatcherPriority.Background);
tskOpt = plnOpt.Optimize(extPln, idxRun);
if (tskOpt.success)
{
logger.WriteLog(tskOpt.message);
ShowMessage(string.Format("Optimization iteration no. {0} completed, computing dose.", idxRun));
logger.WriteLog(string.Format("Start dose computation in plan \"{0}\".", extPln.PlanId));
tskCalcDs = plnOpt.ComputeDose(extPln);
if (tskCalcDs.success)
{
ShowMessage(tskCalcDs.message);
logger.WriteLog(tskCalcDs.message);
inPln.Stat = "✓";
}
else
{
ShowMessage(tskCalcDs.message, Colors.Red);
logger.WriteLog(tskCalcDs.message);
inPln.Stat = "✕";
break; // Stop further iterations on failure
}
}
else
{
ShowMessage(tskOpt.message, Colors.Red);
logger.WriteLog(tskOpt.message);
inPln.Stat = "✕";
break; // Stop further iterations on failure
}
// Yield for responsiveness
await Dispatcher.Yield(DispatcherPriority.Background);
}
}
}
catch (Exception ex)
{
// Show exception details and mark plan as failed
ShowMessage(string.Format("For row {0}, unexpected fault.", rowNumber), Colors.Red);
logger.WriteLog(string.Format("Exception in processing row {0} : {1}", rowNumber, ex.Message));
inPln.Stat = "✕";
}
// Yield between plans too for responsiveness
await Dispatcher.Yield(DispatcherPriority.Background);
}
// Finish logging and update UI
logger.WriteLog(string.Format("All batch process completed."));
ShowMessage(string.Format("All batch process completed."));
txtbStat.Text = "Ready.";
logger.EndLog();
}
finally
{
// Cancel popup-closing thread and free token source
cts.Cancel();
cts.Dispose();
// Enable controls back for user interaction
EnableControls(true);
}
}
/// <summary>
/// Enables or disables the main UI controls for plan input and batch processing.
/// </summary>
/// <param name="enable">True to enable, false to disable controls.</param>
private void EnableControls(bool enable)
{
dataGridPlns.IsReadOnly = !enable; // If enabling, DataGrid is editable; else read-only
btnAddPln.IsEnabled = enable;
btnRmvPln.IsEnabled = enable;
btnChckPln.IsEnabled = enable;
btnRunOpt.IsEnabled = enable;
chkBxDoseCalcOnly.IsEnabled = enable;
}
/// <summary>
/// Cleanly exits the ESAPI app when main window closes.
/// </summary>
private void CloseWindow(object sender, CancelEventArgs e)
{
plnOpt.Exit();
}
/// <summary>
/// Shows a message both in the ListView history and scrolls message box to bottom.
/// Safely marshals to UI thread using Dispatcher.
/// </summary>
private void ShowMessage(string msg, Color? txtColor = null)
{
Dispatcher.BeginInvoke(new Action(delegate ()
{
ListViewItem itm = new ListViewItem();
itm.Content = msg;
itm.Foreground = new SolidColorBrush(txtColor.HasValue ? txtColor.Value : Colors.Black);
lstvHst.Items.Add(itm);
// Scroll to the bottom so newest messages are visible
if (VisualTreeHelper.GetChildrenCount(lstvHst) > 0)
{
Decorator border = VisualTreeHelper.GetChild(lstvHst, 0) as Decorator;
ScrollViewer scrollViewer = border.Child as ScrollViewer;
scrollViewer.ScrollToBottom();
}
}));
}
}
/// <summary>
/// Extended plan data is used for ESAPI calls.
/// </summary>
internal class ExtPlan
{
public string PatientId;
public string CourseId;
public string PlanId;
public int N_Runs;
public ExtPlan(PlanInput pln)
{
PatientId = pln.Pat;
CourseId = pln.Crs;
PlanId = pln.Pln;
N_Runs = Math.Max(pln.Iter, 1);
}
}
/// <summary>
/// Plan input row model with change notification for UI updates.
/// </summary>
internal class PlanInput : INotifyPropertyChanged
{
private string pat = "";
public string Pat { get => pat; set { pat = value; OnPropertyChanged(nameof(Pat)); } }
private string crs = "";
public string Crs { get => crs; set { crs = value; OnPropertyChanged(nameof(Crs)); } }
private string pln = "";
public string Pln { get => pln; set { pln = value; OnPropertyChanged(nameof(Pln)); } }
private int iter = 1;
public int Iter { get => iter; set { iter = value; OnPropertyChanged(nameof(Iter)); } }
private string stat = "⨁";
public string Stat { get => stat; set { stat = value; OnPropertyChanged(nameof(Stat)); } }
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propName) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
}
/// <summary>
/// Simple class to hold result and message for each batch task.
/// </summary>
internal class RunTask
{
public bool success { get; set; }
public string message { get; set; }
public RunTask()
{
}
public RunTask(bool scs, string msg)
{
success = scs;
message = msg;
}
}
}