Question

I have a WPF program where my model need to load a "Out-of-Proc" (.exe) COM component in order to achieve some validations when a user make an action on the UI. I'd like to inform the user that a long action will take place to let him know that the application is busy, not just frozen. But any action on the UI does occur after the COM action is completed.

I think that any COM communication should be done on the main UI thread. It eliminates any solution that would run on another thread than the main (UI) thread.

I tried many options without success:

I can't see how I can achieve a synchronous action from a model where I would need to refresh the UI. My action has a property "IsLoading" that I subscribe from my view and where I try to update the UI according to its state but it seems that is not possible in WPF ???

Any other suggestions ?

Can I use async/await and do my COM action from another thread running another dispatcher (kind of complex) and will loose required synchronicity (user need results of COM operation to continue its work) ?

Mainly for Blindy... Some clearer explications (more details about required synchronicity):

When a user click on a TreeView item, I load a grid then need to verify that data entered in the grid is still valid. To do validation, I need to load an application through COM and automate it to load a document, then parse it and verify the data in the grid (in the Model of the Grid in the view). That takes 10 seconds. If I do that on another thread, then user can do an action to select to add a new row in the grid which still depends on the same COM application loaded with the previous document. I still need to wait for the application to load. It is a synchronous action. My application depends on that COM application with its loaded document to be in valid state for user to take more actions. But I need to give user some feedback on what I’m doing (start COM app and load on document). Doing COM action on another thread just report the problem later but do not solve the fact that user need to wait that the action would complete. I think I need to (force) update my WPF app but can’t find any (twisted) way to do it.

Was it helpful?

Solution

[UPDATE] As the OP has updated the question and specified he's using out-of-proc COM objects, the custom STA thread plumbing described below doesn't make sense. Now, a simple await Task.Run(() => { /* call the out-of-proc COM */}) is enough to keep the UI responsive. Kudos to @acelent for clarifying this point.


Recently I answered a related question: StaTaskScheduler and STA thread message pumping.

The solution was to create and use STA COM objects on a dedicated background STA thread which provides both message pumping and thread affinity for those COM objects.

I'd like to show how ThreadWithAffinityContext can be used in your case, with async/await:

dynamic _comObject = null;

ThreadWithAffinityContext _staThread = null;

// Start the long-running task
Task NewCommandHandlerAsync()
{
    // create the ThreadWithAffinityContext if haven't done this yet
    if (_staThread == null)
        _staThread = new ThreadWithAffinityContext(
            staThread: true,
            pumpMessages: true);

    // create the COM Object if haven't done this yet
    if (_comObject == null)
    {
        await _staThread.Run(() =>
        {
            // _comObject will live on a dedicated STA thread,
            // run by ThreadWithAffinityContext
            _comObject = new ComObject();
        }, CancellationToken.None);
    }

    // use the COM object
    await _staThread.Run(() =>
    {
        // run a lengthy process
        _comObject.DoWork();
    }, CancellationToken.None);
}

// keep track of pending NewCommandHandlerAsync
Task _newCommandHandler = null;

// handle a WPF command
private async void NewCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
    try
    {
        // avoid re-entrancy (i.e., running two NewCommandHandlerAsync in parallel)
        if (_newCommandHandler != null)
            throw new InvalidOperationException("One NewCommandHandlerAsync at a time!");
        try
        {
            await _newCommandHandler = NewCommandHandlerAsync();
        }
        finally
        {
            _newCommandHandler = null;
        }
    }
    catch (Exception ex)
    {
        // handle all exceptions possibly thrown inside "async void" method
        MessageBox.Show(ex.Message);
    }
}

The fact that we have offloaded the lengthy process _comObject.DoWork() to a separate thread doesn't automatically solve the other common UI-related problem:

How to handle the UI when the lengthy background operation is pending?

There is a number of options. E.g., you can disable the UI elements which fire NewCommand_Executed event, to avoid re-entrancy, and enable another UI element to allow the user to cancel the pending work (a Stop button). You should also provide some progress feedback, if your COM object supports that.

Alternatively, you can display a modal dialog before staring the long-running task and hide it when the task has completed. As far as the UI usability goes, modality is less desirable, but it's very easy to implement (example).

OTHER TIPS

You can create and use COM objects on any thread, the marshaller will take care of running it on a background thread if your application uses the STA threading model. There's no need to funnel every call through the main UI thread.

As to your last question in comments, since you'll be running this on a background thread, you of course need to synchronize like usual with lock and Invoke the result back on the main thread when done. This is nothing specially related to COM, it's just how Windows threading works.

In short, stop using the UI thread for heavy duty, non-UI related work.

I used this once for WPF so as to force the screen to re-Paint : I used auto-translation from VB so I hope it's correct

private Action EmptyDelegate = () => { };
[System.Runtime.CompilerServices.Extension()]
public void Refresh(UIElement uiElement)
{
    uiElement.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Render, EmptyDelegate);
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top