diff --git a/LibationWinForms/AsyncNotifyPropertyChanged.cs b/LibationWinForms/AsyncNotifyPropertyChanged.cs index d9511f2d..20b8b512 100644 --- a/LibationWinForms/AsyncNotifyPropertyChanged.cs +++ b/LibationWinForms/AsyncNotifyPropertyChanged.cs @@ -4,15 +4,13 @@ using System.Threading; namespace LibationWinForms { - public abstract class AsyncNotifyPropertyChanged : INotifyPropertyChanged + public abstract class AsyncNotifyPropertyChanged : SynchronizeInvoker, INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; - private CrossThreadSync ThreadSync { get; } = new CrossThreadSync(); - public AsyncNotifyPropertyChanged() - =>ThreadSync.ObjectReceived += (_, args) => PropertyChanged?.Invoke(this, args); + public AsyncNotifyPropertyChanged() { } protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "") - => ThreadSync.Post(new PropertyChangedEventArgs(propertyName)); + =>BeginInvoke(PropertyChanged, new object[] { this, new PropertyChangedEventArgs(propertyName) }); } } diff --git a/LibationWinForms/BookLiberation/BaseForms/LiberationBaseForm.cs b/LibationWinForms/BookLiberation/BaseForms/LiberationBaseForm.cs index e978aa79..b7f48d94 100644 --- a/LibationWinForms/BookLiberation/BaseForms/LiberationBaseForm.cs +++ b/LibationWinForms/BookLiberation/BaseForms/LiberationBaseForm.cs @@ -11,15 +11,14 @@ namespace LibationWinForms.BookLiberation.BaseForms { protected IStreamable Streamable { get; private set; } protected LogMe LogMe { get; private set; } - private CrossThreadSync FormSync { get; } = new CrossThreadSync(); + private SynchronizeInvoker Invoker { get; init; } public LiberationBaseForm() { //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(); + Invoker = new SynchronizeInvoker(); } public void RegisterFileLiberator(IStreamable streamable, LogMe logMe = null) @@ -134,7 +133,7 @@ namespace LibationWinForms.BookLiberation.BaseForms /// 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) => FormSync.Send(Show); + private void OnStreamingBeginShow(object sender, string beginString) => Invoker.Invoke(Show); #endregion diff --git a/LibationWinForms/CrossThreadSync[T].cs b/LibationWinForms/CrossThreadSync[T].cs deleted file mode 100644 index 5b9b6b63..00000000 --- a/LibationWinForms/CrossThreadSync[T].cs +++ /dev/null @@ -1,45 +0,0 @@ -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); - } - } -} diff --git a/LibationWinForms/GridEntry.cs b/LibationWinForms/GridEntry.cs index b173051a..558d22ec 100644 --- a/LibationWinForms/GridEntry.cs +++ b/LibationWinForms/GridEntry.cs @@ -96,7 +96,6 @@ namespace LibationWinForms public string Description { get; } public string DisplayTags => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated); public (LiberatedState, PdfState) Liberate => (LibraryCommands.Liberated_Status(Book), LibraryCommands.Pdf_Status(Book)); - #endregion #region Data Sorting diff --git a/LibationWinForms/SynchronizeInvoker.cs b/LibationWinForms/SynchronizeInvoker.cs new file mode 100644 index 00000000..0d1388ad --- /dev/null +++ b/LibationWinForms/SynchronizeInvoker.cs @@ -0,0 +1,143 @@ +using System; +using System.ComponentModel; +using System.Threading; + +namespace LibationWinForms +{ + public class SynchronizeInvoker : ISynchronizeInvoke + { + public bool InvokeRequired => Thread.CurrentThread.ManagedThreadId != InstanceThreadId; + private int InstanceThreadId { get; set; } = Thread.CurrentThread.ManagedThreadId; + private SynchronizationContext SyncContext { get; init; } = SynchronizationContext.Current; + + public SynchronizeInvoker() + { + SyncContext = SynchronizationContext.Current; + if (SyncContext is null) + throw new NullReferenceException($"Could not capture a current {nameof(SynchronizationContext)}"); + } + + public IAsyncResult BeginInvoke(Action action) => BeginInvoke(action, null); + public IAsyncResult BeginInvoke(Delegate method) => BeginInvoke(method, null); + public IAsyncResult BeginInvoke(Delegate method, object[] args) + { + var tme = new ThreadMethodEntry(method, args); + + if (InvokeRequired) + { + SyncContext.Post(ThreadMethodEntry.OnSendOrPostCallback, tme); + } + else + { + tme.Complete(); + tme.CompletedSynchronously = true; + } + return tme; + } + + public object EndInvoke(IAsyncResult result) + { + if (result is not ThreadMethodEntry crossThread) + throw new ArgumentException($"{nameof(result)} was not returned by {nameof(BeginInvoke)}"); + + if (!crossThread.IsCompleted) + crossThread.AsyncWaitHandle.WaitOne(); + + return crossThread.ReturnValue; + } + + public object Invoke(Action action) => Invoke(action, null); + public object Invoke(Delegate method) => Invoke(method, null); + public object Invoke(Delegate method, object[] args) + { + var tme = new ThreadMethodEntry(method, args); + + if (InvokeRequired) + { + SyncContext.Send(ThreadMethodEntry.OnSendOrPostCallback, tme); + } + else + { + tme.Complete(); + tme.CompletedSynchronously = true; + } + + return tme.ReturnValue; + } + + private class ThreadMethodEntry : IAsyncResult + { + public object AsyncState => null; + public bool CompletedSynchronously { get; internal set; } + public bool IsCompleted { get; private set; } + public object ReturnValue { get; private set; } + public WaitHandle AsyncWaitHandle + { + get + { + if (resetEvent == null) + { + lock (invokeSyncObject) + { + if (resetEvent == null) + { + resetEvent = new ManualResetEvent(initialState: false); + } + } + } + return resetEvent; + } + } + + private object invokeSyncObject = new object(); + private Delegate method; + private object[] args; + private ManualResetEvent resetEvent; + + public ThreadMethodEntry(Delegate method, object[] args) + { + this.method = method; + this.args = args; + resetEvent = new ManualResetEvent(initialState: false); + } + /// + /// This callback executes on the SynchronizationContext thread. + /// + public static void OnSendOrPostCallback(object asyncArgs) + { + var e = asyncArgs as ThreadMethodEntry; + + e.Complete(); + } + + public void Complete() + { + try + { + switch (method) + { + case Action actiton: + actiton(); + break; + default: + ReturnValue = method.DynamicInvoke(args); + break; + } + } + finally + { + IsCompleted = true; + resetEvent.Set(); + } + } + + ~ThreadMethodEntry() + { + if (resetEvent != null) + { + resetEvent.Close(); + } + } + } + } +} \ No newline at end of file