Improved cross thread execution and minor refactoring.

This commit is contained in:
Michael Bucari-Tovo 2021-08-13 09:31:19 -06:00
parent 0e7930f2b6
commit 766d427b19
4 changed files with 90 additions and 99 deletions

View File

@ -7,31 +7,12 @@ namespace LibationWinForms
public abstract class AsyncNotifyPropertyChanged : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private int InstanceThreadId { get; } = Thread.CurrentThread.ManagedThreadId;
private bool InvokeRequired => Thread.CurrentThread.ManagedThreadId != InstanceThreadId;
private SynchronizationContext SyncContext { get; } = SynchronizationContext.Current;
private CrossThreadSync<PropertyChangedEventArgs> ThreadSync { get; } = new CrossThreadSync<PropertyChangedEventArgs>();
public AsyncNotifyPropertyChanged()
=>ThreadSync.ObjectReceived += (_, args) => PropertyChanged?.Invoke(this, args);
protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
var propertyChangedArgs = new PropertyChangedEventArgs(propertyName);
if (InvokeRequired)
{
SyncContext.Post(
PostPropertyChangedCallback,
new AsyncCompletedEventArgs(null, false, propertyChangedArgs));
}
else
{
OnPropertyChanged(propertyChangedArgs);
}
}
private void PostPropertyChangedCallback(object asyncArgs)
{
var e = asyncArgs as AsyncCompletedEventArgs;
OnPropertyChanged(e.UserState as PropertyChangedEventArgs);
}
private void OnPropertyChanged(PropertyChangedEventArgs e) => PropertyChanged?.Invoke(this, e);
=> ThreadSync.Post(new PropertyChangedEventArgs(propertyName));
}
}

View File

