Jump to content

Recommended Posts

Posted

Struggling with the same although this works a little better and without going to the api call. But still expect controls.add should do the job

Func _Example()
;~  Local $ofrmAssembly = _CLR_LoadLibrary("System.Windows.Forms")
    Local $oAssembly = _CLR_LoadLibrary("System.Windows.Forms")

    ConsoleWrite("!$oAssembly: " & IsObj($oAssembly) & @CRLF)
    Local $oForm = _CRL_CreateObject($oAssembly, "System.Windows.Forms.Form")
    ConsoleWrite("!$oForm: " & IsObj($oForm) & @CRLF)

    Local $oSize = _CRL_CreateObject($oAssembly, "System.Drawing.Size(10,10)")
    ConsoleWrite("!$oSize: " & IsObj($oSize) & @CRLF)




$oform.name="Form1"

#Region dynamic controls
 $oCol=$oForm.controls
 ConsoleWrite("!$oCol: " & IsObj($oCol) & @CRLF)

 Local $oBtn= _CRL_CreateObject($oAssembly, "System.Windows.Forms.Button")
 ConsoleWrite("!$oBtn " & IsObj($oBtn) & @CRLF)
 $oBtn.Name="Button1"
 $oBtn.Width=50
 $oBtn.Left = 150

 Local $oText= _CRL_CreateObject($oAssembly, "System.Windows.Forms.TextBox")
 ConsoleWrite("!$oText: " & IsObj($oText) & @CRLF)

 $oText.Name="Text1"
;~  $oForm.opacity=0.75
 $oForm.Text = "Form From Net - AutoIt Rocks"
 $oForm.Width = 800
 $oForm.Height = 400
 $oForm.Show()

 $oBtn.parent = $oForm
 $oText.parent = $oForm

 consolewrite("ocol.count " & $ocol.count & @crlf)
;~  $oForm.controls.Add($oBtn)
;~  $oForm.controls.Add($oText)

#EndRegion


    Sleep(1000)

    ConsoleWrite("$oForm Handle: " & $oForm.Handle & @CRLF)

EndFunc   ;==>_Example

 

Posted

Hi all,

The m-word and much more that .Net has to offer is one of the main reasons why we could not wait to break this framework open ... :drool:

I am so pleased we finally got there ... even if most of the community members had no need for it apparently...

But than again the things you don't know, you don't miss... so it's up to us to promote these feature publishing examples.

Regarding the Form / Button issue, Can it be that we are facing a double dot notation issue ?

If this does not work => $oForm.controls.Add($oBtn) and this seem to work  $oBtn.parent.

Has to be investigated further...

Posted (edited)

I think controls property is not exposed as a com collections class (just like a lot of the .NET FCL classes have an interface and not a COM exposed interface without (un)wrapping it)

maybe something like this has to be in between but no clue what interface we then should have

$pControls = $form.controls
$oControls = ObjCreateInterface(...,...,$pControls)
Public Function CreateInstance (
	assemblyName As String,
	typeName As String,
	ignoreCase As Boolean,
	bindingAttr As BindingFlags,
	binder As Binder,
	args As Object(),
	culture As CultureInfo,
	activationAttributes As Object()
) As ObjectHandle
  • Reading on the internet
    Appdomain.CreateInstance returns a System.Runtime.Remoting.ObjectHandle instance
    so its unwrap must be called in order to obtain a reference to the desired instance which is a COM-Callable Wrapper (CCW)
     
Edited by junkew
Posted

Yes the QTP has a very nice .Net integration... something we can all learn from.

The form.Controls seem to be a read only one, that get's all the Form Controls listed

Quote

' System.Windows.Forms.Control
<Browsable(False), DesignerSerializationVisibility(DesignerSerializationVisibility.Content), SRDescription("ControlControlsDescr")>
Public ReadOnly Property Controls() As Control.ControlCollection
    Get
        Dim controlCollection As Control.ControlCollection = CType(AddressOf Me.Properties.GetObject(Control.PropControlsCollection), Control.ControlCollection)
        If controlCollection Is Nothing Then
            controlCollection = Me.CreateControlsInstance()
            AddressOf Me.Properties.SetObject(Control.PropControlsCollection, controlCollection)
        End If
        Return controlCollection
    End Get
