Reputation: 6202
I'm trying to run a STA (Single-Threaded Apartment) thread from a Web API (2.1) controller.
To do this, I'm using StaTaskScheduler:
/// <summary>Provides a scheduler that uses STA threads.</summary>
public sealed class StaTaskScheduler : TaskScheduler, IDisposable
{
/// <summary>Stores the queued tasks to be executed by our pool of STA threads.</summary>
private BlockingCollection<Task> _tasks;
/// <summary>The STA threads used by the scheduler.</summary>
private readonly List<Thread> _threads;
/// <summary>Initializes a new instance of the StaTaskScheduler class with the specified concurrency level.</summary>
/// <param name="numberOfThreads">The number of threads that should be created and used by this scheduler.</param>
public StaTaskScheduler(int numberOfThreads)
{
// Validate arguments
if (numberOfThreads < 1) throw new ArgumentOutOfRangeException("concurrencyLevel");
// Initialize the tasks collection
_tasks = new BlockingCollection<Task>();
// Create the threads to be used by this scheduler
_threads = Enumerable.Range(0, numberOfThreads).Select(i =>
{
var thread = new Thread(() =>
{
// Continually get the next task and try to execute it.
// This will continue until the scheduler is disposed and no more tasks remain.
foreach (var t in _tasks.GetConsumingEnumerable())
{
TryExecuteTask(t);
}
});
thread.IsBackground = true;
thread.SetApartmentState(ApartmentState.STA);
return thread;
}).ToList();
// Start all of the threads
_threads.ForEach(t => t.Start());
}
}
One option is to run the STA thread from a CustomHttpControllerDispatcher
:
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
//constraints: null,
// - Trying to avoid this
//handler: new Controllers.CustomHttpControllerDispatcher(config)
);
}
public class CustomHttpControllerDispatcher : System.Web.Http.Dispatcher.HttpControllerDispatcher
{
public CustomHttpControllerDispatcher(HttpConfiguration configuration) : base(configuration)
{
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// My stuff here
}
}
However, that would require manual construction of the http response, which involves json serialization, something I would rather avoid.
That leaves me with my current option, which is running the STA thread from within the library that the controller calls into (to generate a custom form).
Everything works OK if the library is called using a "Windows Application" type test application with a Main
decorated with [STAThread]
. However, when called from the web service controller, there is a "swallowed" exception when the following task returns:
Note: I'm am not rendering any dialogs, so there shouldn't be any "Showing a modal dialog box or form when the application is not running in UserInteractive mode is not a valid operation." exceptions, which do not occur at all when generating the custom form from an STA thread started within a CustomHttpControllerDispatcher
.
private Task<AClass> SendAsync1()
{
var staTaskScheduler = new StaTaskScheduler(1);
return Task.Factory.StartNew<CustomForm>(() =>
{
// - execution causes "swallowed exception"
return new AClass();
},
CancellationToken.None,
TaskCreationOptions.None,
staTaskScheduler
);
}
That is, when step debugging, the stack trace disappears after stepping over:
return new AClass();
I usually deal with this by narrowing down some breakpoints, but for this case I don't think it is possible since there are no debugging symbols or source for Task.cs (and the multitude of associated files).
Note: I can now step through the disassembly of System.Threading.Tasks
, however the debugging process will probably be lengthy since these are not trivial libraries, so any insight would be greatly appreciated.
I'm suspecting that maybe it's b/c I'm trying to schedule an STA thread from an MTA thread (controller), but am not positive?
Usage
GetCustomForm().Wait();
private FixedDocumentSequence _sequence;
private async Task GetCustomForm()
{
// - will be slapped with a "calling thread cannot access...", next issue
_sequence = await SendAsync1b();
}
private readonly StaTaskScheduler _staTaskScheduler = new StaTaskScheduler(1);
private Task<FixedDocumentSequence> SendAsync1b()
{
//var staTaskScheduler = new StaTaskScheduler(100);
//var staTaskScheduler = new StaTaskScheduler(1);
return Task.Factory.StartNew<FixedDocumentSequence>(() =>
{
FixedDocumentSequence sequence;
CustomForm view = new CustomForm();
view.ViewModel = new ComplaintCustomFormViewModel(BuildingEntity.form_id, (int)Record);
sequence = view.ViewModel.XpsDocument.GetFixedDocumentSequence();
return sequence;
},
CancellationToken.None,
TaskCreationOptions.None,
_staTaskScheduler);
}
References
- ASP.Net WebApi STA Mode
- Custom route handlers in ASP.Net WebAPI
- https://www.c-sharpcorner.com/article/global-and-per-route-message-handlers-in-webapi/
- https://learn.microsoft.com/en-us/aspnet/web-api/overview/advanced/http-message-handlers
- https://weblog.west-wind.com/posts/2012/Sep/18/Creating-STA-COM-compatible-ASPNET-Applications
Upvotes: 1
Views: 1804
Reputation: 6202
Running the WPF/Entity code directly in an STA thread seems to be working:
Thread thread = GetCustomFormPreviewView4();
thread.Start();
thread.Join();
private Thread GetCustomFormPreviewView4()
{
var thread = new Thread(() =>
{
FixedDocumentSequence sequence;
CustomForm view = new CustomForm();
view.ViewModel = new ComplaintCustomFormViewModel(BuildingEntity.form_id, (int)Record);
sequence = view.ViewModel.XpsDocument.GetFixedDocumentSequence();
//view.ShowDialog();
//...
}
);
thread.IsBackground = true;
thread.SetApartmentState(ApartmentState.STA);
return thread;
}
Upvotes: 0