diff --git a/LibationWinForms/AsyncNotifyPropertyChanged.cs b/LibationWinForms/AsyncNotifyPropertyChanged.cs index 1c8c8775..d9511f2d 100644 --- a/LibationWinForms/AsyncNotifyPropertyChanged.cs +++ b/LibationWinForms/AsyncNotifyPropertyChanged.cs @@ -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 ThreadSync { get; } = new CrossThreadSync(); + + 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)); } } diff --git a/LibationWinForms/BookLiberation/BaseForms/LiberationBaseForm.cs b/LibationWinForms/BookLiberation/BaseForms/LiberationBaseForm.cs index 21618888..e978aa79 100644 --- a/LibationWinForms/BookLiberation/BaseForms/LiberationBaseForm.cs +++ b/LibationWinForms/BookLiberation/BaseForms/LiberationBaseForm.cs @@ -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 FormSync { get; } = new CrossThreadSync(); 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()); /// - /// 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. /// - private void OnStreamingBeginShow(object sender, string beginString) - { - 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(); - } + private void OnStreamingBeginShow(object sender, string beginString) => FormSync.Send(Show); + #endregion #region IStreamable event handlers diff --git a/LibationWinForms/BookLiberation/ProcessorAutomationController.cs b/LibationWinForms/BookLiberation/ProcessorAutomationController.cs index 78faf7e4..736de469 100644 --- a/LibationWinForms/BookLiberation/ProcessorAutomationController.cs +++ b/LibationWinForms/BookLiberation/ProcessorAutomationController.cs @@ -52,7 +52,7 @@ namespace LibationWinForms.BookLiberation { public static async Task BackupSingleBookAsync(LibraryBook libraryBook, EventHandler 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(onDownloadFileStreamingCompleted); - async void runDownload() => await downloadFile.PerformDownloadFileAsync(url, destination); - new Task(runDownload).Start(); - } - private static IProcessable CreateBackupBook(EventHandler completedAction, LogMe logMe) { var downloadPdf = CreateProcessable(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(); + } + /// /// Create a new and links it to a new . /// - /// The derrived type to create. + /// The derrived type to create. /// The derrived Form to create on , Show on , Close on , and Dispose on /// The logger /// An additional event handler to handle /// A new of type - private static TStrProc CreateProcessable(LogMe logMe, EventHandler completedAction = null) + private static TProcessable CreateProcessable(LogMe logMe, EventHandler 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; + strProc.Completed += completedAction; return strProc; } - - /// - /// Creates a new and links it to a new - /// - /// The derrived type to create. - /// The derrived Form to create, which will Show on and Close, Dispose on . - /// An additional event handler to handle - /// A new of type - private static TStr CreateStreamable(EventHandler 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 diff --git a/LibationWinForms/CrossThreadSync[T].cs b/LibationWinForms/CrossThreadSync[T].cs new file mode 100644 index 00000000..5b9b6b63 --- /dev/null +++ b/LibationWinForms/CrossThreadSync[T].cs @@ -0,0 +1,45 @@ +using System; +using System.ComponentModel; +using System.Threading; + +namespace LibationWinForms +{ + internal class CrossThreadSync + { + public event EventHandler 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); + } + } +}