Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions src/Avalonia.Base/Threading/CulturePreservingExecutionContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
#if NET6_0_OR_GREATER
// In .NET Core, the security context and call context are not supported, however,
// the impersonation context and culture would typically flow with the execution context.
// See: https://learn.microsoft.com/en-us/dotnet/api/system.threading.executioncontext
//
// So we can safely use ExecutionContext without worrying about culture flowing issues.
#else
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Threading;

namespace Avalonia.Threading;

/// <summary>
/// An ExecutionContext that preserves culture information across async operations.
/// This is a modernized version that removes legacy compatibility switches and
/// includes nullable reference type annotations.
/// </summary>
internal sealed class CulturePreservingExecutionContext
{
private readonly ExecutionContext _context;
private CultureAndContext? _cultureAndContext;

private CulturePreservingExecutionContext(ExecutionContext context)
{
_context = context;
}

/// <summary>
/// Captures the current ExecutionContext and culture information.
/// </summary>
/// <returns>A new CulturePreservingExecutionContext instance, or null if no context needs to be captured.</returns>
public static CulturePreservingExecutionContext? Capture()
{
// ExecutionContext.SuppressFlow had been called.
// We expect ExecutionContext.Capture() to return null, so match that behavior and return null.
if (ExecutionContext.IsFlowSuppressed())
{
return null;
}

var context = ExecutionContext.Capture();
if (context == null)
return null;

return new CulturePreservingExecutionContext(context);
}

/// <summary>
/// Runs the specified callback in the captured execution context while preserving culture information.
/// This method is used for .NET Framework and earlier .NET versions.
/// </summary>
/// <param name="executionContext">The execution context to run in.</param>
/// <param name="callback">The callback to execute.</param>
/// <param name="state">The state to pass to the callback.</param>
public static void Run(CulturePreservingExecutionContext executionContext, ContextCallback callback, object? state)
Comment thread
walterlv marked this conversation as resolved.
{
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (callback == null)
return;

// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (executionContext == null)
ThrowNullContext();

// Save culture information - we will need this to restore just before
// the callback is actually invoked from CallbackWrapper.
executionContext._cultureAndContext = CultureAndContext.Initialize(callback, state);

try
{
ExecutionContext.Run(
executionContext._context,
s_callbackWrapperDelegate,
executionContext._cultureAndContext);
}
finally
{
// Restore culture information - it might have been modified during callback execution.
executionContext._cultureAndContext.RestoreCultureInfos();
}
}

[DoesNotReturn]
private static void ThrowNullContext()
{
throw new InvalidOperationException("ExecutionContext cannot be null.");
}

private static readonly ContextCallback s_callbackWrapperDelegate = CallbackWrapper;

/// <summary>
/// Executes the callback and saves culture values immediately afterwards.
/// </summary>
/// <param name="obj">Contains the actual callback and state.</param>
private static void CallbackWrapper(object? obj)
{
var cultureAndContext = (CultureAndContext)obj!;

// Restore culture information saved during Run()
cultureAndContext.RestoreCultureInfos();

try
{
// Execute the actual callback
cultureAndContext.Callback(cultureAndContext.State);
}
finally
{
// Save any culture changes that might have occurred during callback execution
cultureAndContext.CaptureCultureInfos();
}
}

/// <summary>
/// Helper class to manage culture information across execution contexts.
/// </summary>
private sealed class CultureAndContext
{
public ContextCallback Callback { get; }
public object? State { get; }

private CultureInfo? _culture;
private CultureInfo? _uiCulture;

private CultureAndContext(ContextCallback callback, object? state)
{
Callback = callback;
State = state;
CaptureCultureInfos();
}

public static CultureAndContext Initialize(ContextCallback callback, object? state)
{
return new CultureAndContext(callback, state);
}

public void CaptureCultureInfos()
{
_culture = Thread.CurrentThread.CurrentCulture;
_uiCulture = Thread.CurrentThread.CurrentUICulture;
}

public void RestoreCultureInfos()
{
if (_culture != null)
Thread.CurrentThread.CurrentCulture = _culture;

if (_uiCulture != null)
Thread.CurrentThread.CurrentUICulture = _uiCulture;
}
}
}
#endif
48 changes: 35 additions & 13 deletions src/Avalonia.Base/Threading/DispatcherOperation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
using System.Threading;
using System.Threading.Tasks;

