[GH-ISSUE #165] Trigger signal on main thread from secondary thread #105

Closed
opened 2026-05-05 11:04:56 -06:00 by gitea-mirror · 6 comments
Owner

Originally created by @WingedToaster on GitHub (Sep 16, 2019).
Original GitHub issue: https://github.com/qmlnet/qmlnet/issues/165

I'm trying to integrate a barcode reader into an app, the serial port is waiting for data come in on a secondary thread but if I try to show the result from that thread, I get a message saying the signal must be called on the main thread, but I'm not quite sure how to do that. In Qt I'm able to just link up the two threads and then when a barcode is read I can emit it to the main thread. How can I do that in Qml.Net?

Any help would be appreciated.

Originally created by @WingedToaster on GitHub (Sep 16, 2019). Original GitHub issue: https://github.com/qmlnet/qmlnet/issues/165 I'm trying to integrate a barcode reader into an app, the serial port is waiting for data come in on a secondary thread but if I try to show the result from that thread, I get a message saying the signal must be called on the main thread, but I'm not quite sure how to do that. In Qt I'm able to just link up the two threads and then when a barcode is read I can `emit` it to the main thread. How can I do that in Qml.Net? Any help would be appreciated.
Author
Owner

@pauldotknopf commented on GitHub (Sep 17, 2019):

The QCoreApplication has a method to invoke actions in the GUI. I'm mobile right now, but you should be able to find what you are looking for there.

<!-- gh-comment-id:532020825 --> @pauldotknopf commented on GitHub (Sep 17, 2019): The QCoreApplication has a method to invoke actions in the GUI. I'm mobile right now, but you should be able to find what you are looking for there.
Author
Owner

@WingedToaster commented on GitHub (Sep 17, 2019):

I tried a few things and it crashes every time...

SynchronizationContext.Current.ActivateSignal("scanResultChanged", resultPage);
SynchronizationContext.Current.Post(delegate {this.ActivateSignal("scanResultChanged", resultPage);}, null);
QGuiApplication.AwaitTask(Task.Run(() => {
    this.ActivateSignal("scanResultChanged", resultPage);
}), QEventLoop.ProcessEventsFlag.AllEvents);

I don't have my scanner handy tonight but to trigger it, I just created a thread, slept for 4 seconds and then tried to call it...

ScannerController.cs

    [Signal("resultSignal", NetVariantType.Object)]
    public class ScannerController
    {
        private Thread _tempThread;
        ...
        public async Task ComponentCompleted()
        {
           ...
            _tempThread = new Thread(tempThread);
            _tempThread.Start();
           ...
        }

        ...

        private void tempThread()
        {
            Thread.Sleep(4000);
            var resultPage = new ScanResultType() {
                        title = "VALID", 
                        backgroundColor = _configuration["result:valid:backgroundColor"], 
                        foregroundColor = _configuration["result:valid:foregroundColor"], 
                        timerLength = int.Parse(_configuration["result:valid:timeout"]), 
                        };
            SynchronizationContext.Current.ActivateSignal("resultSignal", resultPage);
        }


    }

Then in my qml, I do have a call to connect to the signal scanner.resultSignal.connect(function() {...}) but this is the error I get now...

Unhandled Exception: System.ArgumentNullException: Value cannot be null.
Parameter name: key
   at System.Runtime.CompilerServices.ConditionalWeakTable`2.TryGetValue(TKey key, TValue& value)
   at Qml.Net.Internal.ObjectSignals.GetAttachedDelegates(Object obj, String signal)
   at Qml.Net.Signals.ActivateSignal(Object instance, String signalName, Object[] args)
   at test.Controllers.ScannerController.tempThread() in /test/Controllers/ScannerController.cs:line 566
   at System.Threading.Thread.ThreadMain_ThreadStart()
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()

Seems like I'm missing something where the signal needs to be registered or something, but I don't know what...

<!-- gh-comment-id:532079729 --> @WingedToaster commented on GitHub (Sep 17, 2019): I tried a few things and it crashes every time... ``` SynchronizationContext.Current.ActivateSignal("scanResultChanged", resultPage); ``` ``` SynchronizationContext.Current.Post(delegate {this.ActivateSignal("scanResultChanged", resultPage);}, null); ``` ``` QGuiApplication.AwaitTask(Task.Run(() => { this.ActivateSignal("scanResultChanged", resultPage); }), QEventLoop.ProcessEventsFlag.AllEvents); ``` I don't have my scanner handy tonight but to trigger it, I just created a thread, slept for 4 seconds and then tried to call it... **ScannerController.cs** ``` [Signal("resultSignal", NetVariantType.Object)] public class ScannerController { private Thread _tempThread; ... public async Task ComponentCompleted() { ... _tempThread = new Thread(tempThread); _tempThread.Start(); ... } ... private void tempThread() { Thread.Sleep(4000); var resultPage = new ScanResultType() { title = "VALID", backgroundColor = _configuration["result:valid:backgroundColor"], foregroundColor = _configuration["result:valid:foregroundColor"], timerLength = int.Parse(_configuration["result:valid:timeout"]), }; SynchronizationContext.Current.ActivateSignal("resultSignal", resultPage); } } ``` Then in my qml, I do have a call to connect to the signal `scanner.resultSignal.connect(function() {...})` but this is the error I get now... ``` Unhandled Exception: System.ArgumentNullException: Value cannot be null. Parameter name: key at System.Runtime.CompilerServices.ConditionalWeakTable`2.TryGetValue(TKey key, TValue& value) at Qml.Net.Internal.ObjectSignals.GetAttachedDelegates(Object obj, String signal) at Qml.Net.Signals.ActivateSignal(Object instance, String signalName, Object[] args) at test.Controllers.ScannerController.tempThread() in /test/Controllers/ScannerController.cs:line 566 at System.Threading.Thread.ThreadMain_ThreadStart() at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state) --- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() ``` Seems like I'm missing something where the signal needs to be registered or something, but I don't know what...
Author
Owner

@pauldotknopf commented on GitHub (Sep 17, 2019):

You'll want to use the Dispatch and DispatchAsync methods here.

The AwaitTask doesn't dispatch anything, it simply blocks the caller until the given task is completed.

I don't blame you though, there isn't a lot of documentation.

<!-- gh-comment-id:532200951 --> @pauldotknopf commented on GitHub (Sep 17, 2019): You'll want to use the ```Dispatch``` and ```DispatchAsync``` methods [here](https://github.com/qmlnet/qmlnet/blob/c2540cc34333ca8b36d453e5e007874f0c83f455/src/net/Qml.Net/QCoreApplication.cs#L89). The ```AwaitTask``` doesn't dispatch anything, it simply blocks the caller until the given task is completed. I don't blame you though, there isn't a lot of documentation.
Author
Owner

@WingedToaster commented on GitHub (Sep 17, 2019):

OK, I was afraid of that since it's the instance method. I assume there's no quick way of getting the instance of the running application and I should try to follow the example of the AppModel with the UiDelegate code in the PhotoFrame example...

<!-- gh-comment-id:532228392 --> @WingedToaster commented on GitHub (Sep 17, 2019): OK, I was afraid of that since it's the instance method. I assume there's no quick way of getting the instance of the running application and I should try to follow the example of the `AppModel` with the `UiDelegate` code in the PhotoFrame example...
Author
Owner

@pauldotknopf commented on GitHub (Sep 17, 2019):

In my solutions, I create an IDispatcher that takes the instance of QCoreApplication at the root, and registers it with a dependency container. Then, when your AppModel get's resolved, you can take in an IDispatcher to dispatch your actions to the GUI thread.

Another option is to store the SynchronizationContext into a your own variable, which can be used to dispatch the action.

<!-- gh-comment-id:532229669 --> @pauldotknopf commented on GitHub (Sep 17, 2019): In my solutions, I create an ```IDispatcher``` that takes the instance of ```QCoreApplication``` at the root, and registers it with a dependency container. Then, when your ```AppModel``` get's resolved, you can take in an ```IDispatcher``` to dispatch your actions to the GUI thread. Another option is to store the ```SynchronizationContext``` into a your own variable, which can be used to dispatch the action.
Author
Owner

@WingedToaster commented on GitHub (Sep 18, 2019):

Ok, after spending my entire day on this, not really knowing what I was doing (but thinking I did)...and failing...miserably...I mean really miserably...I finally figured this out...I think...

ScanController.cs

[Signal("resultSignal", NetVariantType.Object)]
    public class ScanController : IQmlComponentCompleted
    {

        public static UiDispatchDelegate UiDispatch { get; set; }

        private async void SerialPort_DataReceivedHandlerAsync(object sender, SerialDataReceivedEventArgs e)
        {
            SerialPort sp = (SerialPort)sender;
            //string barcode = sp.ReadTo("\x03"); // Read until the EOT code
            string barcode = sp.ReadLine();

            [...validation logic...]

            if (valid)
            {
                    var resultPage = new ScanResultType() {
                                            title = "VALID", 
                                            backgroundColor = _configuration["result:valid:backgroundColor"], 
                                            foregroundColor = _configuration["result:valid:foregroundColor"], 
                                            timerLength = int.Parse(_configuration["result:valid:timeout"]), 
                                            sound = "valid"
                                            };
                    this.DisplayScanResult(resultPage);
            }
            else
            {
                    var resultPage = new ScanResultType() {
                                            title = "INVALID", 
                                            backgroundColor = _configuration["result:invalid:backgroundColor"], 
                                            foregroundColor = _configuration["result:invalid:foregroundColor"], 
                                            timerLength = int.Parse(_configuration["result:invalid:timeout"]), 
                                            sound = "invalid"
                                            };                    
                    this.DisplayScanResult(resultPage);
            }
        }

        private void DisplayScanResult(ScanResultType scanResult) 
        {
            UiDispatch.Invoke(() => {
                this.ActivateSignal("resultSignal", scanResult);
            });
        }

        ...
    }

Program.cs

ScanController.UiDispatch += (a) => app.Dispatch(a);

ScanPage.qml

        ....

        Component.onCompleted: {

            scanner.resultSignal.connect(function(result) {
                scanResult.title = result.title;
                scanResult.message = result.message;
                scanResult.backgroundColor = result.backgroundColor
                scanResult.foregroundColor = result.foregroundColor
                scanResult.timerLength = result.timerLength
                scanResult.sound = result.sound
                scanResult.visible = true
            })

        }

        ...

<!-- gh-comment-id:532530601 --> @WingedToaster commented on GitHub (Sep 18, 2019): Ok, after spending my entire day on this, not really knowing what I was doing (but thinking I did)...and failing...miserably...I mean really miserably...I finally figured this out...I think... **ScanController.cs** ``` [Signal("resultSignal", NetVariantType.Object)] public class ScanController : IQmlComponentCompleted { public static UiDispatchDelegate UiDispatch { get; set; } private async void SerialPort_DataReceivedHandlerAsync(object sender, SerialDataReceivedEventArgs e) { SerialPort sp = (SerialPort)sender; //string barcode = sp.ReadTo("\x03"); // Read until the EOT code string barcode = sp.ReadLine(); [...validation logic...] if (valid) { var resultPage = new ScanResultType() { title = "VALID", backgroundColor = _configuration["result:valid:backgroundColor"], foregroundColor = _configuration["result:valid:foregroundColor"], timerLength = int.Parse(_configuration["result:valid:timeout"]), sound = "valid" }; this.DisplayScanResult(resultPage); } else { var resultPage = new ScanResultType() { title = "INVALID", backgroundColor = _configuration["result:invalid:backgroundColor"], foregroundColor = _configuration["result:invalid:foregroundColor"], timerLength = int.Parse(_configuration["result:invalid:timeout"]), sound = "invalid" }; this.DisplayScanResult(resultPage); } } private void DisplayScanResult(ScanResultType scanResult) { UiDispatch.Invoke(() => { this.ActivateSignal("resultSignal", scanResult); }); } ... } ``` **Program.cs** ``` ScanController.UiDispatch += (a) => app.Dispatch(a); ``` **ScanPage.qml** ``` .... Component.onCompleted: { scanner.resultSignal.connect(function(result) { scanResult.title = result.title; scanResult.message = result.message; scanResult.backgroundColor = result.backgroundColor scanResult.foregroundColor = result.foregroundColor scanResult.timerLength = result.timerLength scanResult.sound = result.sound scanResult.visible = true }) } ... ```
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: github-starred/qmlnet#105
No description provided.