Refactor Classic process queue

The queue is now more MVVM-like.
This commit is contained in:
MBucari 2025-07-16 22:58:03 -06:00
parent 7e79e98771
commit 747451d243
5 changed files with 108 additions and 211 deletions

View File

@ -22,6 +22,49 @@ namespace LibationWinForms.ProcessQueue
public static Color QueuedColor = SystemColors.Control; public static Color QueuedColor = SystemColors.Control;
public static Color SuccessColor = Color.PaleGreen; public static Color SuccessColor = Color.PaleGreen;
private ProcessBookViewModelBase m_Context;
public ProcessBookViewModelBase Context
{
get => m_Context;
set
{
if (m_Context != value)
{
OnContextChanging();
m_Context = value;
OnContextChanged();
}
}
}
private void OnContextChanging()
{
if (Context is not null)
Context.PropertyChanged -= Context_PropertyChanged;
}
private void OnContextChanged()
{
Context.PropertyChanged += Context_PropertyChanged;
Context_PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(null));
}
private void Context_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
SuspendLayout();
if (e.PropertyName is null or nameof(Context.Cover))
SetCover(Context.Cover as Image);
if (e.PropertyName is null or nameof(Context.Title) or nameof(Context.Author) or nameof(Context.Narrator))
SetBookInfo($"{Context.Title}\r\nBy {Context.Author}\r\nNarrated by {Context.Narrator}");
if (e.PropertyName is null or nameof(Context.Status) or nameof(Context.StatusText))
SetStatus(Context.Status, Context.StatusText);
if (e.PropertyName is null or nameof(Context.Progress))
SetProgress(Context.Progress);
if (e.PropertyName is null or nameof(Context.TimeRemaining))
SetRemainingTime(Context.TimeRemaining);
ResumeLayout();
}
public ProcessBookControl() public ProcessBookControl()
{ {
InitializeComponent(); InitializeComponent();

View File

@ -162,7 +162,6 @@
this.virtualFlowControl2.Name = "virtualFlowControl2"; this.virtualFlowControl2.Name = "virtualFlowControl2";
this.virtualFlowControl2.Size = new System.Drawing.Size(390, 424); this.virtualFlowControl2.Size = new System.Drawing.Size(390, 424);
this.virtualFlowControl2.TabIndex = 3; this.virtualFlowControl2.TabIndex = 3;
this.virtualFlowControl2.VirtualControlCount = 0;
// //
// panel1 // panel1
// //

View File

@ -1,9 +1,6 @@
using LibationFileManager; using LibationFileManager;
using LibationUiBase; using LibationUiBase;
using LibationUiBase.ProcessQueue;
using System; using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Drawing; using System.Drawing;
@ -34,12 +31,11 @@ internal partial class ProcessQueueControl : UserControl
numericUpDown1.Value = speedLimitMBps > numericUpDown1.Maximum || speedLimitMBps < numericUpDown1.Minimum ? 0 : speedLimitMBps; numericUpDown1.Value = speedLimitMBps > numericUpDown1.Maximum || speedLimitMBps < numericUpDown1.Minimum ? 0 : speedLimitMBps;
statusStrip1.Items.Add(PopoutButton); statusStrip1.Items.Add(PopoutButton);
virtualFlowControl2.RequestData += VirtualFlowControl1_RequestData;
virtualFlowControl2.ButtonClicked += VirtualFlowControl2_ButtonClicked; virtualFlowControl2.ButtonClicked += VirtualFlowControl2_ButtonClicked;
ViewModel.LogWritten += (_, text) => WriteLine(text); ViewModel.LogWritten += (_, text) => WriteLine(text);
ViewModel.PropertyChanged += ProcessQueue_PropertyChanged; ViewModel.PropertyChanged += ProcessQueue_PropertyChanged;
ViewModel.BookPropertyChanged += ProcessBook_PropertyChanged; virtualFlowControl2.Items = ViewModel.Items;
Load += ProcessQueueControl_Load; Load += ProcessQueueControl_Load;
} }
@ -60,15 +56,13 @@ internal partial class ProcessQueueControl : UserControl
ViewModel.Queue.ClearQueue(); ViewModel.Queue.ClearQueue();
if (ViewModel.Queue.Current is not null) if (ViewModel.Queue.Current is not null)
await ViewModel.Queue.Current.CancelAsync(); await ViewModel.Queue.Current.CancelAsync();
virtualFlowControl2.VirtualControlCount = ViewModel.Queue.Count; virtualFlowControl2.RefreshDisplay();
UpdateAllControls();
} }
private void btnClearFinished_Click(object? sender, EventArgs e) private void btnClearFinished_Click(object? sender, EventArgs e)
{ {
ViewModel.Queue.ClearCompleted(); ViewModel.Queue.ClearCompleted();
virtualFlowControl2.VirtualControlCount = ViewModel.Queue.Count; virtualFlowControl2.RefreshDisplay();
UpdateAllControls();
if (!ViewModel.Running) if (!ViewModel.Running)
runningTimeLbl.Text = string.Empty; runningTimeLbl.Text = string.Empty;
@ -92,22 +86,13 @@ internal partial class ProcessQueueControl : UserControl
#region View-Model update event handling #region View-Model update event handling
private void ProcessBook_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (sender is not ProcessBookViewModel pbvm)
return;
int index = ViewModel.Queue.IndexOf(pbvm);
UpdateControl(index, e.PropertyName);
}
private void ProcessQueue_PropertyChanged(object? sender, PropertyChangedEventArgs e) private void ProcessQueue_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{ {
if (e.PropertyName is null or nameof(ViewModel.QueuedCount)) if (e.PropertyName is null or nameof(ViewModel.QueuedCount))
{ {
queueNumberLbl.Text = ViewModel.QueuedCount.ToString(); queueNumberLbl.Text = ViewModel.QueuedCount.ToString();
queueNumberLbl.Visible = ViewModel.QueuedCount > 0; queueNumberLbl.Visible = ViewModel.QueuedCount > 0;
virtualFlowControl2.VirtualControlCount = ViewModel.Queue.Count; virtualFlowControl2.RefreshDisplay();
} }
if (e.PropertyName is null or nameof(ViewModel.ErrorCount)) if (e.PropertyName is null or nameof(ViewModel.ErrorCount))
{ {
@ -134,107 +119,41 @@ internal partial class ProcessQueueControl : UserControl
} }
} }
/// <summary>
/// Index of the first <see cref="ProcessBookViewModel"/> visible in the <see cref="VirtualFlowControl"/>
/// </summary>
private int FirstVisible = 0;
/// <summary>
/// Number of <see cref="ProcessBookViewModel"/> visible in the <see cref="VirtualFlowControl"/>
/// </summary>
private int NumVisible = 0;
/// <summary>
/// Controls displaying the <see cref="ProcessBookViewModel"/> state, starting with <see cref="FirstVisible"/>
/// </summary>
private IReadOnlyList<ProcessBookControl>? Panels;
/// <summary>
/// Updates the display of a single <see cref="ProcessBookControl"/> at <paramref name="queueIndex"/> within <see cref="Queue"/>
/// </summary>
/// <param name="queueIndex">index of the <see cref="ProcessBookViewModel"/> within the <see cref="Queue"/></param>
/// <param name="propertyName">The nme of the property that needs updating. If null, all properties are updated.</param>
private void UpdateControl(int queueIndex, string? propertyName = null)
{
try
{
int i = queueIndex - FirstVisible;
if (Panels is null || i > NumVisible || i < 0) return;
var proc = ViewModel.Queue[queueIndex];
Invoke(() =>
{
Panels[i].SuspendLayout();
if (propertyName is null or nameof(proc.Cover))
Panels[i].SetCover(proc.Cover as Image);
if (propertyName is null or nameof(proc.Title) or nameof(proc.Author) or nameof(proc.Narrator))
Panels[i].SetBookInfo($"{proc.Title}\r\nBy {proc.Author}\r\nNarrated by {proc.Narrator}");
if (propertyName is null or nameof(proc.Status) or nameof(proc.StatusText))
Panels[i].SetStatus(proc.Status, proc.StatusText);
if (propertyName is null or nameof(proc.Progress))
Panels[i].SetProgress(proc.Progress);
if (propertyName is null or nameof(proc.TimeRemaining))
Panels[i].SetRemainingTime(proc.TimeRemaining);
Panels[i].ResumeLayout();
});
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error updating the queued item's display.");
}
}
private void UpdateAllControls()
{
int numToShow = Math.Min(NumVisible, ViewModel.Queue.Count - FirstVisible);
for (int i = 0; i < numToShow; i++)
UpdateControl(FirstVisible + i);
}
/// <summary> /// <summary>
/// View notified the model that a botton was clicked /// View notified the model that a botton was clicked
/// </summary> /// </summary>
/// <param name="queueIndex">index of the <see cref="ProcessBookViewModel"/> within <see cref="Queue"/></param> /// <param name="sender">the <see cref="ProcessBookControl"/> whose button was clicked</param>
/// <param name="panelClicked">The clicked control to update</param> /// <param name="buttonName">The name of the button clicked</param>
private async void VirtualFlowControl2_ButtonClicked(int queueIndex, string buttonName, ProcessBookControl panelClicked) private async void VirtualFlowControl2_ButtonClicked(object? sender, string buttonName)
{ {
if (sender is not ProcessBookControl control || control.Context is not ProcessBookViewModel item)
return;
try try
{ {
var item = ViewModel.Queue[queueIndex]; if (buttonName is nameof(ProcessBookControl.cancelBtn))
if (buttonName == nameof(panelClicked.cancelBtn))
{ {
if (item is not null) await item.CancelAsync();
ViewModel.Queue.RemoveQueued(item);
virtualFlowControl2.RefreshDisplay();
}
else
{
QueuePosition? position = buttonName switch
{ {
await item.CancelAsync(); nameof(ProcessBookControl.moveFirstBtn) => QueuePosition.Fisrt,
if (ViewModel.Queue.RemoveQueued(item)) nameof(ProcessBookControl.moveUpBtn) => QueuePosition.OneUp,
virtualFlowControl2.VirtualControlCount = ViewModel.Queue.Count; nameof(ProcessBookControl.moveDownBtn) => QueuePosition.OneDown,
nameof(ProcessBookControl.moveLastBtn) => QueuePosition.Last,
_ => null
};
if (position is not null)
{
ViewModel.Queue.MoveQueuePosition(item, position.Value);
virtualFlowControl2.RefreshDisplay();
} }
} }
else if (buttonName == nameof(panelClicked.moveFirstBtn))
{
ViewModel.Queue.MoveQueuePosition(item, QueuePosition.Fisrt);
UpdateAllControls();
}
else if (buttonName == nameof(panelClicked.moveUpBtn))
{
ViewModel.Queue.MoveQueuePosition(item, QueuePosition.OneUp);
UpdateControl(queueIndex);
if (queueIndex > 0)
UpdateControl(queueIndex - 1);
}
else if (buttonName == nameof(panelClicked.moveDownBtn))
{
ViewModel.Queue.MoveQueuePosition(item, QueuePosition.OneDown);
UpdateControl(queueIndex);
if (queueIndex + 1 < ViewModel.Queue.Count)
UpdateControl(queueIndex + 1);
}
else if (buttonName == nameof(panelClicked.moveLastBtn))
{
ViewModel.Queue.MoveQueuePosition(item, QueuePosition.Last);
UpdateAllControls();
}
} }
catch(Exception ex) catch(Exception ex)
{ {
@ -242,17 +161,6 @@ internal partial class ProcessQueueControl : UserControl
} }
} }
/// <summary>
/// View needs updating
/// </summary>
private void VirtualFlowControl1_RequestData(int firstIndex, int numVisible, IReadOnlyList<ProcessBookControl> panelsToFill)
{
FirstVisible = firstIndex;
NumVisible = numVisible;
Panels = panelsToFill;
UpdateAllControls();
}
#endregion #endregion
private void numericUpDown1_ValueChanged(object? sender, EventArgs e) private void numericUpDown1_ValueChanged(object? sender, EventArgs e)

View File

@ -1,11 +1,7 @@
using DataLayer; using DataLayer;
using LibationUiBase.ProcessQueue; using LibationUiBase.ProcessQueue;
using System; using System;
using System.Collections; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
#nullable enable #nullable enable
namespace LibationWinForms.ProcessQueue; namespace LibationWinForms.ProcessQueue;
@ -13,62 +9,16 @@ namespace LibationWinForms.ProcessQueue;
internal class ProcessQueueViewModel : ProcessQueueViewModelBase internal class ProcessQueueViewModel : ProcessQueueViewModelBase
{ {
public event EventHandler<string>? LogWritten; public event EventHandler<string>? LogWritten;
/// <summary> public List<ProcessBookViewModelBase> Items { get; }
/// Fires when a ProcessBookViewModelBase in the queue has a property changed
/// </summary>
public event EventHandler<PropertyChangedEventArgs>? BookPropertyChanged;
private ObservableCollection<ProcessBookViewModelBase> Items { get; }
public ProcessQueueViewModel() : base(CreateEmptyList()) public ProcessQueueViewModel() : base(new List<ProcessBookViewModelBase>())
{ {
Items = Queue.UnderlyingList as ObservableCollection<ProcessBookViewModelBase> Items = Queue.UnderlyingList as List<ProcessBookViewModelBase>
?? throw new ArgumentNullException(nameof(Queue.UnderlyingList)); ?? throw new ArgumentNullException(nameof(Queue.UnderlyingList));
Items.CollectionChanged += Items_CollectionChanged;
} }
private void Items_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
subscribe(e.NewItems);
break;
case NotifyCollectionChangedAction.Remove:
unubscribe(e.OldItems);
break;
}
void subscribe(IList? items)
{
foreach (var item in e.NewItems?.OfType<ProcessBookViewModel>() ?? [])
item.PropertyChanged += Item_PropertyChanged;
}
void unubscribe(IList? items)
{
foreach (var item in e.NewItems?.OfType<ProcessBookViewModel>() ?? [])
item.PropertyChanged -= Item_PropertyChanged;
}
}
private void Item_PropertyChanged(object? sender, PropertyChangedEventArgs e)
=> BookPropertyChanged?.Invoke(sender, e);
public override void WriteLine(string text) => Invoke(() => LogWritten?.Invoke(this, text.Trim())); public override void WriteLine(string text) => Invoke(() => LogWritten?.Invoke(this, text.Trim()));
protected override ProcessBookViewModelBase CreateNewProcessBook(LibraryBook libraryBook) protected override ProcessBookViewModelBase CreateNewProcessBook(LibraryBook libraryBook)
=> new ProcessBookViewModel(libraryBook, Logger); => new ProcessBookViewModel(libraryBook, Logger);
private static ObservableCollection<ProcessBookViewModelBase> CreateEmptyList()
=> new ProcessBookCollection();
private class ProcessBookCollection : ObservableCollection<ProcessBookViewModelBase>
{
protected override void ClearItems()
{
//ObservableCollection doesn't raise Remove for each item on Clear, so we need to do it ourselves.
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, this));
base.ClearItems();
}
}
} }

