Dispatcher.InvokeAsync() locks up the GUI until task is finished - multithreading

I have a control (type DataGrid) that holds large amount of data (up to 1M rows). I implemented a copy-to-clipboard method that dumps the entire contents to the clipboard. This usually takes up to 2 to 3 minutes.
To avoid my users waiting for the process, I'd like to use another thread to process the copy method and return the control back to GUI immediately so the users can proceed with other tasks. When the copy method is complete, then pop up a message box to inform the user.
However, when the code is executed, the GUI gets locked up and does not responds to any user actions until the entire copy method is completed. I wonder where I got it wrong.
Here is the code snappet:
private void Copy_To_Clipboard()
{
this.gridView.ClipboardCopyMode = DataGridClipboardCopyMode.IncludeHeader;
DataGridSelectionUnit u = this.gridView.SelectionUnit;
this.gridView.SelectionUnit = DataGridSelectionUnit.CellOrRowHeader;
this.gridView.SelectAll();
ApplicationCommands.Copy.Execute(null, this.gridView);
this.gridView.UnselectAll();
this.gridView.SelectionUnit = u;
}
private async void cmdClipboard_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("The data is being copied to clipboard. This can take a while as there are " +
this.NumRecords.ToString("###,###,##0") + " records to be copied. You will be notified when it’s finished",
"Copy to Clipboard", MessageBoxButton.OK, MessageBoxImage.Information);
await this.Dispatcher.InvokeAsync((Action)(() =>
{
Copy_To_Clipboard();
}));
MessageBox.Show("Copy to clipboard is completed.", "Copy to Clipboard", MessageBoxButton.OK, MessageBoxImage.Information);
}

InvokeAsync doesn't mean "run this code on a background thread"; it means "run this code on the UI thread". Since the code calling InvokeAsync is already on the UI thread, that call does exactly nothing helpful.
The problem is that UI operations must run on the UI thread. This includes reading all the grid values and writing to the clipboard (which is considered a UI object on Windows).

Related

Why DoModal() on object of CFileDialog crashes on second call?

happens only if I have filter whose string is loaded from an resource ID.
e.g.
CString szFilter;
szFilter.LoadString(IDC_ALLFILES);
where IDC_ALLFILES = "All files (*.*)|*.*||"
when I try to do DoModal() on same instance of CFileDialaog, it crashes on second time.
I have created a small sample project to simulate the exact behavior.
First thing I have done is declared a CFileDialog pointer as follows:
class CFeatureDialogFileDlg : public CDialog
{
private:
CFileDialog* m_pFileDialog;
}
I have two buttons 'Set Flags' and 'Open features' as follows:
void CFeatureDialogFileDlg::OnBnClickedButtonSetFlags()
{
static CString szFilter;
szFilter.LoadStringW(IDC_ALLFILES);
m_pFileDialog = new CFileDialog(TRUE,NULL,NULL,OFN_HIDEREADONLY | OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR,szFilter);
}
void CFeatureDialogFileDlg::OnBnClickedButtonOpenFeatures()
{
if(m_pFileDialog->DoModal() == IDOK){}
}
Now,
I just click 'Set Flags' to create a new object on heap.
then I click on 'Open Features' to call DoModal().
First time it gets called properly.
But second time when I click 'Open Features' without clicking on 'Set Flags', I get an error dialog "Debug Assertion Failed in file C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\atlmfc\src\mfc\dlgfile.cpp"
if I click 'ignore' I get "Encountered an improper argument" dialog.
Thank you all for your replies.
I have recognized the cause of the problem.
In mfc 9, two extra parameters were introduced i.e. dwSize and bVistaStyle for CFileDialog.
Because of bVistaStyle = TRUE, we call new Vista style dialog box and multiple calls to CFileDialog::DoModal for the same instance of a CFileDialog generates ASSERT.
Below line gives E_UNEXPECTED on second time call to DoModal()
HRESULT hr;
hr = (static_cast(m_pIFileDialog))->SetFileTypes(nFilterCount, pFilter);
from file dlgfile.cpp which is at location C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\atlmfc\src\mfc
Explanation can be found on https://msdn.microsoft.com/da-dk/library/dk77e5e7(v=vs.90).aspx in Note section.
Possible solutions are:
Use Old style dialog box by changing the default parameter bVistaStyle = FALSE
Create a new dialog each time and delete it.
We can not call DoModal() multile times if bVistaStyle = TRUE

Advanced Edit of TDocument locks up the UI

