1

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

samus
  • 6,102
  • 6
  • 31
  • 69
  • 1
    Just curious: how a web API is related to STA Thread. I remember the apartment problem being tied to COM (and never really understood it, actually), how the subject comes out in a "modern" web api? – Gian Paolo Aug 08 '18 at 19:38
  • @GianPaolo I added some references to post... I'm trying to *instantiate* a window, `CustomForm : Window`, so that I can create an XPS (and PDF) from it. However, I'm *not rendering* it to screen with `view.ShowDialog()`, just inflating it. – samus Aug 08 '18 at 20:16
  • 2
    So if I'm reading this correctly, you want to shoehorn a winforms app to do something that has nothing to do with winforms via an asp.net application? That seems like a very wrong course of action to take to fulfill a requirement. – Erik Philips Aug 08 '18 at 20:49
  • @ErikPhilips Updated post with usage example... I'm trying leverage existing functionality as much as possible, which uses Entity and Windows Forms to generate a view that can be transformed into an xps and then ultimately a pdf. This will give identical print-outs in the field and office. Writing a report generation tool from the ground up would be daunting. Do you know of any tools that can take windows forms xaml, bind data, and convert them to pdf (I certainly do not want to reinvent this wheel, but will if I must, though time is of essence)? – samus Aug 08 '18 at 21:11
  • Honestly, in **my opinion** it would be better to spend the time writing a PDF and integrating it back into winforms/wpf than it would be the other way around. And I have extensive experience creating reports from scratch in c# (while keeping them cross compatible with Full .Net and CE .Net long ago...) – Erik Philips Aug 08 '18 at 21:16
  • @ErikPhilips I wouldn't even know where to begin with such an approach? – samus Aug 08 '18 at 21:23
  • Generally speaking, it's easier for many programmers (i don't know if it's most or not) to generate html and use a convert to convert to pdf, then to write pure PDF. Additionally, an advantage is that html can then be used in an email or viewed in a browser. However this is more time consuming then just writing pure PDF. There are many free/$$$ frameworks that allow you to do both. – Erik Philips Aug 08 '18 at 23:37
  • @ErikPhilips Bringing the application to the web would be nice, but isn't a current priority. – samus Aug 09 '18 at 12:29
  • Rather than messing with the already complicated threading behavior inside of Web API, why not look at [hosting a dedicated Windows Service to do what you are trying.](https://stackoverflow.com/questions/2001667/net-windows-service-needs-to-use-stathread) A WIndows Service is also MTA but since your code "owns" the process you limit the fallout of clobbering other threads that would be running serving requests. Personally, I would prefer taking an approach like what @ErikPhilips recommends. – Sixto Saez Aug 09 '18 at 13:07
  • @SixtoSaez I will consider the windows service route, thanks. I'm not overly concerned about blocking requests, since each web service instance is low volume. I can get the Window/Form to instantiate within it's containing Task, which runs to completion on it's assigned thread. However, it seems to be cancelling at some point after returning, and looking for cancellation token (I think, stepping through disassembled System.Threading libraries seems pointless without debugging symbols, it's interesting that it even can, not sure why it bothers since it's never goes to the right line). – samus Aug 09 '18 at 13:52

1 Answers1

0

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;
}
samus
  • 6,102
  • 6
  • 31
  • 69
  • related issue: https://stackoverflow.com/questions/51902658/cannot-get-xps-rendered-flowdocument-to-update-data-bound-elements-fields – samus Aug 20 '18 at 13:40