#if NET6_0_OR_GREATER
using ExecutionContext = System.Threading.ExecutionContext;
#else
using ExecutionContext = Avalonia.Threading.CulturePreservingExecutionContext;
#endif

namespace Avalonia.Threading;

[DebuggerDisplay("{DebugDisplay}")]
Expand All @@ -28,18 +34,19 @@ public DispatcherPriority Priority

protected internal object? Callback;
protected object? TaskSource;

internal DispatcherOperation? SequentialPrev { get; set; }
internal DispatcherOperation? SequentialNext { get; set; }
internal DispatcherOperation? PriorityPrev { get; set; }
internal DispatcherOperation? PriorityNext { get; set; }
internal PriorityChain? Chain { get; set; }

internal bool IsQueued => Chain != null;

private EventHandler? _aborted;
private EventHandler? _completed;
private DispatcherPriority _priority;
private readonly ExecutionContext? _executionContext;

internal DispatcherOperation(Dispatcher dispatcher, DispatcherPriority priority, Action callback, bool throwOnUiThread) :
this(dispatcher, priority, throwOnUiThread)
Expand All @@ -52,6 +59,7 @@ private protected DispatcherOperation(Dispatcher dispatcher, DispatcherPriority
ThrowOnUiThread = throwOnUiThread;
Priority = priority;
Dispatcher = dispatcher;
_executionContext = ExecutionContext.Capture();
Comment thread
walterlv marked this conversation as resolved.
}

internal string DebugDisplay
Expand Down Expand Up @@ -103,7 +111,7 @@ public event EventHandler Completed
_completed += value;
}
}
Comment thread
walterlv marked this conversation as resolved.

remove
{
lock(Dispatcher.InstanceLock)
Expand All @@ -112,7 +120,7 @@ public event EventHandler Completed
}
}
}

public bool Abort()
{
if (Dispatcher.Abort(this))
Expand Down Expand Up @@ -155,7 +163,7 @@ public void Wait(TimeSpan timeout)
// we throw an exception instead.
throw new InvalidOperationException("A thread cannot wait on operations already running on the same thread.");
}

var cts = new CancellationTokenSource();
EventHandler finishedHandler = delegate
{
Expand Down Expand Up @@ -241,7 +249,7 @@ private void Exit()
}

public Task GetTask() => GetTaskCore();

/// <summary>
/// Returns an awaiter for awaiting the completion of the operation.
/// </summary>
Expand All @@ -259,21 +267,35 @@ internal void CallAbortCallbacks()
AbortTask();
_aborted?.Invoke(this, EventArgs.Empty);
}

internal void Execute()
{
Debug.Assert(Status == DispatcherOperationStatus.Executing);
try
{
using (AvaloniaSynchronizationContext.Ensure(Dispatcher, Priority))
InvokeCore();
{
if (_executionContext is { } executionContext)
{
#if NET6_0_OR_GREATER
ExecutionContext.Restore(executionContext);
InvokeCore();
#else
ExecutionContext.Run(executionContext, static s => ((DispatcherOperation)s!).InvokeCore(), this);
#endif
}
else
{
InvokeCore();
}
}
}
finally
{
_completed?.Invoke(this, EventArgs.Empty);
}
}

protected virtual void InvokeCore()
{
try
Expand Down Expand Up @@ -305,7 +327,7 @@ protected virtual void InvokeCore()
}

internal virtual object? GetResult() => null;

protected virtual void AbortTask()
{
object? taskSource;
Expand Down Expand Up @@ -401,14 +423,14 @@ internal sealed class SendOrPostCallbackDispatcherOperation : DispatcherOperatio
{
private readonly object? _arg;

internal SendOrPostCallbackDispatcherOperation(Dispatcher dispatcher, DispatcherPriority priority,
SendOrPostCallback callback, object? arg, bool throwOnUiThread)
internal SendOrPostCallbackDispatcherOperation(Dispatcher dispatcher, DispatcherPriority priority,
SendOrPostCallback callback, object? arg, bool throwOnUiThread)
: base(dispatcher, priority, throwOnUiThread)
{
Callback = callback;
_arg = arg;
}

protected override void InvokeCore()
{
try
Expand Down
Loading
Loading