My Caliburn Micro UI gets locked up because of a rather advanced set of controls contained in a TDocument. I have tried several different Async approaches to move the activity to another thread, but without success. Here is a simplified view of the code. You may recognize it because it is taken from the Hello Screens sample application.
To see briefly how the Document Conductor Works, here is the interface:
Public MustInherit Class DocumentWorkspace(Of TDocument As {Class, INotifyPropertyChanged, IDeactivate, IHaveDisplayName})
Inherits Conductor(Of TDocument).Collection.OneActive
Implements IDocumentWorkspace
Protected Sub New()
Public MustOverride ReadOnly Property Icon As String Implements IWorkspace.Icon
Public MustOverride Property IconName As String Implements IWorkspace.IconName
Public Property PixelLabTransitions As BindableCollection(Of Transition)
Public Property ScreenTransition As Transition
Public Property State As DocumentWorkspaceState
Public Property Status As String Implements IWorkspace.Status
Protected ReadOnly Property Conductor As IConductor
Public Overrides Sub ActivateItem(item As TDocument)
Public Sub Edit(child As TDocument)
Public Sub Hide()
Public Sub Show() Implements IWorkspace.Show
End Class
Here is the offending code:
_SelectedDesignerElement = value
'adjust Count located next to Icon
vm.DisplayName = value.DesignerDisplayText
count += 1
vm.IsDirty = True
'the next line of code works but
'disables the UI for a long time
Edit(vm)
So the simplest way I can show the problem is to try to move this long activity to another thread:
'Plan to show a Busy indicator here
'Below I have tried to move the edit to another thread
'but this simply does not work
Dim t As Task = Task.Factory.StartNew(Sub()
Edit(vm)
End Sub)
t.Wait()
'Plan to remove Busy indicator here
Does anyone have a better idea how to free up the UI for this long winded process?
BTW The problem is clearly the fact that the Edit(VM) is not happy on another thread, because I tested the same approach with a Busy indicator using just a counter to create a delay and the Start Busy / End Busy work just fine and the UI remains responsive.
The epiphany happened after I awoke in the middle of the night.
The Hello Screens Project was imported into a current version of Visual Studio (2015) but it never dawned on me that the targeted Framework would remain the same as the original project. So the Framework was pointing to a version prior to Async and Await support.
I have completed adding a Public Domain Busy Indicator to the Caliburn.Micro Hello Screens sample project. There are several different situations where you may incur the need for a Busy indicator and there are several different ways to activate the control. For example I found that the place to Activate Busy in my original question is: in the DocumentWorkspace as shown here.
Public Async Sub ShowAsync() Implements HelloScreensWPF.Framework.IWorkspace.ShowAsync
'Notice that this Sub is Async and the Name is changed from Show (In the CM Sample) to Show Async
'Not sure this is the very best way but it works pretty good
'Anytime the user clicks an icon at the bottom of the UI this sub is called
Dim haveActive = TryCast(Parent, IHaveActiveItem)
If haveActive IsNot Nothing AndAlso haveActive.ActiveItem Is Me Then
DisplayName = IconName
State = DocumentWorkspaceState.Master
Else
'We need to have access to the ShellViewModel
Dim Svm = CType(Parent, ShellViewModel)
'Activate the Busy indicator
'The BusyIndicatorViewModel has a built in '200 µ Sec Delay
'So if it is Deactivated before it 'fires' the user will not see a
'flash of an unnecessary Busy Indicator
Svm.ShellBusyIndicator.ShowBusyIndicator(True, True, "Loading", IconName)
'You will need to push the conductor ActivateItem off to another thread for a real application and
'get rid of the Sample Task.Delay here
Await Task.Delay(4000)
Conductor.ActivateItem(Me)
Dim list = CType(Parent, IConductor).GetChildren
'Deactivate Busy Indicator
Svm.ShellBusyIndicator.ShowBusyIndicator(False)
End If
End Sub
If anyone would like a copy of the HelloScreens with Busy Solution, just send me an email ranck at aqsi.net with an #.

How to set the one section timer to the Dialog derived from CProperty sheet using VC++ 2005