End Property

 

Posted (edited)
Its weirdly documented and zillions of examp
les work on controls.add ....


/** @property */
public ControlCollection get_Controls

 

A Control can act as a parent to a collection of controls. For example, when several controls are added to a Form, each of the controls is a member of the Control.ControlCollection assigned to the Controls property of the form, which is derived from the Control class.

You can manipulate the controls in the Control.ControlCollection assigned to the Controls property by using the methods available in the Control.ControlCollection class.

  • Form.ControlCollection could be an alternative for form.controls
  • Form.get_controls could be an alternative for form.controls

MSDN states this

https://msdn.microsoft.com/nl-nl/library/system.windows.forms.form.controlcollection.add(v=vs.110).aspx

Just have to try when I am behind my desktop

 

most likely its not COM exposed see ComVisibleAttribute = false

 

[ListBindableAttribute(false)]
[ComVisibleAttribute(false)]
public ref class ControlCollection : ArrangedElementCollection, 
    IList, ICollection, IEnumerable, ICloneable

 

 

Edited by junkew
Posted

junkew, Your code in post 77. I think you have to do it this way:

Replace these lines in CLR.au3

"GetAssemblies hresult();" & _
"get_FullName hresult();" & _ ; (Two lines)

with these lines

"GetAssemblies hresult(ptr*);" & _
"get_FullName hresult(bstr*);" & _ ; (Two lines)

Run this code:

#include <StructureConstants.au3>
#include <WinAPI.au3>
#include "CLR.au3"

Example()

Func Example()
  Local $oDomain=_CLR_GetDefaultDomain()
  ConsoleWrite("$oDomain: " & IsObj($oDomain) & @CRLF)

  Local $oXPTable = _CLR_LoadLibrary("XPTable.dll")

  Local $sFullName
  $oXPTable.get_FullName( $sFullName )
  ConsoleWrite( "$sFullName = " & $sFullName & @CRLF )

  Local $pAssemblyArray
  $oDomain.getAssemblies( $pAssemblyArray )

  Local $iDim = SafeArrayGetDim( $pAssemblyArray )
  ConsoleWrite( "$iDim = " & $iDim & @CRLF )

  Local $iLBound, $iUBound
  SafeArrayGetLBound( $pAssemblyArray, 1, $iLBound )
  SafeArrayGetUBound( $pAssemblyArray, 1, $iUBound )
  ConsoleWrite( "$iLBound = " & $iLBound & @CRLF )
  ConsoleWrite( "$iUBound = " & $iUBound & @CRLF )

  Local $tAssemblyArray = DllStructCreate( $tagSAFEARRAY, $pAssemblyArray )
  Local $fFeatures = DllStructGetData( $tAssemblyArray, "fFeatures" )
  ConsoleWrite( "$fFeatures = 0x" & Hex( $fFeatures ) & @CRLF )

  Local $vt
  SafeArrayGetVartype( $pAssemblyArray, $vt )
  ConsoleWrite( "$vt = " & $vt & @CRLF )

  Local $tGUID = $tagGUID
  Local $tGUID = DllStructCreate( $tagGUID, $pAssemblyArray - 16 )
  Local $sGUID = _WinAPI_StringFromGUID( $tGUID )
  ConsoleWrite( "$sGUID = " & $sGUID & @CRLF )

  Local $pAssemblyArrayData
  SafeArrayAccessData( $pAssemblyArray, $pAssemblyArrayData )
  Local $tAssembly = DllStructCreate( "ptr", $pAssemblyArrayData )
  Local $pAssembly = DllStructGetData( $tAssembly, 1 )
  ConsoleWrite( "$pAssembly = " & $pAssembly & @CRLF )
  SafeArrayUnaccessData( $pAssemblyArray )

  Local $oAssembly = ObjCreateInterface( $pAssembly, $sIID_IAssembly, $sTag_IAssembly )
  ConsoleWrite( "IsObj( $oAssembly ) = " & IsObj( $oAssembly ) & @CRLF )

  Local $sFullName
  $oAssembly.get_FullName( $sFullName )
  ConsoleWrite( "$sFullName = " & $sFullName & @CRLF )
EndFunc

Output:

>oClrHost: 1
>pCLRRuntimeInfo: 0x00B20658
>oCLRRuntimeInfo: 1
>IsLoadable: True
>pCLRRuntimeHost: 0x00B54538
>oCLRRuntimeHost: 1
IsObj( $oCorRuntimeHost ) = 1
$pAppDomain = 0x007A002C
IsObj( $oAppDomain ) = 1
$oDomain: 1
>oClrHost: 1
>pCLRRuntimeInfo: 0x00B20A18
>oCLRRuntimeInfo: 1
>IsLoadable: True
>pCLRRuntimeHost: 0x00B549B8
>oCLRRuntimeHost: 1
IsObj( $oCorRuntimeHost ) = 1
$pAppDomain = 0x007A002C
IsObj( $oAppDomain ) = 1
$pType = 0x007A0018
IsObj( $oType ) = 1
$pAssembly = 0x007AFFD0
IsObj( $oAssembly ) = 1
$pAssemblyType = 0x007AFF98
IsObj( $oAssemblyType ) = 1
-$pObject = 0x007AFF50
-IsObj( $oObject ) = 1
$sFullName = XPTable, Version=1.2.0.22555, Culture=neutral, PublicKeyToken=24950705800d2198
$iDim = 1
$iLBound = 0
$iUBound = 1
$fFeatures = 0x00000240
$vt = 0x0000000D
$sGUID = {00000000-0000-0000-C000-000000000046}
$pAssembly = 0x007AFFD0
IsObj( $oAssembly ) = 1
$sFullName = mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

 

ptrex, You are too hasty in your conclusions.

Posted

to much to read and learn:) thx @LarsJ will try later

regarding controls property

MSForms.Controls controls = (MSForms.Controls) pageObjType.InvokeMember ("Controls", BindingFlags.GetProperty | BindingFlags.IgnoreCase, null, pageObj, null);

similar construct found in excelDNA

object controls = ComObjectType.InvokeMember("Controls", BindingFlags.GetProperty, null, ComObject, null);

So I assume bindingflags have to be set at several places with invokemember and createinstance.

Func _CRL_CreateObject(ByRef $oAssembly, $sTypeName = "", $sParameter3 = "")
    #forceref $oAssembly, $sTypeName, $sParameter3

    If @NumParams = 2 Then
        Local $oObject = 0
        $oAssembly.CreateInstance_2($sTypeName, True, $oObject)
        Return $oObject
    EndIf

    If @NumParams = 3 Then
        ; static Array_Empty := ComObjArray(0xC,0), null := ComObject(13,0)
        Local $pSAEmpty, $tSAB = DllStructCreate($tagSAFEARRAYBOUND)
        DllStructSetData($tSAB, "cElements", 0)
        DllStructSetData($tSAB, "lLbound", 0)
        $pSAEmpty = SafeArrayCreate($VT_VARIANT, 0, $tSAB)
        Local $oObject = 0

;~      Local $bFlags=bitor($BindingFlags_IgnoreCase,$BindingFlags_Instance,$BindingFlags_Public,$BindingFlags_NonPublic,$BindingFlags_GetField,$BindingFlags_GetProperty,$BindingFlags_PutDispProperty,$BindingFlags_PutRefDispProperty,$BindingFlags_SetProperty)
;~      Local $bFlags=$BindingFlags_GetProperty
        Local $bFlags=bitor($BindingFlags_IgnoreCase,$BindingFlags_Public,$BindingFlags_Instance,$BindingFlags_NonPublic,$BindingFlags_GetProperty)
        $oAssembly.CreateInstance_3($sTypeName, True, $bFlags, 0, CreateSafeArray($sParameter3), 0, $pSAEmpty, $oObject)
;~      $oAssembly.CreateInstance_3($sTypeName, True, 0, 0, CreateSafeArray($sParameter3), 0, $pSAEmpty, $oObject)

        Return $oObject
    EndIf

EndFunc   ;==>_CRL_CreateObject

 

Posted

GetType_2 example and the links in the example are good learning material

#include "CLR.Au3"
#include <WinAPI.au3>
#include <GDIPlus.au3>
#include <GUIConstantsEx.au3>
#include <StructureConstants.au3>
Example()