View File

@ -1,44 +1,42 @@
using System; using LibationUiBase.ProcessQueue;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing; using System.Drawing;
using System.Windows.Forms; using System.Windows.Forms;
namespace LibationWinForms.ProcessQueue namespace LibationWinForms.ProcessQueue
{ {
internal delegate void RequestDataDelegate(int queueIndex, int numVisible, IReadOnlyList<ProcessBookControl> panelsToFill);
internal delegate void ControlButtonClickedDelegate(int queueIndex, string buttonName, ProcessBookControl panelClicked);
internal partial class VirtualFlowControl : UserControl internal partial class VirtualFlowControl : UserControl
{ {
/// <summary>
/// Triggered when the <see cref="VirtualFlowControl"/> needs to update the displayed <see cref="ProcessBookControl"/>s
/// </summary>
public event RequestDataDelegate RequestData;
/// <summary> /// <summary>
/// Triggered when one of the <see cref="ProcessBookControl"/>'s buttons has been clicked /// Triggered when one of the <see cref="ProcessBookControl"/>'s buttons has been clicked
/// </summary> /// </summary>
public event ControlButtonClickedDelegate ButtonClicked; public event EventHandler<string> ButtonClicked;
private List<ProcessBookViewModelBase> m_Items;
public List<ProcessBookViewModelBase> Items
{
get => m_Items;
set
{
m_Items = value;
if (m_Items is not null)
RefreshDisplay();
}
}
public void RefreshDisplay()
{
AdjustScrollBar();
DoVirtualScroll();
}
#region Dynamic Properties #region Dynamic Properties
/// <summary> /// <summary>
/// The number of virtual <see cref="ProcessBookControl"/>s in the <see cref="VirtualFlowControl"/> /// The number of virtual <see cref="ProcessBookControl"/>s in the <see cref="VirtualFlowControl"/>
/// </summary> /// </summary>
public int VirtualControlCount public int VirtualControlCount => Items?.Count ?? 0;
{
get => _virtualControlCount;
set
{
if (_virtualControlCount == 0)
vScrollBar1.Value = 0;
_virtualControlCount = value;
AdjustScrollBar();
DoVirtualScroll();
}
}
private int _virtualControlCount;
int ScrollValue => Math.Max(vScrollBar1.Value, 0); int ScrollValue => Math.Max(vScrollBar1.Value, 0);
/// <summary> /// <summary>
@ -100,12 +98,7 @@ namespace LibationWinForms.ProcessQueue
{ {
InitializeComponent(); InitializeComponent();
panel1.Resize += (_, _) => panel1.Resize += (_, _) => RefreshDisplay();
{
AdjustScrollBar();
DoVirtualScroll();
};
var control = InitControl(0); var control = InitControl(0);
VirtualControlHeight = this.DpiUnscale(control.Height + control.Margin.Top + control.Margin.Bottom); VirtualControlHeight = this.DpiUnscale(control.Height + control.Margin.Top + control.Margin.Bottom);
@ -151,9 +144,7 @@ namespace LibationWinForms.ProcessQueue
while (form is not ProcessBookControl) while (form is not ProcessBookControl)
form = form.Parent; form = form.Parent;
int clickedIndex = BookControls.IndexOf((ProcessBookControl)form); ButtonClicked?.Invoke(form, button.Name);
ButtonClicked?.Invoke(FirstVisibleVirtualIndex + clickedIndex, button.Name, BookControls[clickedIndex]);
} }
/// <summary> /// <summary>
@ -186,7 +177,8 @@ namespace LibationWinForms.ProcessQueue
/// <summary> /// <summary>
/// Calculated the virtual controls that are in view at the currrent scroll position and windows size, /// Calculated the virtual controls that are in view at the currrent scroll position and windows size,
/// positions <see cref="panel1"/> to simulate scroll activity, then fires <see cref="RequestData"/> to notify the model to update all visible controls /// positions <see cref="panel1"/> to simulate scroll activity, then fires updates the controls with
/// the context corresponding to the virtual scroll position
/// </summary> /// </summary>
private void DoVirtualScroll() private void DoVirtualScroll()
{ {
@ -203,10 +195,15 @@ namespace LibationWinForms.ProcessQueue
numVisible = Math.Min(numVisible, VirtualControlCount); numVisible = Math.Min(numVisible, VirtualControlCount);
numVisible = Math.Min(numVisible, VirtualControlCount - firstVisible); numVisible = Math.Min(numVisible, VirtualControlCount - firstVisible);
RequestData?.Invoke(firstVisible, numVisible, BookControls); for (int i = 0; i < numVisible; i++)
{
BookControls[i].Context = Items[firstVisible + i];
}
for (int i = 0; i < BookControls.Count; i++) for (int i = 0; i < BookControls.Count; i++)
{
BookControls[i].Visible = i < numVisible; BookControls[i].Visible = i < numVisible;
}
} }
/// <summary> /// <summary>