Form thread safety

This commit is contained in:
Michael Bucari-Tovo 2025-07-21 22:52:17 -06:00
parent 7848366818
commit 1fdcea929f
6 changed files with 87 additions and 69 deletions

View File

@ -1,5 +1,6 @@
using AudibleApi; using AudibleApi;
using AudibleUtilities; using AudibleUtilities;
using Avalonia.Threading;
using LibationUiBase.Forms; using LibationUiBase.Forms;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -17,42 +18,46 @@ namespace LibationAvalonia.Dialogs.Login
} }
public async Task<string> Get2faCodeAsync(string prompt) public async Task<string> Get2faCodeAsync(string prompt)
=> await Dispatcher.UIThread.InvokeAsync(async () =>
{ {
var dialog = new _2faCodeDialog(prompt); var dialog = new _2faCodeDialog(prompt);
if (await dialog.ShowDialogAsync() is DialogResult.OK) if (await dialog.ShowDialogAsync() is DialogResult.OK)
return dialog.Code; return dialog.Code;
return null; return null;
} });
public async Task<(string password, string guess)> GetCaptchaAnswerAsync(string password, byte[] captchaImage) public async Task<(string password, string guess)> GetCaptchaAnswerAsync(string password, byte[] captchaImage)
=> await Dispatcher.UIThread.InvokeAsync(async () =>
{ {
var dialog = new CaptchaDialog(password, captchaImage); var dialog = new CaptchaDialog(password, captchaImage);
if (await dialog.ShowDialogAsync() is DialogResult.OK) if (await dialog.ShowDialogAsync() is DialogResult.OK)
return (dialog.Password, dialog.Answer); return (dialog.Password, dialog.Answer);
return (null, null); return (null, null);
} });
public async Task<(string name, string value)> GetMfaChoiceAsync(MfaConfig mfaConfig) public async Task<(string name, string value)> GetMfaChoiceAsync(MfaConfig mfaConfig)
=> await Dispatcher.UIThread.InvokeAsync(async () =>
{ {
var dialog = new MfaDialog(mfaConfig); var dialog = new MfaDialog(mfaConfig);
if (await dialog.ShowDialogAsync() is DialogResult.OK) if (await dialog.ShowDialogAsync() is DialogResult.OK)
return (dialog.SelectedName, dialog.SelectedValue); return (dialog.SelectedName, dialog.SelectedValue);
return (null, null); return (null, null);
} });
public async Task<(string email, string password)> GetLoginAsync() public async Task<(string email, string password)> GetLoginAsync()
=> await Dispatcher.UIThread.InvokeAsync(async () =>
{ {
var dialog = new LoginCallbackDialog(_account); var dialog = new LoginCallbackDialog(_account);
if (await dialog.ShowDialogAsync() is DialogResult.OK) if (await dialog.ShowDialogAsync() is DialogResult.OK)
return (_account.AccountId, dialog.Password); return (_account.AccountId, dialog.Password);
return (null, null); return (null, null);
} });
public async Task ShowApprovalNeededAsync() public async Task ShowApprovalNeededAsync()
=> await Dispatcher.UIThread.InvokeAsync(async () =>
{ {
var dialog = new ApprovalNeededDialog(); var dialog = new ApprovalNeededDialog();
await dialog.ShowDialogAsync(); await dialog.ShowDialogAsync();
} });
} }
} }

View File