;~ http://stackoverflow.com/questions/1833615/difference-between-assembly-createinstance-and-activator-createinstance
;~ https://blogs.msdn.microsoft.com/calvin_hsia/2016/01/29/use-c-and-no-managed-code-to-create-a-wpf-form/
;~ https://blogs.msdn.microsoft.com/calvin_hsia/2015/12/30/use-the-power-of-reflection-to-create-and-manipulate-managed-objects/
;~ https://gist.github.com/GilesBathgate/84e1e37235f7669c32860a56247e26d4
Func Example()
    Local $oAssembly = _CLR_LoadLibrary("mscorlib")
    ConsoleWrite("!$oAssembly: " & IsObj($oAssembly) & @CRLF)

    Local $pType
    $oAssembly.GetType_2("System.Activator",$pType)
    ConsoleWrite("!$pType: " & ptr($pType) & @CRLF)

    Local $oType = ObjCreateInterface($pType, $sIID_IType, $sTag_IType)
    ConsoleWrite("IsObj( $oType ) = " & IsObj($oType) & @CRLF)

EndFunc   ;==>Example

 

Posted

Dozens of controls.add tried (except the correct one :sweating: still not found)

Another trivial example (as also done with HP UFT dotnetfactory) in powershell just showing stuff should work with controls.add.
Could it be an issue in AutoIt? or is createInstance returning a different type of COM?

Add-Type -AssemblyName System.Windows.Forms

function fgetOSinfo{
	Get-WmiObject win32_OperatingSystem
}

$form = New-Object system.Windows.Forms.Form
$form.Width=600
$form.Height= 600
$btn = New-Object system.Windows.Forms.Button
$form.Controls.Add($btn)
$btn.Width=100
$btn.Height=50
$btn.Text = "Get OS data"
$btn.add_click({
	$varobj=fgetOSinfo
	$textbox1.lines+="varObj Type ="+$varobj.GetType().FullName
	$textbox1.lines+= "varobj=$varobj"     
	$textbox1.lines+= $varobj.BuildNumber
	$textbox1.Lines+=''
	$textbox1.Lines+=$varobj|Out-String
	}
)

$textbox1=New-Object System.Windows.Forms.Textbox
$textbox1.Multiline=$true
$textbox1.Width=600
$textbox1.Height=400
$textbox1.Top=60
$form.Controls.Add($textbox1)

$form.ShowDialog()

 

 

 

Posted

Hmmm, not sure who is managing the development part of COM in AutoIT these days, or development at all ...

In the past trancexx was the main leader of the gang... but not sure someone can pick this up and check it from the backend...

 

Posted

ptrex, I've tested your example in post 80 by running this code:

#include "CLR.au3"

Example() ; Form Using System.Windows.Forms.Form

Func Example()
  ConsoleWrite( "_CLR_LoadLibrary System.Windows.Forms" & @CRLF )
  Local $oAssembly = _CLR_LoadLibrary( "System.Windows.Forms" )
  ConsoleWrite( "IsObj( $oAssembly ) = " & IsObj( $oAssembly ) & @CRLF )

  ConsoleWrite( @CRLF & "_CRL_CreateObject: System.Windows.Forms.Form" & @CRLF )
  Local $oForm = _CRL_CreateObject( $oAssembly, "System.Windows.Forms.Form" )
  ConsoleWrite( "IsObj( $oForm ) = " & IsObj( $oForm ) & @CRLF )

  $oForm.Text = "Form From Net - AutoIt Rocks"
  $oForm.Width = 800
  $oForm.Height = 400

  ConsoleWrite( @CRLF & "_CRL_CreateObject System.Windows.Forms.Button" & @CRLF )
  Local $oButton1 = _CRL_CreateObject($oAssembly, "System.Windows.Forms.Button")
  $oButton1.Text = "button"
  $oButton1.Width = 60
  $oButton1.Height = 30

  $oForm.Controls.Add( $oButton1 ) ; ERR
  ;$oButton1.Parent = $oForm ; OK

  $oForm.ShowDialog()

  $oForm.Dispose()
EndFunc

Error:

err.number is:     0x80131509
err.windescription:  
err.description is:   
err.source is:     
err.helpfile is:   
err.helpcontext is:   
err.lastdllerror is:   0
err.scriptline is:   24
err.retcode is:   0x00000000

 

I've also tested this AutoHotkey translation of the code:

#Include CLR.ahk

oAssembly := CLR_LoadLibrary("System.Windows.Forms")

oForm := CLR_CreateObject( oAssembly, "System.Windows.Forms.Form" )
oForm.Text := "Form From Net - AutoHotkey Rocks"
oForm.Width := 800
oForm.Height := 400

oButton1 := CLR_CreateObject(oAssembly, "System.Windows.Forms.Button")
oButton1.Text := "button"
oButton1.Width := 60
oButton1.Height := 30

oForm.Controls.Add(oButton1) ; ERR
;oButton1.Parent := oForm ; OK

oForm.ShowDialog()

oForm.Dispose()

Error:

Error:  0x80131509 - 

Specifically: Add

    Line#
    006: oForm.Text := "Form From Net - AutoHotkey Rocks"
    007: oForm.Width := 800  
    008: oForm.Height := 400  
    010: oButton1 := CLR_CreateObject(oAssembly, "System.Windows.Forms.Button")
    011: oButton1.Text := "button"  
    012: oButton1.Width := 60  
    013: oButton1.Height := 30  
--->    015: oForm.Controls.Add(oButton1)  
    018: oForm.ShowDialog()  
    020: oForm.Dispose()  
    021: Exit
    022: Exit
    022: Exit

Continue running the script?

 

If the AutoHotkey code fails, our code will also fail. So far, we've relied on working AutoHotkey code.

Posted
oForm.Controls

Seems to return a System.Object instead a ControlCollection.  I think is hard to create a 100% funtional .NET support (It will require a lot of time to learn about .NET internal) I prefer to keep this for simple object manipulation and extern  libraries.. No GUI, No basic .NET Types. etc...

 


Saludos

Posted

COR_E_INVALIDOPERATION

0x80131509

An operation is not legal in the current state.

Details can be found here: https://blogs.msdn.microsoft.com/yizhang/2010/12/17/interpreting-hresults-returned-from-netclr-0x8013xxxx/

My UDFs and Tutorials:

Spoiler

UDFs:
Active Directory (NEW 2024-07-28 - Version 1.6.3.0) - Download - General Help & Support - Example Scripts - Wiki
ExcelChart (2017-07-21 - Version 0.4.0.1) - Download - General Help & Support - Example Scripts
OutlookEX (2021-11-16 - Version 1.7.0.0) - Download - General Help & Support - Example Scripts - Wiki
OutlookEX_GUI (2021-04-13 - Version 1.4.0.0) - Download
Outlook Tools (2019-07-22 - Version 0.6.0.0) - Download - General Help & Support - Wiki
PowerPoint (2021-08-31 - Version 1.5.0.0) - Download - General Help & Support - Example Scripts - Wiki
Task Scheduler (2022-07-28 - Version 1.6.0.1) - Download - General Help & Support - Wiki

Standard UDFs:
Excel - Example Scripts - Wiki
Word - Wiki

Tutorials:
ADO - Wiki
WebDriver - Wiki

 

Posted

indeed, at the moment there are many things that don't work at the moment...

I test to use for example the Button Events, but no success.

$oButton1.add_click(test())

This is what I picked up from the AHK forum on this matter : 

Quote