@ -3,8 +3,6 @@ using Dinah.Core.Net.Http;
using Dinah.Core.Windows.Forms;
using FileLiberator;
using System;
using System.ComponentModel;
using System.Threading;
using System.Windows.Forms;
namespace LibationWinForms.BookLiberation.BaseForms
@ -13,15 +11,15 @@ namespace LibationWinForms.BookLiberation.BaseForms
{
protected IStreamable Streamable { get; private set; }
protected LogMe LogMe { get; private set; }
private int InstanceThreadId { get; } = Thread.CurrentThread.ManagedThreadId;
public new bool InvokeRequired => Thread.CurrentThread.ManagedThreadId != InstanceThreadId;
private SynchronizationContext SyncContext { get; }
private CrossThreadSync<Action> FormSync { get; } = new CrossThreadSync<Action>();
public LiberationBaseForm()
{
//Will be null if set outside constructor.
SyncContext = SynchronizationContext.Current;
//SynchronizationContext.Current will be null until the process contains a Form.
//If this is the first form created, it will not exist until after execution
//reaches inside the constructor. So need to reset the context here.
FormSync.ResetContext();
FormSync.ObjectReceived += (_, action) => action();
}
public void RegisterFileLiberator(IStreamable streamable, LogMe logMe = null)
@ -130,30 +128,14 @@ namespace LibationWinForms.BookLiberation.BaseForms
private void OnCompletedDispose(object sender, LibraryBook e) => this.UIThread(() => Dispose());
/// <summary>
/// If StreamingBegin is fired from a worker thread, the window will be created on
/// that UI thread. We need to make certain that we show the window on the same
/// thread that created form, otherwise the form and the window handle will be on
/// different threads, and the renderer will be on a worker thread which could cause
/// it to freeze. Form.BeginInvoke won't work until the form is created (ie. shown)
/// because control doesn't get a window handle until it is Shown.
/// If StreamingBegin is fired from a worker thread, the window will be created on that
/// worker thread. We need to make certain that we show the window on the UI thread (same
/// thread that created form), otherwise the renderer will be on a worker thread which
/// could cause it to freeze. Form.BeginInvoke won't work until the form is created
/// (ie. shown) because Control doesn't get a window handle until it is Shown.
/// </summary>
private void OnStreamingBeginShow(object sender, string beginString)
{
static void sendCallback(object asyncArgs)
{
var e = asyncArgs as AsyncCompletedEventArgs;
((Action)e.UserState)();
}
private void OnStreamingBeginShow(object sender, string beginString) => FormSync.Send(Show);
Action show = Show;
if (InvokeRequired)
SyncContext.Send(
sendCallback,
new AsyncCompletedEventArgs(null, false, show));
else
show();
}
#endregion
#region IStreamable event handlers

View File

@ -52,7 +52,7 @@ namespace LibationWinForms.BookLiberation
{
public static async Task BackupSingleBookAsync(LibraryBook libraryBook, EventHandler<LibraryBook> completedAction = null)
{
Serilog.Log.Logger.Information("Begin backup single {@DebugInfo}", new { libraryBook?.Book?.AudibleProductId });
Serilog.Log.Logger.Information($"Begin {nameof(BackupSingleBookAsync)} {{@DebugInfo}}", new { libraryBook?.Book?.AudibleProductId });
var logMe = LogMe.RegisterForm();
var backupBook = CreateBackupBook(completedAction, logMe);
@ -96,21 +96,6 @@ namespace LibationWinForms.BookLiberation
await new BackupLoop(logMe, downloadPdf, automatedBackupsForm).RunBackupAsync();
}
public static void DownloadFile(string url, string destination, bool showDownloadCompletedDialog = false)
{
Serilog.Log.Logger.Information($"Begin {nameof(DownloadFile)} for {url}");
void onDownloadFileStreamingCompleted(object o, string s)
{
if (showDownloadCompletedDialog)
MessageBox.Show("File downloaded to:\r\n\r\n" + s);
}
var downloadFile = CreateStreamable<DownloadFile, DownloadForm>(onDownloadFileStreamingCompleted);
async void runDownload() => await downloadFile.PerformDownloadFileAsync(url, destination);
new Task(runDownload).Start();
}
private static IProcessable CreateBackupBook(EventHandler<LibraryBook> completedAction, LogMe logMe)
{
var downloadPdf = CreateProcessable<DownloadPdf, PdfDownloadForm>(logMe);
@ -126,19 +111,40 @@ namespace LibationWinForms.BookLiberation
return downloadDecryptBook;
}
public static void DownloadFile(string url, string destination, bool showDownloadCompletedDialog = false)
{
Serilog.Log.Logger.Information($"Begin {nameof(DownloadFile)} for {url}");
void onDownloadFileStreamingCompleted(object sender, string savedFile)
{
Serilog.Log.Logger.Information($"Completed {nameof(DownloadFile)} for {url}. Saved to {savedFile}");
if (showDownloadCompletedDialog)
MessageBox.Show($"File downloaded to:{Environment.NewLine}{Environment.NewLine}{savedFile}");
}
var downloadFile = new DownloadFile();
var downloadForm = new DownloadForm();
downloadForm.RegisterFileLiberator(downloadFile);
downloadFile.StreamingCompleted += onDownloadFileStreamingCompleted;
async void runDownload() => await downloadFile.PerformDownloadFileAsync(url, destination);
new Task(runDownload).Start();
}
/// <summary>
/// Create a new <see cref="IProcessable"/> and links it to a new <see cref="LiberationBaseForm"/>.
/// </summary>
/// <typeparam name="TStrProc">The <see cref="IProcessable"/> derrived type to create.</typeparam>
/// <typeparam name="TProcessable">The <see cref="IProcessable"/> derrived type to create.</typeparam>
/// <typeparam name="TForm">The <see cref="LiberationBaseForm"/> derrived Form to create on <see cref="IProcessable.Begin"/>, Show on <see cref="IStreamable.StreamingBegin"/>, Close on <see cref="IStreamable.StreamingCompleted"/>, and Dispose on <see cref="IProcessable.Completed"/> </typeparam>
/// <param name="logMe">The logger</param>
/// <param name="completedAction">An additional event handler to handle <see cref="IProcessable.Completed"/></param>
/// <returns>A new <see cref="IProcessable"/> of type <typeparamref name="TStrProc"/></returns>
private static TStrProc CreateProcessable<TStrProc, TForm>(LogMe logMe, EventHandler<LibraryBook> completedAction = null)
private static TProcessable CreateProcessable<TProcessable, TForm>(LogMe logMe, EventHandler<LibraryBook> completedAction = null)
where TForm : LiberationBaseForm, new()
where TStrProc : IProcessable, new()
where TProcessable : IProcessable, new()
{
var strProc = new TStrProc();
var strProc = new TProcessable();
strProc.Begin += (sender, libraryBook) =>
{
@ -147,33 +153,10 @@ namespace LibationWinForms.BookLiberation
processForm.OnBegin(sender, libraryBook);
};
if (completedAction != null)
strProc.Completed += completedAction;
return strProc;
}
/// <summary>
/// Creates a new <see cref="IStreamable"/> and links it to a new <see cref="LiberationBaseForm"/>
/// </summary>
/// <typeparam name="TStr">The <see cref="IStreamable"/> derrived type to create.</typeparam>
/// <typeparam name="TForm">The <see cref="LiberationBaseForm"/> derrived Form to create, which will Show on <see cref="IStreamable.StreamingBegin"/> and Close, Dispose on <see cref="IStreamable.StreamingCompleted"/>.</typeparam>
/// <param name="completedAction">An additional event handler to handle <see cref="IStreamable.StreamingCompleted"/></param>
/// <returns>A new <see cref="IStreamable"/> of type <typeparamref name="TStr"/></returns>
private static TStr CreateStreamable<TStr, TForm>(EventHandler<string> completedAction = null)
where TForm : LiberationBaseForm, new()
where TStr : IStreamable, new()
{
var streamable = new TStr();
var streamForm = new TForm();
streamForm.RegisterFileLiberator(streamable);
if (completedAction != null)
streamable.StreamingCompleted += completedAction;
return streamable;
}
}
internal abstract class BackupRunner

View File

@ -0,0 +1,45 @@
using System;
using System.ComponentModel;
using System.Threading;
namespace LibationWinForms
{
internal class CrossThreadSync<T>
{
public event EventHandler<T> ObjectReceived;
private int InstanceThreadId { get; set; } = Thread.CurrentThread.ManagedThreadId;
private SynchronizationContext SyncContext { get; set; } = SynchronizationContext.Current;
private bool InvokeRequired => Thread.CurrentThread.ManagedThreadId != InstanceThreadId;
public void ResetContext()
{
SyncContext = SynchronizationContext.Current;
InstanceThreadId = Thread.CurrentThread.ManagedThreadId;
}
public void Send(T obj)
{
if (InvokeRequired)
SyncContext.Send(SendOrPostCallback, new AsyncCompletedEventArgs(null, false, obj));
else
ObjectReceived?.Invoke(this, obj);
}
public void Post(T obj)
{
if (InvokeRequired)
SyncContext.Post(SendOrPostCallback, new AsyncCompletedEventArgs(null, false, obj));
else
ObjectReceived?.Invoke(this, obj);
}
private void SendOrPostCallback(object asyncArgs)
{
var e = asyncArgs as AsyncCompletedEventArgs;
var userObject = (T)e.UserState;
ObjectReceived?.Invoke(this, userObject);
}
}
}