Advanced Edit of TDocument locks up the UI - multithreading

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 #.

Related

Catch event trigger from external application in Excel VBA

I use VBA to automate an external application that recently changed their COM API. The new API loads files asynchronously (used to be synchronous) so I need to wait for the file loaded trigger before I continue when I try to load a file.
I have tried the methods listed on the Microsoft website (EX1, EX2) which were also part of an accepted answer on StackOverflow.
Below is the code I have in a class module named UCExternal to contain the external application object:
Public WithEvents obj As External.Application
Private fileLoaded As Boolean
Private Sub obj_OnFileLoaded(ByVal lLayer As Long, ByVal strUNCPath As String)
Debug.Print lLayer
Debug.Print strUNCPath
fileLoaded = True
End Sub
Public Sub LoadSingleFile(fileStr As String)
fileLoaded = False
obj.LoadFile 0, fileStr
Do
DoEvents
Loop Until fileLoaded
End Sub
And then this is what I had in a normal code module to run using a button on the sheet:
Sub TryLoadFile()
Dim extObj as New UCExternal
set extObj.obj = CreateObject("External.Application")
filePath = "path/to/file"
extObj.LoadSingleFile filePath
End Sub
The event code never seems to fire and instead the Do Loop just runs until Excel crashes. I don't know if there is a way to confirm the application actually sent the event trigger? I have read through the new documentation for the application and that is the event they say to wait for. I have reached out to them for help as well but I wasn't sure if there was something more general I may have been missing. I have not worked with events external to Excel in the past. If I just step through it using the debugger and manually exit the Do Loop eventually the rest of the code that works on the loaded file works as well, so it does load the file.
extObj needs to be declared outside of TryLoadFile, or it will go out of scope and get cleared as soon as TryLoadFile completes
Dim extObj as New UCExternal
Sub TryLoadFile()
Set extObj = New UCExternal
set extObj.obj = CreateObject("External.Application")
filePath = "path/to/file"
extObj.LoadSingleFile filePath
End Sub

Calling a VBA form from a button causes UserForm_Initialize to run twice, breaking my code?

Hello wonderful VBA community,
I'm still really new to vba and am trying to learn a lot. Thank you in advance for looking through my code and my description of the issue I'm facing.
I have a button on a page that calls a new Userform.
CODE SNIPPET 1:
Sub btnShowDetails_Click()
Call frmShowDeets.ShowDeets
End Sub
... which calls the next bit of code in the 'frmShowDeets' UserForm:
CODE SNIPPET 2:
Public Sub ShowDeets()
Dim frm As frmShowDeets
Set frm = New frmShowDeets 'this line triggers the Userform_Initialize() event below
frm.Show
End Sub
... triggering:
CODE SNIPPET 3:
Private Sub UserForm_Initialize()
Dim comboBoxItem As Range
For Each comboBoxItem In ContactList.Range("tblContactList[CompanyName]")
'^refers to unique values in a named range
With Me.boxCompanySelection
.AddItem comboBoxItem.Value
End With
Next comboBoxItem
End Sub
So at this point, the form I want to display has values loaded in its one combobox for user selection. The user selects a company and the Combobox_Change event triggers other routines that pull information for that company.
CODE SNIPPET 4:
Public Sub boxCompanySelection_Change()
Call frmShowDeets.PullData
End Sub
Sub PullData()
Dim numCompanies As Long
numCompanies = ContactList.Range("B6").Value 'this holds a count of the rows in the named range
Dim FoundCell As Range
Set FoundCell = ContactList.Range("tblContactList[Company Name]").Find(What:=boxCompanySelection.Text, LookIn:=xlValues, LookAt:=xlWhole)
Dim CompanyRow As Long
CompanyRow = FoundCell.Row
With ContactList
'pull a bunch of the company's details
End With
End Sub
Here is where it gets weird... Once the form is shown and the user selects one of the combo box items, triggering the Combobox_Change event the code breaks because the 'What:=boxCompanySelection.Text' part of the Range().Find method reads as "" empty (even though Code Snippet 3 is meant to load in company names and Code Snippet 4 is only triggered when the user selects one of those company names from the combobox) and I shouldn't need to build something to handle 'not found' exceptions since the only possible values should be the ones pulled in from my named range.
From stepping through the code, I have determined that for some reason, Code Snippets 2 and 3 run TWICE before Snippet 4 is run. Does anyone know what about my code is causing this to happen? I'm thinking there's a disconnect between the form that is shown and loaded with combobox values and whatever Code Snippet 4 is reading data from.
What is weirder is that if I run the code starting from Code Snippet 2 (ignoring the button call in Code Snippet 1), the form works as intended and from what I can tell 2 and 3 are only run once.
The problem is probably something simple I'm overlooking but I just cannot figure out what it is. Thanks again!
You have to understand that a form is an object - exactly as any other class module, except a form happens to have a designer and a base class, so UserForm1 inherits the members of the UserForm class.
A form also has a default instance, and a lot of tutorials just happily skip over that very important but rather technical bit, which takes us exactly here on Stack Overflow, with a bug involving global state accidentally stored on the default instance.
Call frmShowDeets.ShowDeets
Assuming frmShowDeets is the name of the form class, and assuming this is the first reference to that form that gets to run, then the UserForm_Initialize handler of the default instance runs when the . dot operator executes and dereferences the object. Then the ShowDeets method runs.
Public Sub ShowDeets()
Dim frm As frmShowDeets
Set frm = New frmShowDeets 'this line triggers the Userform_Initialize() event below
frm.Show
End Sub
That line triggers UserForm_Initialize on the local instance named frm - which is an entirely separate object, of the same class. The Initialize handler runs whenever an instance of a class is, well, initialized, i.e. created. The Terminate handler runs when that instance is destroyed.
So ShowDeets is acting as some kind of "factory method" that creates & shows a new instance of the frmShowDeets class/form - in other words whatever happened on the default instance is irrelevant beyond that point: the object you're working with exists in the ShowDeets scope, is named frm, and gets destroyed as soon as it goes out of scope.
Remove the ShowDeets method altogether. Replace this:
Call frmShowDeets.ShowDeets
With this:
With New frmShowDeets
.Show
End With
Now the Initialize handler no longer runs on the default instance.
What you want, is to avoid using the default instance at all. Replace all frmShowDeets in the form's code-behind, with Me (see Understanding 'Me' (no flowers, no bees)), so that no state ever accidentally gets stored in the default instance.
Call frmShowDeets.PullData
Becomes simply:
Call Me.PullData
Or even:
PullData
Since Call is never required anywhere, and the Me qualifier is always implicit when you make a member call in a class module's code.

