2

I'm working with an external library for a card scanner which to keep simple, I'll refer to as ScanLib. The scanning process is long, and of course, it tends to block the UI thread while calling methods like ScanLib.Scan(), thus I figured this would be a great time to use Tasks. Here the simplified (or TLDR) code for context(Full code below for more details, but for time saving's sake, here it is summarized):

public partial class MainForm : Form () 
{
//Here I initialize the reference variable which I will use to work with the Scanner library
ScanLib scanLibraryReference = new ScanLib();

   // Form constructor
   public Form()
   {
      //This initializes the scanner and it's library components, it runs until the program closes
      Task scannerInitTask = Task.Run(() => scanLibraryReference.InitializeScanLibrary())
      InitializeComponent();
   }

   private void Button_Click(object sender, EventArgs e) {
      Task scannerTask = Task.Run(() => scanLibraryReference.ScanCard()); 
   }
}

The problem is, that when I try to scan a card, it freezes the main UI thread, even tough I run both the ScanLib.InitializeScanLib() and ScanLib.ScanCard() methods inside other tasks to try not block the main UI thread since the latter is time consuming. I have read up on reasons for main UI thread blocking, and I think it might be one of 2 things:

  1. I'm using the global declared scanLibraryReference variable to use the library, and even tough I use it in a task, it might block the main UI thread while using it since the variable is declared on it.
  2. All ScanLib methods, according to documentation can throw many errors which come in numbers (error 1001,1002,1003 etc.), and to simplify error logging the documentation asked me to declare about +100 constants similar to: public const int SCANLIB_ERR_SCANFAILED = 1001;. All these "error constants" are declared in another file of type public partial class of MainForm, so maybe usage of these constants from another task (thread) might freeze the main UI thread

Those are my main suspects, but having said that, you would think I would have solved it, but I haven't, and that's where my question lies: I will constantly need a ScanLib reference throughout the program's duration, but if I create it on the main UI thread, it gets blocked. I could try to create a new task like: Task backgroundWorker = Task.Run(() => { ScanLib scanLibRef = new ScanLib(); scanLibRef.InitializeLibrary() } ); But it is my understanding this variable will now live on this thread, and can't be used from another, or can it? Or even If I create a simple thread to just house the variable, that thread will die once it is done declaring a variable. I've tought about just doing this with Thread functions, but then a problem comes of how to call it back into action when I press a button, and feed it a function to run the scanner. Can anyone suggest a solution on how can I declare a global variable I need to constantly use without blocking the main UI thread?

Full code requested (Sorry if it's long, all the Console.WriteLines are used for debugging)

    using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using ScannerTest;
using System.Threading;

namespace ScannerTest
{
    public partial class Form1 : Form
    {
        // All 4 variables below are global and used extensively to call scanner methods
        // Main Scan basic functions Library
        NetScanW.SLibEx scanSlibxEx = new NetScanW.SLibEx();
        // Main Scan extended functios Library
        NetScanW.CImage scanCImage = new NetScanW.CImage();
        // Main Scan OCR functions library
        NetScanW.IdData scanIdData = new NetScanW.IdData();
        // Main Scan extended functions 2 library
        NetScanWex.SLibEx scanWexSlibxEx = new NetScanWex.SLibEx();
        string ImageSource = @"C:\Scans\";

        public Form1()
        {
            Task initTask = Task.Run(() => InitScanLibraries());
            InitializeComponent();
            Console.WriteLine("\nForm initialized succesfully.");
        }

        #region Button events

        // The normal way of scanning which of course blocks the main UI thread
        private void ScanCardNonAsync_Click(object sender, EventArgs e)
        {
            Console.WriteLine("");
            Console.WriteLine("*********************");
            Console.WriteLine("Starting new scan...");
            string currentScan = ImageSource + "MyScan.bmp";
            string modifiedScan = ImageSource + "MyEditedScan.bmp";
            ScanCard(currentScan, modifiedScan);
            OCRscan();
            GetOCRData();
            Console.WriteLine("Scan finalized..");
            Console.WriteLine("*********************");
        }

        // My attempt at scanning asynchronously which still blocks the UI thread
        private void ScanCardAsync_Click(object sender, EventArgs e)
        {
            Console.WriteLine("");
            Console.WriteLine("*********************");
            Console.WriteLine("Starting new scan...");
            string currentScan = ImageSource + "MyScan.bmp";
            string modifiedScan = ImageSource + "MyEditedScan.bmp";
            // Here I chain the methods in a task chain to scan a Card
            Task ScannerStart = new Task(() => ScanCard(currentScan, modifiedScan));
            Task ScannerStep2 = ScannerStart.ContinueWith((x) => OCRscan());
            Task ScannerStep3 = ScannerStep2.ContinueWith((y) => GetOCRData());
            ScannerStart.Start();
        }

        #endregion

        #region Methods

        private void InitScanLibraries()
        {
            switch (scanSlibxEx.InitLibrary("49B2MFWE8WUJXLBW"))
            {
                case SLIB_ERR_SCANNER_BUSSY:
                    System.Console.WriteLine("ERROR: Scanner Busy...");
                    break;
                case LICENSE_VALID:
                    System.Console.WriteLine("");
                    System.Console.WriteLine("**********************************");
                    System.Console.WriteLine("SlibxEx initialized succesfully...");
                    break;
                case LICENSE_INVALID:
                    System.Console.WriteLine("ERROR: License Invalid");
                    break;
                case LICENSE_EXPIRED:
                    System.Console.WriteLine("ERROR: License Expired");
                    break;
                case SLIB_ERR_DRIVER_NOT_FOUND:
                    System.Console.WriteLine("ERROR: Driver not found");
                    break;
                case SLIB_ERR_SCANNER_NOT_FOUND:
                    System.Console.WriteLine("ERROR: Scanner not found");
                    break;
            }

            switch (scanIdData.InitLibrary("49B2MFWE8WUJXLBW"))
            {
                case SLIB_ERR_SCANNER_BUSSY:
                    System.Console.WriteLine("ERROR: Scanner Busy...");
                    break;
                case LICENSE_VALID:
                    System.Console.WriteLine("License validation succesful...");
                    break;
                case LICENSE_INVALID:
                    System.Console.WriteLine("ERROR: License Invalid");
                    break;
                case LICENSE_EXPIRED:
                    System.Console.WriteLine("ERROR: License Expired");
                    break;
                case SLIB_ERR_DRIVER_NOT_FOUND:
                    System.Console.WriteLine("ERROR: Driver not found");
                    break;
                case SLIB_ERR_SCANNER_NOT_FOUND:
                    System.Console.WriteLine("ERROR: Scanner not found");
                    break;
                case GENERAL_ERR_PLUG_NOT_FOUND:
                    System.Console.WriteLine("ERROR: Attatched scanner is not one of the following:\n ScanShell 600 \n ScanShell 800 \n ScanShell1000");
                    break;
                case SLIB_LIBRARY_ALREADY_INITIALIZED:
                    System.Console.WriteLine("ERROR: Call ignored, library already initialized");
                    break;
            }

        }

        private void ScanCard(string ImagePath, string ModifiedImagePath)
        {
            Console.WriteLine("Attempting scan...");

            switch (scanSlibxEx.ScanToFile(ImagePath))
            {
                case SLIB_ERR_NONE:
                    Console.WriteLine("Scan succesful...");
                    break;
                case SLIB_ERR_SCANNER_BUSSY:
                    Console.WriteLine("ERROR: Scanner is busy...");
                    break;
                case LICENSE_INVALID:
                    Console.WriteLine("ERROR: License invalid");
                    break;
                case SLIB_ERR_SCANNER_NOT_FOUND:
                    Console.WriteLine("ERROR: Scanner not found");
                    break;
                case SLIB_ERR_SCANNER_GENERAL_FAIL:
                    Console.WriteLine("ERROR: Scanner general fail");
                    break;
                case SLIB_ERR_HARDWARE_ERROR:
                    Console.WriteLine("ERROR: Hardware error");
                    break;
                case SLIB_ERR_PAPER_FED_ERROR:
                    Console.WriteLine("ERROR: Paper fed error");
                    break;
                case SLIB_ERR_SCANABORT:
                    Console.WriteLine("ERROR: Scan aborted");
                    break;
                case SLIB_ERR_NO_PAPER:
                    Console.WriteLine("ERROR: No paper");
                    break;
                case SLIB_ERR_PAPER_JAM:
                    Console.WriteLine("ERROR: Paper jammed");
                    break;
                case SLIB_ERR_FILE_IO_ERROR:
                    Console.WriteLine("ERROR: File I/O error");
                    break;
                case SLIB_ERR_PRINTER_PORT_USED:
                    Console.WriteLine("ERROR: Printer port used");
                    break;
                case SLIB_ERR_OUT_OF_MEMORY:
                    Console.WriteLine("ERROR: Out of memory");
                    break;
            }
            //scanCImage.RotateImage(ImageSource, 90, 1, ModifiedImagePath);

        }

        private void OCRscan()
        {
            Console.WriteLine("Attempting OCR extraction...");
            string data = "";
            int region = scanIdData.AutoDetectState(data);
            // Check for card region
            switch (region)
            {
                case ID_ERR_USA_TEMPLATES_NOT_FOUND:
                    Console.WriteLine("ERROR: No USA templates found");
                    break;
                case INVALID_INTERNAL_IMAGE:
                    Console.WriteLine("ERROR: No internal image loaded");
                    break;
                case ID_ERR_STATE_NOT_SUPORTED:
                    Console.WriteLine("ERROR: State not supported");
                    break;
                case ID_ERR_STATE_NOT_RECOGNIZED:
                    Console.WriteLine("ERROR: State not recognized");
                    break;
                default:
                    Console.WriteLine("Region catch succesful");
                    break;
            }

            // Begin OCR extraction
            string data2 = "";
            Console.WriteLine("Attempting data extraction...");
            switch (scanIdData.ProcState(data2, region))
            {
                case ID_TRUE:
                    Console.WriteLine("Data extraction succesful.");
                    break;
                case LICENSE_INVALID:
                    Console.WriteLine("ERROR: LICENSE_INVALID");
                    break;
                case SLIB_ERR_SCANNER_NOT_FOUND:
                    Console.WriteLine("ERROR: SLIB_ERR_SCANNER_NOT_FOUND. ");
                    break;
                case SLIB_ERR_INVALID_SCANNER:
                    Console.WriteLine("ERROR: SLIB_ERR_INVALID_SCANNER. ");
                    break;
                case ID_ERR_STATE_NOT_SUPORTED:
                    Console.WriteLine("ERROR: ID_ERR_STATE_NOT_SUPORTED. ");
                    break;
                case INVALID_INTERNAL_IMAGE:
                    Console.WriteLine("ERROR: INVALID_INTERNAL_IMAGE. ");
                    break;
                default:
                    Console.WriteLine("ERROR: Uncatched exception in Form1.OCRScan()");
                    break;
            }
            // Data copying to local
            Console.WriteLine("Copying data locally...");
            if (scanIdData.RefreshData() != 0) Console.WriteLine("Data copied succesfully."); else Console.WriteLine("ERROR: Problem while copying data");
        }

        private void GetOCRData()
        {
            //loc* Variables are locally declared global variables, while the scanIdData.* are library variables where OCR scan results are saved
            Console.WriteLine("Saving data locally...");
            locName = scanIdData.Name;
            locNameFirst = scanIdData.NameFirst;
            locNameMiddle = scanIdData.NameMiddle;
            locNameLast = scanIdData.NameLast;
            locNameSuffix = scanIdData.NameSuffix;
            locID = scanIdData.Id;
            locLicense = scanIdData.license;
            locIssueDate = scanIdData.IssueDate;
            locAddress = scanIdData.Address;
            locExperationDate = scanIdData.ExpirationDate;
            locCSC = scanIdData.CSC;
            locCity = scanIdData.City;
            locEyes = scanIdData.Eyes;
            locDup_Test = scanIdData.Dup_Test;
            locState = scanIdData.State;
            locHair = scanIdData.Hair;
            locEndorsements = scanIdData.Endorsements;
            locZip = scanIdData.Zip;
            locHeight = scanIdData.Height;
            locFee = scanIdData.Fee;
            locCounty = scanIdData.County;
            locClass = scanIdData.Class;
            locRestriction = scanIdData.Restriction;
            locDateOfBirth = scanIdData.DateOfBirth;
            locSex = scanIdData.Sex;
            locSigNum = scanIdData.SigNum;
            locType = scanIdData.Type;
            locWeight = scanIdData.Weight;
            locAddress2 = scanIdData.Address2;
            locAddress3 = scanIdData.Address3;
            locAddress4 = scanIdData.Address4;
            locAddress5 = scanIdData.Address5;
            locText1 = scanIdData.Text1;
            locText2 = scanIdData.Text2;
            locText3 = scanIdData.Text3;
            Console.WriteLine("Data saved succesfully.");
        }

        #endregion
    }
}

EDIT

I did what Onur suggested and yes, what's blocking the main UI thread is the global variables scan Lib. I ran the following code and it didn't freeze the main UI thread:

Task debugTask = Task.Run(() =>
        {
            // All 4 variables below are global and used extensively to call scanner methods
            // Main Scan basic functions Library
            NetScanW.SLibEx scanSlibxEx = new NetScanW.SLibEx();
            // Main Scan extended functios Library
            NetScanW.CImage scanCImage = new NetScanW.CImage();
            // Main Scan OCR functions library
            NetScanW.IdData scanIdData = new NetScanW.IdData();
            // Main Scan extended functions 2 library
            NetScanWex.SLibEx scanWexSlibxEx = new NetScanWex.SLibEx();
            string ImageSource = @"C:\Scans\";
            string currentScan = ImageSource + "MyScan.bmp";
            string modifiedScan = ImageSource + "MyEditedScan.bmp";
            InitScanLibraries(scanSlibxEx, scanIdData);
            ScanCard(currentScan, modifiedScan, scanSlibxEx);
        });

Yes, it's very, very messy, but it worked and didn't freeze. All I did was declare the global variables, initialize the libraries AND run the scan in the same thread and of course, it didn't freeze the main UI thread, but it's far from what I want. I need the libraries to stay initialized, running in a secondary thread, and when I need to scan something, let it call the ScanLib method from the ScanLib reference variable which I'm still stumped since I don't know where to put it so it doesn't block the main UI thread. I will try the answer below from Onur and see what happens.

FINAL EDIT

Just to finalize on my question, I would like to add the solved code in case anybody else needs it. Accordingly to Orun's answer, I, instead of declaring the global variables at the top like ScanLib refScanLib = new ScanLib(), I declared them as null objects like so: ScanLib refScanLib = null, and in the Form constructor, I added a new method called InitializeVariables() that does the following:

public void InitializeVariables()
    {
        NetScanW.SLibEx scanSLibExx = null;
        NetScanW.IdData scanIdDataa = null;
        NetScanW.CImage scanCImagee = null;
        NetScanWex.SLibEx scanWexSLibExx = null;
        var th = new Thread(() => 
        {
            scanSLibExx = new NetScanW.SLibEx();
            scanIdDataa = new NetScanW.IdData();
            scanCImagee = new NetScanW.CImage();
            scanWexSLibExx = new NetScanWex.SLibEx();
        });
        th.SetApartmentState(ApartmentState.MTA);
        th.Start();
        th.Join();
        this.scanSlibxEx = scanSLibExx;
        this.scanIdData = scanIdDataa;
        this.scanCImage = scanCImagee;
        this.scanWexSlibxEx = scanWexSLibExx;
    }

After this, everything worked wonderfully. I've yet to understand it fully, but it works, and thank you guys for all the help.

sgarcia.dev
  • 5,671
  • 14
  • 46
  • 80
  • Globally declared variables shouldn't be the problem, it doesn't matter if they were declared on the UI thread. I think we're missing more of your code, since it looks fine from here. Is there anything else going on in the background that you haven't posted? Maybe post what your `ScanCard` method is doing – Yuval Itzchakov Jul 31 '14 at 16:41
  • I simplified the code the most I could since I didn't want to confuse everyone. most of the ScanLib methods are hidden so all I can do is feed it data. I'll post the full code in a second below for more extensive context – sgarcia.dev Jul 31 '14 at 16:43
  • 1
    Pause the debugger while the UI is blocked. The current stack shows you who is blocking the UI. – usr Jul 31 '14 at 16:49
  • Any idea what window/toolbar displays this? – sgarcia.dev Jul 31 '14 at 16:53
  • Just out of curiosity: how do you get the result of the scan ? – Thierry Jul 31 '14 at 16:55
  • The GetOCRData() does this. OCR scan results are saved in the Scanner Library's internal data structure, all I do is copy them from those properties, to my local Variables like shown: locName is my local variable, while ScannerLibrary.Name is the name stored on the scanner's data structure. This data is overwritten every time I scan something else. – sgarcia.dev Jul 31 '14 at 16:58
  • 1
    Once I had a similar issue with a library using `COM`. The problem was that since the `scanLibraryReference` was created on the UI thread, the stuff related to `COM` was somehow bound to the UI thread. Creating the `scanLibraryReference` in a different thread/task may help. I also had to add the `[STAThread]` attribute to the `Program.Main` method. – Onur Jul 31 '14 at 17:04
  • I thought about that, but you may be right, Going to do that right now, altough I don't know yet what will happens since I thought variables declared in a thread, stay in a thread. Will post results in about 2 minutes. – sgarcia.dev Jul 31 '14 at 17:07
  • It's hard to understand why it is blocking based on the information you've sent. Maybe when you are updating something in the screen with the result of the scan, it can be blocked, or a Thread.Sleep forgotten in some place. Another very strange situation is if the scanner lib posts some messages to your main window and it's causing the UI to block. To debug this, I suggest you override the WndProc method of your main form and check if any weird messages are posted to the message loop while scanning. – Eric Lemes Jul 31 '14 at 17:09
  • This is surely an apartment-threaded COM server. An expensive word that means that the library is not thread-safe. COM takes care of marshaling the call back to the thread that created the object, to ensure it is thread-safe. So your call actually runs on the UI thread and blocks your UI. The Task class is *very* incompatible with that, you'll need to give the object a safe home, a separate STA thread that runs for the life of the object. The code in [this answer](http://stackoverflow.com/a/21684059/17034) can provide one. – Hans Passant Jul 31 '14 at 17:17
  • Wow, I had no idea about any of that...To be honest your comment kinda scared me Hans, but I'll read up and see what works, I'm still kind of new to COM objects so this is all kind of new to me. I'll try what Onur suggested below as an answer and hopefully that will do something. Thank you for your code example, that will help greatly. If it does, I'll let you know so you can post it as an answer to claim Accepted answer. Be right back, trying it. – sgarcia.dev Jul 31 '14 at 17:28

1 Answers1

2

This is the thing that worked for me in a similar case when using a COM library.

internal static class Program
{
    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    [STAThread]            // <= I needed to add this attribute
    private static void Main()
    {
       //...
    }
}




public partial class MainForm : Form () 
{    
    // you can call this in the InitializeComponents() for instance
    void someMethodInYourFormIERunningOnTheUIThread()
    {
        ScanLib scanLib  = null;
        var th = new Thread(() =>
        {
            scanLib = new ScanLib();
        });
        th.SetApartmentState(ApartmentState.MTA); // <== this prevented binding the UI thread for some operations
        th.Start();
        th.Join();
        this.scanLibraryReference = scanLib;
    }
    //...
}
Onur
  • 5,017
  • 5
  • 38
  • 54
  • I did what you suggested previously, and edited my post with the test results. Yes, you were right, what was blocking the UI thread was those global variables, and although putting it all in a new Task to demonstrate, it still makes me wonder how can I keep it alive to access those variables. Either way, i will try this right now and hopefully see what happens, I have no idea what STAthread or SetApartmentState() does, but let's see what I learn by example. Thank you. – sgarcia.dev Jul 31 '14 at 17:24
  • Wow...it worked...I still can't believe it. I have no idea what SetApartment or STAthread even did tough, but I'll be sure to read up heavilly on that if I'm going to continue working with COM objects. Thank you very much for your help. – sgarcia.dev Jul 31 '14 at 17:46
  • As far as I remember they determine among others how synchronization is achieved. – Onur Jul 31 '14 at 18:02