Events raised by .NET objects cannot be handled by unmanaged code "directly." Instead, the unmanaged callback must be wrapped in a .NET Delegate. Fortunately, the .NET Framework 2.0 provides GetDelegateForFunctionPointer(). (Since it is not an instance method, it is much easier to call from C# than from AutoHotkey.)

You may notice that Delegate4FuncPtr() accepts a uint, but casts it to IntPtr. It seems that IntPtr is marshalled into VT_INT (22), but since VT_INT is marshalled into System.Int32, Invoke will always fail. (It would, however, be possible to call the function directly via the exported COM interface.)

Parameters of type System.Object, unless otherwise specified by [MarshalAsAttribute], are marshalled as VARIANT. This means that for every Object parameter in .NET code, 16 bytes are pushed onto the stack, which equals 4 parameters for the AHK callback. The low-word of the first parameter (vt) defines the COM VARTYPE of the variant. For most managed types, this should be 13 (VT_UNKNOWN.) If VT_UNKNOWN, the third parameter is a pointer to the IUnknown interface of the object (which can generally be COM_Invoke'd.)

Parameters of more specific reference types (such as System.EventArgs) are marshalled as a pointer to the relevant exported COM interface - i.e. 4 bytes = 1 parameter. (Usually this is an automatically-generated interface which can be COM_Invoke'd.) 

So definitely there are limitations that are hard to get around it seems...

Posted (edited)

From VBA same results on Controls and Controls.Count so its not AutoIt

Somehow there has to be made a Com Callable Wrapper around a return value of certain properties

VBA tried with .NET 2.0 (and 4.x) (sleep function is not needed)

' Add references to mscoree.tlb and mscorlib.tlb of .NET 2.0
' Then call the methods below
Private Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)

Sub test()
    Dim clr As mscoree.CorRuntimeHost               'The runtime host
    Dim domain As mscorlib.AppDomain                'The domain we are running in
    Dim oFormAssembly As Assembly                   'A specific assembly loaded
    Dim iType As mscorlib.Type                      'A reference to the type of the FCL
    Dim loadedAsms() As Assembly                    'A variant array
    
    Dim oForm As Object                             'A reference to but an instance of a form in
    Dim oCol As Object                              'A (not working) reference to get the controls collection in
    Dim tStr As String                              'String with the class/type to be created
    
    Set clr = New mscoree.CorRuntimeHost            'Create the CLR

    clr.Start                                       'Start the CLR
    clr.GetDefaultDomain domain                     'Get the default running domain
 
    loadedAsms = domain.GetAssemblies()             'See which assemblies are loaded by default
    'Load the System.Windows.Forms dll
    Set oFormAssembly = domain.Load_2("System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")
    
    tStr = "System.Windows.Forms.Form"               'Name of the type to create
    Set iType = oFormAssembly.GetType_2(tStr)        'Get an interface so things can be invoked/get/set etc.
    Set oForm = oFormAssembly.CreateInstance_2(tStr, True) 'Create an actual instance of the given type
    oForm.Name = "Nice try from VBA"                 'Set a name property
    oForm.Text = "Nice, VBA rocks to"                'Set a title property

    tStr = "System.Windows.Forms.Button"             'Name of the type to create
    Set iType = oFormAssembly.GetType_2(tStr)        'Get an interface so things can be invoked/get/set etc.
    Set oBtn = oFormAssembly.CreateInstance_2(tStr, True) 'Create an actual instance of the given type
    oBtn.Text = "do not hit"                          'Set a caption property
    Set oBtn.Parent = oForm                           'Link it to the form
    
    oForm.Show                'Show the form
    
'Here the problems start
        'bFlags = 0
      'Fails
      'Debug.Print oCol.Count
    
    'Set pUnwrapped = oForm.Unwrap
    
'    Set oCol = iType.InvokeMember_3("Controls", bFlags, Null, pUnwrapp, Null, Null)

End Sub

For the moment it works fine by setting the parent of a childcontrol on the form

  • However I am sure we face other <Type>.Count and iterators to be walked thru

 

finally a more logical error    0x80131509 : " "This type has a ComVisible(false) parent in its hierarchy, therefore QueryInterface calls for IDispatch or class interfaces are disallowed".

 

 

Edited by junkew
finally a more logical error      0x80131509 : " "This type has a ComVisible(false) parent in its hierarchy, therefore QueryInterface calls for IDispatch or class interfaces are disallowed".
Posted

@Junkew, this is a good reflex to test this in VBA as well, so we know where the problem resides.

Anyhow the control.parent approach is good enough to get the controls to show on the Gui. Maybe we should leave it as it is and focus on other area's now...

Like getting the events to run ... there is no use in getting the controls to show of they don't trigger any events, correct ?

 

 

Posted

on my "short term"  list are

0. Scan thru the interfaces for "completeness" of hosting. Repaired AIOWrapperGenerator for that

1. create the basic types like system.int32 etc

2. logic like dotnetfactory with ireflection.
 I think this is a good example for that https://blogs.msdn.microsoft.com/shrib/2007/09/04/using-ireflect-to-expose-a-type-as-idispatch-to-com/

   if that works I can see if form.controls is working in a better way

3. Examples for all system.collections.*  including for each / for i=0 to and getnext logic

Looking at LUA.NET and IronXXX languages source code gives a lot of understanding material

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...