@ -1,5 +1,6 @@
using AudibleApi; using AudibleApi;
using AudibleUtilities; using AudibleUtilities;
using Avalonia.Threading;
using LibationFileManager; using LibationFileManager;
using LibationUiBase.Forms; using LibationUiBase.Forms;
using System; using System;
@ -21,6 +22,9 @@ namespace LibationAvalonia.Dialogs.Login
} }
public async Task<ChoiceOut?> StartAsync(ChoiceIn choiceIn) public async Task<ChoiceOut?> StartAsync(ChoiceIn choiceIn)
=> await Dispatcher.UIThread.InvokeAsync(() => StartAsyncInternal(choiceIn));
private async Task<ChoiceOut?> StartAsyncInternal(ChoiceIn choiceIn)
{ {
if (Configuration.IsWindows && Environment.OSVersion.Version.Major >= 10) if (Configuration.IsWindows && Environment.OSVersion.Version.Major >= 10)
{ {

View File

@ -22,7 +22,7 @@ namespace LibationAvalonia.Views
public MainWindow() public MainWindow()
{ {
DataContext = new MainVM(this); DataContext = new MainVM(this);
ApiExtended.LoginChoiceFactory = account => new Dialogs.Login.AvaloniaLoginChoiceEager(account); ApiExtended.LoginChoiceFactory = account => Dispatcher.UIThread.Invoke(() => new Dialogs.Login.AvaloniaLoginChoiceEager(account));
AudibleApiStorage.LoadError += AudibleApiStorage_LoadError; AudibleApiStorage.LoadError += AudibleApiStorage_LoadError;
InitializeComponent(); InitializeComponent();

View File

@ -5,18 +5,19 @@ namespace LibationWinForms.Dialogs.Login
{ {
public abstract class WinformLoginBase public abstract class WinformLoginBase
{ {
private readonly IWin32Window _owner; protected Control Owner { get; }
protected WinformLoginBase(IWin32Window owner) protected WinformLoginBase(Control owner)
{ {
_owner = owner; Owner = owner;
} }
/// <returns>True if ShowDialog's DialogResult == OK</returns> /// <returns>True if ShowDialog's DialogResult == OK</returns>
protected bool ShowDialog(Form dialog) protected bool ShowDialog(Form dialog)
=> Owner.Invoke(() =>
{ {
var result = dialog.ShowDialog(_owner); var result = dialog.ShowDialog(Owner);
Serilog.Log.Logger.Debug("{@DebugInfo}", new { DialogResult = result }); Serilog.Log.Logger.Debug("{@DebugInfo}", new { DialogResult = result });
return result == DialogResult.OK; return result == DialogResult.OK;
} });
} }
} }

View File

@ -13,48 +13,53 @@ namespace LibationWinForms.Login
public string DeviceName { get; } = "Libation"; public string DeviceName { get; } = "Libation";
public WinformLoginCallback(Account account, IWin32Window owner) : base(owner) public WinformLoginCallback(Account account, Control owner) : base(owner)
{ {
_account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account)); _account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account));
} }
public Task<string> Get2faCodeAsync(string prompt) public Task<string> Get2faCodeAsync(string prompt)
=> Owner.Invoke(() =>
{ {
using var dialog = new _2faCodeDialog(prompt); using var dialog = new _2faCodeDialog(prompt);
if (ShowDialog(dialog)) if (ShowDialog(dialog))
return Task.FromResult(dialog.Code); return Task.FromResult(dialog.Code);
return Task.FromResult<string>(null); return Task.FromResult<string>(null);
} });
public Task<(string password, string guess)> GetCaptchaAnswerAsync(string password, byte[] captchaImage) public Task<(string password, string guess)> GetCaptchaAnswerAsync(string password, byte[] captchaImage)
=> Owner.Invoke(() =>
{ {
using var dialog = new CaptchaDialog(password, captchaImage); using var dialog = new CaptchaDialog(password, captchaImage);
if (ShowDialog(dialog)) if (ShowDialog(dialog))
return Task.FromResult((dialog.Password, dialog.Answer)); return Task.FromResult((dialog.Password, dialog.Answer));
return Task.FromResult<(string, string)>((null, null)); return Task.FromResult<(string, string)>((null, null));
} });
public Task<(string name, string value)> GetMfaChoiceAsync(MfaConfig mfaConfig) public Task<(string name, string value)> GetMfaChoiceAsync(MfaConfig mfaConfig)
=> Owner.Invoke(() =>
{ {
using var dialog = new MfaDialog(mfaConfig); using var dialog = new MfaDialog(mfaConfig);
if (ShowDialog(dialog)) if (ShowDialog(dialog))
return Task.FromResult((dialog.SelectedName, dialog.SelectedValue)); return Task.FromResult((dialog.SelectedName, dialog.SelectedValue));
return Task.FromResult<(string, string)>((null, null)); return Task.FromResult<(string, string)>((null, null));
} });
public Task<(string email, string password)> GetLoginAsync() public Task<(string email, string password)> GetLoginAsync()
=> Owner.Invoke(() =>
{ {
using var dialog = new LoginCallbackDialog(_account); using var dialog = new LoginCallbackDialog(_account);
if (ShowDialog(dialog)) if (ShowDialog(dialog))
return Task.FromResult((dialog.Email, dialog.Password)); return Task.FromResult((dialog.Email, dialog.Password));
return Task.FromResult<(string, string)>((null, null)); return Task.FromResult<(string, string)>((null, null));
} });
public Task ShowApprovalNeededAsync() public Task ShowApprovalNeededAsync()
=> Owner.Invoke(() =>
{ {
using var dialog = new ApprovalNeededDialog(); using var dialog = new ApprovalNeededDialog();
ShowDialog(dialog); ShowDialog(dialog);
return Task.CompletedTask; return Task.CompletedTask;
} });
} }
} }

View File

@ -13,13 +13,16 @@ namespace LibationWinForms.Login
private Account _account { get; } private Account _account { get; }
public WinformLoginChoiceEager(Account account, IWin32Window owner) : base(owner) public WinformLoginChoiceEager(Account account, Control owner) : base(owner)
{ {
_account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account)); _account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account));
LoginCallback = new WinformLoginCallback(_account, owner); LoginCallback = new WinformLoginCallback(_account, owner);
} }
public Task<ChoiceOut> StartAsync(ChoiceIn choiceIn) public Task<ChoiceOut> StartAsync(ChoiceIn choiceIn)
=> Owner.Invoke(() => StartAsyncInternal(choiceIn));
private Task<ChoiceOut> StartAsyncInternal(ChoiceIn choiceIn)
{ {
if (Environment.OSVersion.Version.Major >= 10) if (Environment.OSVersion.Version.Major >= 10)
{ {