Improved cross thread execution and minor refactoring.
This commit is contained in:
parent
0e7930f2b6
commit
766d427b19
@ -7,31 +7,12 @@ namespace LibationWinForms
|
|||||||
public abstract class AsyncNotifyPropertyChanged : INotifyPropertyChanged
|
public abstract class AsyncNotifyPropertyChanged : INotifyPropertyChanged
|
||||||
{
|
{
|
||||||
public event PropertyChangedEventHandler PropertyChanged;
|
public event PropertyChangedEventHandler PropertyChanged;
|
||||||
private int InstanceThreadId { get; } = Thread.CurrentThread.ManagedThreadId;
|
private CrossThreadSync<PropertyChangedEventArgs> ThreadSync { get; } = new CrossThreadSync<PropertyChangedEventArgs>();
|
||||||
private bool InvokeRequired => Thread.CurrentThread.ManagedThreadId != InstanceThreadId;
|
|
||||||
private SynchronizationContext SyncContext { get; } = SynchronizationContext.Current;
|
public AsyncNotifyPropertyChanged()
|
||||||
|
=>ThreadSync.ObjectReceived += (_, args) => PropertyChanged?.Invoke(this, args);
|
||||||
|
|
||||||
protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
|
protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
|
||||||
{
|
=> ThreadSync.Post(new PropertyChangedEventArgs(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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,8 +3,6 @@ using Dinah.Core.Net.Http;
|
|||||||
using Dinah.Core.Windows.Forms;
|
using Dinah.Core.Windows.Forms;
|
||||||
using FileLiberator;
|
using FileLiberator;
|
||||||
using System;
|
using System;
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
|
|
||||||
namespace LibationWinForms.BookLiberation.BaseForms
|
namespace LibationWinForms.BookLiberation.BaseForms
|
||||||
@ -13,15 +11,15 @@ namespace LibationWinForms.BookLiberation.BaseForms
|
|||||||
{
|
{
|
||||||
protected IStreamable Streamable { get; private set; }
|
protected IStreamable Streamable { get; private set; }
|
||||||
protected LogMe LogMe { get; private set; }
|
protected LogMe LogMe { get; private set; }
|
||||||
|
private CrossThreadSync<Action> FormSync { get; } = new CrossThreadSync<Action>();
|
||||||
private int InstanceThreadId { get; } = Thread.CurrentThread.ManagedThreadId;
|
|
||||||
public new bool InvokeRequired => Thread.CurrentThread.ManagedThreadId != InstanceThreadId;
|
|
||||||
private SynchronizationContext SyncContext { get; }
|
|
||||||
|
|
||||||
public LiberationBaseForm()
|
public LiberationBaseForm()
|
||||||
{
|
{
|
||||||
//Will be null if set outside constructor.
|
//SynchronizationContext.Current will be null until the process contains a Form.
|
||||||
SyncContext = SynchronizationContext.Current;
|
//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)
|
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());
|
private void OnCompletedDispose(object sender, LibraryBook e) => this.UIThread(() => Dispose());
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If StreamingBegin is fired from a worker thread, the window will be created on
|
/// If StreamingBegin is fired from a worker thread, the window will be created on that
|
||||||
/// that UI thread. We need to make certain that we show the window on the same
|
/// worker thread. We need to make certain that we show the window on the UI thread (same
|
||||||
/// thread that created form, otherwise the form and the window handle will be on
|
/// thread that created form), otherwise the renderer will be on a worker thread which
|
||||||
/// different threads, and the renderer will be on a worker thread which could cause
|
/// could cause it to freeze. Form.BeginInvoke won't work until the form is created
|
||||||
/// it to freeze. Form.BeginInvoke won't work until the form is created (ie. shown)
|
/// (ie. shown) because Control doesn't get a window handle until it is Shown.
|
||||||
/// because control doesn't get a window handle until it is Shown.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void OnStreamingBeginShow(object sender, string beginString)
|
private void OnStreamingBeginShow(object sender, string beginString) => FormSync.Send(Show);
|
||||||
{
|
|
||||||
static void sendCallback(object asyncArgs)
|
|
||||||
{
|
|
||||||
var e = asyncArgs as AsyncCompletedEventArgs;
|
|
||||||
((Action)e.UserState)();
|
|
||||||
}
|
|
||||||
|
|
||||||
Action show = Show;
|
|
||||||
|
|
||||||
if (InvokeRequired)
|
|
||||||
SyncContext.Send(
|
|
||||||
sendCallback,
|
|
||||||
new AsyncCompletedEventArgs(null, false, show));
|
|
||||||
else
|
|
||||||
show();
|
|
||||||
}
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region IStreamable event handlers
|
#region IStreamable event handlers
|
||||||
|
|||||||
@ -52,7 +52,7 @@ namespace LibationWinForms.BookLiberation
|
|||||||
{
|
{
|
||||||
public static async Task BackupSingleBookAsync(LibraryBook libraryBook, EventHandler<LibraryBook> completedAction = null)
|
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 logMe = LogMe.RegisterForm();
|
||||||
var backupBook = CreateBackupBook(completedAction, logMe);
|
var backupBook = CreateBackupBook(completedAction, logMe);
|
||||||
@ -96,21 +96,6 @@ namespace LibationWinForms.BookLiberation
|
|||||||
await new BackupLoop(logMe, downloadPdf, automatedBackupsForm).RunBackupAsync();
|
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)
|
private static IProcessable CreateBackupBook(EventHandler<LibraryBook> completedAction, LogMe logMe)
|
||||||
{
|
{
|
||||||
var downloadPdf = CreateProcessable<DownloadPdf, PdfDownloadForm>(logMe);
|
var downloadPdf = CreateProcessable<DownloadPdf, PdfDownloadForm>(logMe);
|
||||||
@ -126,19 +111,40 @@ namespace LibationWinForms.BookLiberation
|
|||||||
return downloadDecryptBook;
|
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>
|
/// <summary>
|
||||||
/// Create a new <see cref="IProcessable"/> and links it to a new <see cref="LiberationBaseForm"/>.
|
/// Create a new <see cref="IProcessable"/> and links it to a new <see cref="LiberationBaseForm"/>.
|
||||||
/// </summary>
|
/// </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>
|
/// <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="logMe">The logger</param>
|
||||||
/// <param name="completedAction">An additional event handler to handle <see cref="IProcessable.Completed"/></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>
|
/// <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 TForm : LiberationBaseForm, new()
|
||||||
where TStrProc : IProcessable, new()
|
where TProcessable : IProcessable, new()
|
||||||
{
|
{
|
||||||
var strProc = new TStrProc();
|
var strProc = new TProcessable();
|
||||||
|
|
||||||
strProc.Begin += (sender, libraryBook) =>
|
strProc.Begin += (sender, libraryBook) =>
|
||||||
{
|
{
|
||||||
@ -147,33 +153,10 @@ namespace LibationWinForms.BookLiberation
|
|||||||
processForm.OnBegin(sender, libraryBook);
|
processForm.OnBegin(sender, libraryBook);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (completedAction != null)
|
strProc.Completed += completedAction;
|
||||||
strProc.Completed += completedAction;
|
|
||||||
|
|
||||||
return strProc;
|
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
|
internal abstract class BackupRunner
|
||||||
|
|||||||
45
LibationWinForms/CrossThreadSync[T].cs
Normal file
45
LibationWinForms/CrossThreadSync[T].cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user