VB6 Automation Error when calling Add on previously created MultiPage

I want to generate a bunch of MultiPages and create new Pages dynamically in my app, but i'm getting Run-time error '-2147417848 (80010108)': Automation error The object invoked has disconnected from its clients.
Steps to reproduce
In a Class Module named TestClass:
Public WithEvents TestMultiPage As MsForms.MultiPage
Sub createPage()
TestMultiPage.Add
End Sub
In a UserForm named TestForm:
Dim TestInstances as New Collection
Private Sub UserForm_MouseDown(ByVal Button As Integer, ByVal Shift As Integer, ByVal X as Single, ByVal Y as Single)
If Button = fmButtonRight Then
Dim TestInstance as New TestClass
Set TestInstance.TestMultiPage = Me.Controls.Add("Forms.MultiPage.1")
TestInstances.Add TestInstance
End If
End Sub
Private Sub UserForm_DblClick(ByVal Cancel As MSForms.ReturnBoolean)
Dim TestInstance As TestClass: Set TestInstance = TestInstances(1)
TestInstance.createPage
End Sub
When i right-click the UserForm twice, i get two MultiPages. Then i double-click the UserForm, expecting the first MultiPage to have a new Page. But i hit the automation error at TestInstance.createPage -> TestMultiPage.Add, even though all the variables seem present from the Locals window.
What am i missing?
Conclusion
Following #GSerg's answer, i suppose there's no way to do this with MultiPage.
Instead i have to use TabStrip instead and emulate the other behaviour of MultiPage.
Just to add some context, i was trying to create a browser-like UI with windows and tabs (a TabStrip at the bottom representing different windows, each window corresponding to a MultiPage with multiple tabs). I hit the obscure error when switching back to a previous MultiPage and creating a new tab.
There appears to be a problem in MSForms, where it cripples the existing MultiPage controls when a new one is added. To reproduce the problem, you don't need collections, arrays, classes, or even variables:
Sub Reproduce()
Me.Controls.Add "Forms.MultiPage.1", "TestInstance1"
Me.Controls("TestInstance1").Add ' That works
Me.Controls.Add "Forms.MultiPage.1", "TestInstance2"
Me.Controls("TestInstance1").Add ' Now it does not
Me.Controls("TestInstance2").Add ' But the new shiny one does
Me.Controls.Add "Forms.MultiPage.1", "TestInstance3"
Me.Controls("TestInstance2").Add ' Now the instance 2 is also defunct
Me.Controls("TestInstance3").Add ' Only the latest one works
End Sub
I do not know why that is so. It looks like a bug in MSForms.
The controls work fine otherwise, and their properties are accessible, you just can't call Add anymore.

VBA best practices for modules relative to modeless userforms

