Improved cross threaded invocation.

This commit is contained in:
Michael Bucari-Tovo 2021-08-13 16:34:09 -06:00
parent 766d427b19
commit a44c46333f
5 changed files with 149 additions and 55 deletions

View File

@ -4,15 +4,13 @@ using System.Threading;
namespace LibationWinForms namespace LibationWinForms
{ {
public abstract class AsyncNotifyPropertyChanged : INotifyPropertyChanged public abstract class AsyncNotifyPropertyChanged : SynchronizeInvoker, INotifyPropertyChanged
{ {
public event PropertyChangedEventHandler PropertyChanged; public event PropertyChangedEventHandler PropertyChanged;
private CrossThreadSync<PropertyChangedEventArgs> ThreadSync { get; } = new CrossThreadSync<PropertyChangedEventArgs>();
public AsyncNotifyPropertyChanged() 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)); =>BeginInvoke(PropertyChanged, new object[] { this, new PropertyChangedEventArgs(propertyName) });
} }
} }

View File

@ -11,15 +11,14 @@ 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 SynchronizeInvoker Invoker { get; init; }
public LiberationBaseForm() public LiberationBaseForm()
{ {
//SynchronizationContext.Current will be null until the process contains a Form. //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 //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. //reaches inside the constructor. So need to reset the context here.
FormSync.ResetContext(); Invoker = new SynchronizeInvoker();
FormSync.ObjectReceived += (_, action) => action();
} }
public void RegisterFileLiberator(IStreamable streamable, LogMe logMe = null) 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 /// 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. /// (ie. shown) because Control doesn't get a window handle until it is Shown.
/// </summary> /// </summary>
private void OnStreamingBeginShow(object sender, string beginString) => FormSync.Send(Show); private void OnStreamingBeginShow(object sender, string beginString) => Invoker.Invoke(Show);
#endregion #endregion

View File

@ -1,45 +0,0 @@
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);
}
}
}

View File

@ -96,7 +96,6 @@ namespace LibationWinForms
public string Description { get; } public string Description { get; }
public string DisplayTags => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated); public string DisplayTags => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated);
public (LiberatedState, PdfState) Liberate => (LibraryCommands.Liberated_Status(Book), LibraryCommands.Pdf_Status(Book)); public (LiberatedState, PdfState) Liberate => (LibraryCommands.Liberated_Status(Book), LibraryCommands.Pdf_Status(Book));
#endregion #endregion
#region Data Sorting #region Data Sorting

View File

@ -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);
}
/// <summary>
/// This callback executes on the SynchronizationContext thread.
/// </summary>
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();
}
}
}
}
}