Anyone, could you please help me in display in the One sec timer, this my context:
I derived a class from CPropertySheet. Now the thing is that I want to display the current time in the sheet. So for normal dialogs I'm using ON Timer function to set the one section timer here how can I set the one second timer
Thanks to all ,
I found the Solution for the above problem. this is related to One Second timer for CProperty Sheet.
I Derived Class CReview Sheet so for that I declare a member function OnTimer
void CReviewSheet::OnTimer(UINT_PTR nIDEvent)
{
if(bTimerStatus == true)
{
CTime t1;
t1=CTime::GetCurrentTime();
m_StBar.SetPaneText(2,t1.Format("%H:%M:%S"));
bTimerStatus = false;
}
else
{
bTimerStatus = true;
}
CPropertySheet::OnTimer(nIDEvent);
}
So this is working fine. And I am able to display the Current time in my review Sheet.

Calling a computationally intensive routine from VBA without stalling Excel GUI

I have a set of numerically intensive routines (each takes up to 1 minute to complete) bundled in a COM object, implementing IDispatch cleanly.
I can therefore use them from an Excel worksheet, those routines will be called by VBA macros triggered by buttons.
Now, when one of these routines is called, the Excel user interface is frozen, which is quite uncomfortable for the end users of the sheet.
I'd like to find any mechanism to alleviate this problem.
This could be for instance launching the computation in another thread launched on the COM side, returning immediately, the spawned thread calling back a VBA procedure when results are computed.
Or something simpler, since I only need one computation to be performed at a time.
Now, there may be a lot of issues with calling VBA routines from other threads. I must confess that I am not that experienced with COM, that I only treat as a black box between my code and Excel (I use ATL).
So,
Is it possible to call back VBA routines from another thread ?
Is there a better way to do what I want to achieve ?
UPDATE
After weighing the options and reading a lot of stuff on the internet, I will do cooperative multithreading: in the COM object, instead of having one routine, I shall have three:
class CMyObject : ...
{
...
STDMETHOD(ComputationLaunch)(...); // Spawn a thread and return immediately
STDMETHOD(ComputationQuery)(DOUBLE* progress, BOOL* finished);
STDMETHOD(ComputationResult)(VARIANT* out);
private:
bool finished, progress;
boost::mutex finished_lock, progress_lock;
ResultObject result; // This will be marshaled to out
// when calling ComputationResult
};
And in the VBA:
Private computeActive as Boolean ' Poor man's lock
Public Sub Compute()
OnError GoTo ErrHandler:
If computeActive Then Exit Sub
computeActive = True
Dim o as MyObject
call o.ComputationLaunch
Dim finished as Boolean, progress as Double
While Not o.ComputationQuery(progress)
DoEvents
' Good place also to update a progress display
End While
Dim result as Variant
result = o.ComputationResult
' Do Something with result (eg. display it somewhere)
computeActive = False
Exit Sub
ErrHandler:
computeActive = False
Call HandleErrors
End Sub
Indeed, by doing a depth-first-search on the internet for COM Add-Ins, I realized that VBA macros run in the same event loop as Excel's GUI, that you have the DoEvents facility, and that it is not safe (or at least very tricky) to call back VBA procedures from other threads. This would require eg. tricking the Accesibility facilities to obtain a synchronized handle to an Excel.Application object, and call the OnTime method to set up an asynchronous event handler. Not worth the trouble.
If you want to do this well you need to give up on VBA and write a COM add-in.
Posting my comment as an answer...
You could implement an event in your COM object and have it call back when done. See http://www.dailydoseofexcel.com/archives/2006/10/09/async-xmlhttp-calls/ for an example of how to run a COM object asynchronously.
My dirty hack is: create a new instance of Excel, run the code there.
Another option is to schedule the run for later, have the user say when. (In the example below, I've just hard-coded 5 seconds.) This will still freeze the user interface, but at a scheduled, later time.
Sub ScheduleIt()
Application.OnTime Now + TimeValue("00:00:05"), "DoStuff"
End Sub
Sub DoStuff()
Dim d As Double
Dim i As Long
d = 1.23E+302
For i = 1 To 10000000#
' This loop takes a long time (several seconds).
d = Sqr(d)
Next i
MsgBox "done!"
End Sub

Error on Excel importing

I am trying to import around 1500 Excel files to my system.
The code is working in a loop and I am able open and import around 600 Excel files. After that I am getting an error message like: Error calling external object function open at line 55.....
I really stuck with this issue, if anyone can help that will be grateful.
Code posted in reply comments:
For ll_LoopCnt = 1 To Dw_1.rowcount( )
Ls_File_Name = Dw_1.getitemstring( ll_LoopCnt, "file_name")
Ls_Path =Dw_1.getitemstring( ll_LoopCnt, "file_path")
ll_Sr_No= Dw_1.getitemNumber( ll_LoopCnt, "sr_no")
ldt_File_Date= Dw_1.getitemDateTime( ll_LoopCnt, "file_date")
Excel.Application.DisplayAlerts = "False"
Excel.WorkBooks.Open( Ls_Path )
Excel.Application.Visible = False
Excel.windowstate = 2 // 1 : Normal, 2 : Minimize, 3 : Maximize
Excel.Application.CutCopyMode = False
Lb_sheet_rtn = excel.worksheets(7).Activate
Ls_ClipBoard = clipboard()
Excel.Application.ActiveWorkbook.Save()
Excel.Worksheets(7).UsedRange.Copy
ll_cnt = ds_1.importclipboard()
IF ll_cnt <= 1 THEN
Messagebox("Error", "Could not find.")
Else
Dw_1.Scrolltorow( ll_LoopCnt )
Dw_1.SetItem( ll_LoopCnt, "status", 'Success')
For ll_Inner_LoopCnt = 1 To Ds_1.RowCount( )
Ds_1.Object.file_path[ll_Inner_LoopCnt] = Ls_Path
Ds_1.Object.file_name[ll_Inner_LoopCnt] = Ls_File_Name
Ds_1.Object.file_sr_no[ll_Inner_LoopCnt] = ll_Sr_No
Ds_1.Object.file_date[ll_Inner_LoopCnt] = ldt_File_Date
Next
END IF
Clipboard(ls_ClipBoard)
Ds_1.Reset( ) //Reset the data store
Excel.Application.ActiveWorkbook.Save()
Excel.Application.ActiveWorkbook.Close(False);
Excel.Application.Quit
Excel.Application.CutCopyMode = False
IF ll_LoopCnt = ll_Excel_Cnt Then //--->> After 100 files reset the memmory
ll_Excel_Cnt = ll_LoopCnt + 100
Excel.DisConnectObject()
DESTROY excel
DESTROY TEst_Excel
GarbageCollect ( )
Excel = Create OLEObject
Test_Excel = Create OLEObject
Li_rtn = excel.ConnectToNewObject("excel.application")
IF li_rtn <> 0 THEN
MessageBox('Excel error','can not run Excel Program')
DESTROY Excel
RETURN 0
END IF
End IF
Next
Excel.displayalerts = False
Excel.Application.Quit
Excel.displayalerts = True
Excel.DisConnectObject()
DESTROY Excel
DESTROY Test_Excel /* This is the code i written i dont think the OLE is crashing i think the connnectto the OLE is getting lost after some time, but stile its going fine for almost 600 records.. */
Seeing the line of code would help, but this error message typically (in the context of OLE, which I'm guessing is the case here) comes from PowerBuilder making an OLE call which the OLE host rejects. From the information you've supplied, it's impossible to tell if the OLE host has crashed and isn't responding anymore, or if you've got the OLE host into a state where these functions are no longer applicable, or if the OLE object has become invalid, or what.
If it were me, and it was happening consistently, I'd run the app in the debugger to get to the state where the error is about to happen (you can set advanced attributes in breakpoints to not have a breakpoint activate every time it is passed) and try interrogating the OLE objects. I'd expect you'd also have to throw in some test code, since I'm not confident everything you'd want to test would be available to the debugger.
New Feb 21
I'd also change the set of files being processed, so that I could tell if the key to the crash is a specific file, or the quantity of files processed. For example, if you get rid of the first 100 files, does it still crash on the 600th file (same quantity) or the 500th file (same file)?
One possibility is that you're running out of memory. Each "dot" in an OLE reference (attribute access, method call) creates an object in memory that isn't destroyed until the garbage collect. The code clip you've posted will never enter the block where the GarbageCollect() is called (ll_Excel_Cnt is never initialized), so you might want to make sure that part is working. I'd also get rid of unnecessary calls. For example, you've got several calls that maintain the state of Excel within the loop (e.g. Excel.Application.Visible), when they only need to be called once. I'm also not clear from the code clip why you'd need to call a Save(), either time; this could be expendable as well. This clean up should also make your code run faster.
Good luck,
Terry
Around line 30 you have
Excel.Application.ActiveWorkbook.Save()
Excel.Application.ActiveWorkbook.Close(False);
Excel.Application.Quit
You shouldn't call Excel.Application.Quit there. Also, I always recommend to put anything that uses OLE inside a Try..Catch block and catch OleRuntimeError and RuntimeError.

Resources