I came across this similar issue and read the replies: Modeless form that still pauses code execution
I have been attempting to apply in my own situation the suggestion provided by David Zemens. In my situation, I cannot seem to find an approach that incorporates Mr. Zemen's suggestion without also utilizing a GoTo.
I am wondering if there is a better or more elegant solution.
Here is an outline of what I am doing:
I have a UserForm with a Command Button that begins the code execution that will perform several actions on multiple Excel workbooks. As such, there are a number of blocks of code and the successful completion of one block of code allows for the execution of the subsequent block of code.
At a certain point, depending on the situation, the code might require User input; in other situations, the needed data is obtainable from an Excel. If input is needed from the User, another UserForm is displayed.
The User may need to view several different Excel sheets before entering the input, so the UserForm is modeless. So the code comes to a stop until the User enters the needed input and clicks another Command Button.
It is at this point I am having trouble: how to resume the program flow. Is the only way to 'pick-up where it left-off' is by using a GoTo statement? Or is there some way to organize the modules so there is a single consistent program flow, defined in one spot and not duplicated from the point at which User input might be needed?
Here is my take on the problem . Hope I understood the problem correctly.
Assumptions:
There are two user forms.
UserForm1 with a button to start the processing.
UserForm2 with a button to supply intermediate input.
A sub inside a module to start/ launch UserForm1.
VBA Code (for the sub routine)
Sub LaunchUserForm1()
Dim frm As New UserForm1
'/ Launch the main userform.
frm.Show vbModeless
End Sub
VBA Code (for UserForm1)
Private Sub cmdStart_Click()
Dim i As Long
Dim linc As Long
Dim bCancel As Boolean
Dim frm As UserForm2
'/ Prints 1 to 5 plus the value returned from UserForm2.
For i = 1 To 5
If i = 2 Then
Set frm = New UserForm2
'/ Launch supplementary form.
frm.Show vbModeless
'<< This is just a PoC. If you have large number of inputs, better way will be
' to create another prop such as Waiting(Boolean Type) and then manipulate it as and when User
' supplies valid input. Then validate the same in While loop>>
'/ Wait till we get the value from UserForm2.
'/ Or the User Cancels the Form with out any input.
Do While linc < 1 And (linc < 1 And bCancel = False)
linc = frm.Prop1
bCancel = frm.Cancel
DoEvents
Loop
Set frm = Nothing
End If
Debug.Print i + linc
Next
MsgBox "User Form1's ops finished."
End Sub
VBA Code (for UserForm2)
Dim m_Cancel As Boolean
Dim m_prop1 As Long
Public Property Let Prop1(lVal As Long)
m_prop1 = lVal
End Property
Public Property Get Prop1() As Long
Prop1 = m_prop1
End Property
Public Property Let Cancel(bVal As Boolean)
m_Cancel = bVal
End Property
Public Property Get Cancel() As Boolean
Cancel = m_Cancel
End Property
Private Sub cmdlinc_Click()
'/Set the Property Value to 10
Me.Prop1 = 10
Me.Hide
End Sub
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
'/ Diasble X button
Me.Cancel = True
Me.Hide
Cancel = True
End Sub
OK so here are my thoughts.
You have a userform frmSelectUpdateSheet which you wish to use in order to allow the user to select the sheet, when the sheet can't be determined programmatically. The problem is that if you do .Show vbModeless (which allows the user to navigate the worksheet/s), then code continues to execute which either leads to errors or otherwise undesired output.
I think it's possible to adapt the method I described in the previous answer. However, that's out of the question here unless you're paying me to reverse engineer all of your code :P
Assuming you have a Worksheet object variable (or a string representing the sheet name, etc.) which needs to be assigned at this point (and that this variable is Public in scope), just use the CommandButton on the form to assign this based on the selected item in the frmSelectUpdateSheet list box.
This is probably a superior approach for a number of reasons (not the least of which is trying to avoid application redesign for this sort of fringe case), such as:
This keeps your form vbModal, and does prevent the user from inadvertently tampering with the worksheet during the process, etc.
Using this approach, the thread remains with the vbModal displayed frmSelectUpdateSheet, and you rely on the form's event procedures for control of process flow/code execution.
It should be easier (and hence, cheaper) to implement; whether you're doing it yourself or outsourcing it.
It should be easier (and hence, cheaper) to maintain.
NOW, on closer inspection, it looks like you're already doing this sort of approach with the cmdbtnSelect_Click event handler, which leads me to believe there's a related/follow-up problem:
The sheet names (in listbox) are not sufficient for user to identify the correct worksheet. So if the user needs the ability to "scroll" the sheet (e.g., to review data which does not fit in the window, etc.), then add some spinner buttons or other form controls to allow them to navigate the sheet.

Excel VBA - on Multi Page Change but only once

I have a flexgrid within a Multipage under Main_Window.MultiPage2.Value = 2 this flexgrid has 8000 rows and I don't want those to load unless this page is actually clicked on. The code I have does just that, but the problem is is that it loads every single time and not just once. Is there a way to make it load on the first change, and then that's it?
Private Sub MultiPage2_Change()
If Main_Window.MultiPage2.Value = 2 Then
Call form_segment_carrier_auto
End If
End Sub
in form_segment_carrier_auto is a module that populates the flexgrid.
If I understand you correctly, you could declare a Public Boolean variable, for example:
Public ChangedOnce As Boolean
This should be in some standard code module.
Then change your event handler to:
Private Sub MultiPage2_Change()
If ChangedOnce Then Exit Sub
ChangedOnce = True
If Main_Window.MultiPage2.Value = 2 Then
Call form_segment_carrier_auto
End If
End Sub
The event handler will still be called on multiple occasions if the event occurs on multiple occasions, but only the first call will do anything.

Resources