Return-Type in Base & Inherited Class

M

Michael Maes

Hello,

I have a BaseClass and many Classes which all inherit (directly) from the BaseClass.

One of the functions in the BaseClass is to (de)serialize the (inherited) Class to/from disk.

1. The Deserialization goes like:

#Region " Load "

Public Function Load(ByVal path As String) As Object

Return DeserializeXML(className, path)

End Function

Public Function Load(ByVal path As String, ByVal user As String) As Object

Return DeserializeXML(className, path, user)

End Function

#End Region

The "problem" with this approach is that when a class get's instanciated it is returned as an Object instead a the Derived type so I have to Cast it (option strict on):

Dim Setup As New Namespace.Settings.Setup

Setup = DirectCast(Setup.Load(PathInfo.Settings), Namespace.Settings.Setup)

If I could return it with the right Type the code would be reduced to:

Dim Setup As New Namespace.Settings.Setup

Setup = Setup.Load(PathInfo.Settings)

One way to do this is to put the Load-Method in each derived Class (but that wouldn't be very OO), so I'd like to Dynamicaly (genericaly) Change the Return-Type of the Load-Method.

Only: I haven't got a clue how-to!

2. The Deserialization goes like:

#Region " Persist "

Public Sub Persist(ByVal path As String)

Try

SerializeXML(Me, className, path)

Catch ex As Exception

MsgBox(ex.ToString)

End Try

End Sub

Public Sub Persist(ByVal path As String, ByVal user As String)

Try

SerializeXML(Me, className, path, user)

Catch ex As Exception

MsgBox(ex.ToString)

End Try

End Sub

#End Region

The following code is in a Module (in the same project)

Private Sub PerformSerialization(ByVal o As Object, ByVal callerClass As String, ByVal path As String, ByVal user As String)

.........

.........

Dim swFile As StreamWriter = New IO.StreamWriter(fileName)

Dim t As Type

Dim pi As PropertyInfo()

Dim p As PropertyInfo

Select Case callerClass.

Case "settings.setup"

t = GetType(Setup)

.........

.........

Case "settings.mailserver"

t = GetType(MailServer)

.........

.........

Case "settings.products"

t = GetType(Products)

End Select

pi = t.GetProperties()

' Encrypt Strings

For Each p In pi

If p.PropertyType.ToString = "System.String" Then I also think this syntax is "Not Done". What would be the Best Syntax?

p.SetValue(o, Crypto.EncryptString(p.GetValue(o, Nothing)), Nothing)

End If

Next

' Save to Disk

xmlSerializer = New Xml.Serialization.XmlSerializer(t)

xmlSerializer.Serialize(swFile, o)

swFile.Close()

End Sub

Not very OO hum!

Instead of passing "an Object" I would like to pass the Type of the Inheriting Class. But how do I "know" that in the BaseClass.

Any clues would be greatly appreciated.

Michael

PS: I use vs.Net 2003
 
P

Peter Huang

Hi Michael,

1. I think we can not pass the ctype function the type of string. But I
think you may take a look at the code below, did that work for you?

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As
System.EventArgs) Handles Button1.Click
Dim kc As Color = Color.FromKnownColor(KnownColor.Blue)
Dim cr As Color = Color.FromArgb(kc.ToArgb())
Dim c As KnownColor
For i As Integer = 1 To 137
If Color.FromKnownColor(i).ToArgb() = cr.ToArgb() Then
Debug.WriteLine(CType(i, KnownColor).ToString())
End If
Next
Dim o As New derivedclass
o.str = "Test"
o.se()
Dim b As derivedclass
b = o.de()
MsgBox(b.str)
End Sub

Public Class baseclass
Public Sub se()
Dim ser As New Xml.Serialization.XmlSerializer(Me.GetType())
Dim writer As TextWriter = New StreamWriter("Test.xml")
ser.Serialize(writer, Me)
writer.Close()
End Sub

Public Function de() As Object
Dim ser As New Xml.Serialization.XmlSerializer(Me.GetType())
Dim fs As New FileStream("Test.xml", FileMode.Open)
Return ser.Deserialize(fs)
End Function
End Class
Public Class derivedclass
Inherits baseclass
Public str As String
End Class

2.
PropertyInfo.PropertyType Property [Visual Basic]
Gets the type of this property.
PropertyType is readonly property
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpref/html/
frlrfSystemReflectionPropertyInfoClassPropertyTypeTopic.asp

3.
Even if the method is in the baseclass.
e.g.
Public Function Hello(ByVal o As BaseClass) As String
Return o.GetType().ToString()
End Function
but if we pass an derived class instance into the method, we will still get
the derived method's type.

Best regards,

Peter Huang
Microsoft Online Partner Support

Get Secure! - www.microsoft.com/security
This posting is provided "AS IS" with no warranties, and confers no rights.
 
M

Michael Maes

Hi Peter,

Thanks for your reply.

I have though stumbled upon another Issue namely:

I have a LibraryClass "Assembly_Phone"
Assembly_Phone has <Assembly: AssemblyDefaultAlias("I don't know.")>
Lets suppose it contains a Method:

Class Hello
Sub WhoIsCallingMe()
Dim at As Type = GetType(AssemblyDefaultAliasAttribute)
Dim r() As Object =
Me.GetType.Assembly.GetCallingAssembly.GetCustomAttributes(at, False)
If IsNothing(r) Or r.Length = 0 Then
MsgBox("No Default defined")
Else
MsgBox(CType(r(0), AssemblyDefaultAliasAttribute).DefaultAlias)
End If
End Sub
End Class

I have another Assembly (Executable) in antoher Solution "Assembly_EXE"

Assembly_EXE references Assembly_Phone
Assembly_EXE has <Assembly: AssemblyDefaultAlias("It's me!")>

Sub Main()
Dim Ring As New Assembly_Phone.Hello
Ring.WhoIsCallingMe()
End
End Sub

Running Assembly_EXE should display a Messagbox prompting: "It's me!" BUT
it's prompting "I don't know".

What am I missing here?

Regards,

Michael
 
P

Peter Huang

Hi Michael,

It is strange that I can not reproduce problem with your code.
But I think you may try to add a debug output in the WhoIsCallingMe method
to see what is the Me.GetType.Assembly.GetCallingAssembly actually is.
Based on my test, this will return the exe file which call the dll.
Dim at As Type = GetType(AssemblyDefaultAliasAttribute)
Debug.WriteLine(Me.GetType.Assembly.GetCallingAssembly.FullName)
Dim r() As Object =
Me.GetType.Assembly.GetCallingAssembly.GetCustomAttributes(at, False)

You may have a try and let me know the result.

Best regards,

Peter Huang
Microsoft Online Partner Support

Get Secure! - www.microsoft.com/security
This posting is provided "AS IS" with no warranties, and confers no rights.
 
M

Michael Maes

Hi Peter,

I found out what "went wrong".

I had the 'Me.GetType.Assembly.GetCallingAssembly.GetCustomAttributes(at, False)' in the BaseClass, so it returned it's own Assembly since the Caller was the Derived Class (= same assembly)

'Me.GetType.Assembly.GetCallingAssembly.GetCallingAssembly.GetCustomAttributes(at, False)' doesn't seem to do the trick (although it seems logical to me) so I guess I will have to define the Calling Assembly in the (all) Derived Class(es).

Maybe you could come up with a better alternative?

Thanks,

Michael
 
P

Peter Huang

Hi Michael,

I did not understanding your senario very well.
Do you mean your senario is as below?
you have a BasedClass in assembly liba as below
Imports System.Reflection
Public Class BaseClass
Public Sub WhoIsCallingMe()
Dim at As Type = GetType(AssemblyDefaultAliasAttribute)
Dim r() As Object =
Me.GetType.Assembly.GetCallingAssembly.GetCallingAssembly.GetCustomAttribute
s(at, False)
If IsNothing(r) Or r.Length = 0 Then
MsgBox("No Default defined")
Else
MsgBox(CType(r(0), AssemblyDefaultAliasAttribute).DefaultAlias)
End If
End Sub
End Class

and an exe assembly
Module Module1
Public Class DerivedClass
Inherits LibA.BaseClass
End Class
Sub Main()
Dim o As New DerivedClass
o.WhoIsCallingMe()
End Sub
End Module

Based on my test, this will return the "It's me".
If I have any misunderstanding, please feel free to let me know.

Or can you post a simple sample to demostrator your senario?

Best regards,

Peter Huang
Microsoft Online Partner Support

Get Secure! - www.microsoft.com/security
This posting is provided "AS IS" with no warranties, and confers no rights.
 
M

Michael Maes

Hi Peter,

I'm sorry. It was my mistake. I just "simplified" my real issue too much.
I give you here a more realistic "Dummy".
There is one additional issue I face : The MarkDirty-Method doesn't work. I guess it's a scoping issue, but I can't figure out why it doesn't work.

Kind regards,

Michael

////////////////////////////////////////////////

This is more like the Architecture:

LIBRARY:
Imports System.IO

Imports System.Xml.Serialization

Imports Stegosoft.Security

Imports Stegosoft.Security.DotFuscator

Imports System.ComponentModel

Imports System.Reflection

Namespace Settings

''' -----------------------------------------------------------------------------

''' Project : StegoSettings

''' Class : Settings.BaseSettings

'''

''' -----------------------------------------------------------------------------

''' <summary>

'''

''' </summary>

''' <remarks>

''' </remarks>

''' <history>

''' [Michael.Maes] 6/12/2003 Created

''' [Michael.Maes] 30/06/2004 Transform to better OO

''' </history>

''' -----------------------------------------------------------------------------

<Serializable(), _

SkipRenaming(), _

DebuggerStepThrough()> _

Public MustInherit Class BaseSettings

#Region " Declarations "

Dim xmlSerializer As xmlSerializer

Friend Crypto As RijndaelSimple

Friend CallingAssembly As [Assembly]

Friend CallerClass As String

Dim TrackDirty As Boolean = False

Dim _IsDirty As Boolean = False

#End Region

#Region " Constructors "

Public Sub New()

' Set the Calling Assembly

CallingAssembly = Me.GetType.Assembly.GetCallingAssembly
Instanciate()


CallerClass = Me.GetType.Name.ToString.ToLower()

Crypto = New RijndaelSimple

End Sub

Friend Sub Instanciate()

GetProductFamily()

GetCompany()

End Sub

Private Function GetProductFamily() As String

Dim t As Type = GetType(AssemblyDefaultAliasAttribute)

Dim r() As Object = CallingAssembly.GetCustomAttributes(t, False)

If IsNothing(r) Or r.Length = 0 Then

Return Nothing

Else

_ProductFamily = CType(r(0), AssemblyDefaultAliasAttribute).DefaultAlias

Return _ProductFamily

End If

End Function

Private Function GetCompany() As String

Dim t As Type = GetType(AssemblyCompanyAttribute)

Dim r() As Object = CallingAssembly.GetCustomAttributes(t, False)

If IsNothing(r) Or r.Length = 0 Then

Return Nothing

Else

_Company = CType(r(0), AssemblyCompanyAttribute).Company

Return _Company

End If

End Function

#End Region

#Region " Properties "

Dim _ProductFamily As String = Nothing

Friend ReadOnly Property ProductFamily() As String

Get

If IsNothing(_ProductFamily) Then _ProductFamily = GetProductFamily()

Return _ProductFamily

End Get

End Property

Dim _Company As String = Nothing

Friend ReadOnly Property Company() As String

Get

If IsNothing(_Company) Then _Company = GetCompany()

Return _Company

End Get

End Property

Public Overridable ReadOnly Property IsDirty() As Boolean

Get

Return _IsDirty

End Get

End Property

Protected Sub MarkDirty()

If TrackDirty Then _IsDirty = True

End Sub

Public Sub StartMarkingDirty() 'Protected

TrackDirty = True

End Sub

#End Region

#Region " FileExists "

<Description("Checks if the file exists on the Harddisk with the given Path.")> _

Public Function FileExists(ByVal path As String, Optional ByVal user As String = Nothing) As Boolean

If Not path.EndsWith("\") Then path += "\"

' If User is Undefined -> Set it to the Assembly-Name of the Calling Assembly

If user Is Nothing Then user = Me.ProductFamily

If File.Exists(path + user + FilePrefix + CallerClass + ".xml") = True Then

Return True

Else

Return False

End If

End Function

#End Region

#Region " Load "

Public Overridable Function Load(ByVal path As String, Optional ByVal user As String = Nothing) As Object

Debug.WriteLine("Public Overridable Function Load")

Return PerformDeserialization(path, user)

End Function

Friend Function PerformDeserialization(ByVal path As String, ByVal user As String) As Object

Debug.WriteLine("PerformDeserialization")

If Not path.EndsWith("\") Then path += "\"

If user Is Nothing Then user = Me.ProductFamily

If File.Exists(path + user + FilePrefix + CallerClass + ".xml") = True Then

Dim srFile As StreamReader = New StreamReader(path + user + FilePrefix + CallerClass + ".xml")

Dim t As Type

Dim pi As PropertyInfo()

Dim p As PropertyInfo

Try

xmlSerializer = New Xml.Serialization.XmlSerializer(Me.GetType())

Dim cls As Object

cls = xmlSerializer.Deserialize(srFile)

t = cls.GetType

pi = t.GetProperties()

For Each p In pi

If p.PropertyType.Equals(GetType(System.String)) Then

p.SetValue(cls, Crypto.DecryptString(CType(p.GetValue(cls, Nothing), String)), Nothing)

End If

Next

Return cls

Catch ex As Exception

Debug.WriteLine(ex.ToString)

MsgBox(ex.ToString)

Finally

srFile.Close()

End Try

Else

Dim ClassType As Type = Me.GetType()

Return Activator.CreateInstance(ClassType, True)

End If

End Function

#End Region

#Region " Persist "

Public Sub Persist(ByVal path As String, Optional ByVal user As String = Nothing)

If user Is Nothing Then user = Me.ProductFamily

PerformSerialization(path, user)

End Sub

Private Sub PerformSerialization(ByVal path As String, ByVal user As String)

If Not path.EndsWith("\") Then path += "\"

Dim FileName As String = path + user.ToLower + FilePrefix + CallerClass.ToLower + ".xml"

Dim swFile As StreamWriter = New IO.StreamWriter(FileName)

Dim t As Type = Me.GetType()

Dim pi As PropertyInfo()

Dim p As PropertyInfo

pi = t.GetProperties()

' Encrypt Strings

For Each p In pi

If p.PropertyType.Equals(GetType(System.String)) Then

p.SetValue(Me, Crypto.EncryptString(CType(p.GetValue(Me, Nothing), String)), Nothing)

End If

Next

' Save to Disk

xmlSerializer = New Xml.Serialization.XmlSerializer(t)

xmlSerializer.Serialize(swFile, Me)

swFile.Close()

End Sub

#End Region

End Class

End Namespace

************************

Namespace Settings

''' -----------------------------------------------------------------------------

''' Project : StegoSettings

''' Class : Settings.TestData

'''

''' -----------------------------------------------------------------------------

''' <summary>

''' This is "A" Demo-Class. In reality there are many classes like this, all inheriting from BaseSettings

''' </summary>

''' <remarks>

''' </remarks>

''' <history>

''' [Michael.Maes] 6/12/2003 Created

''' [Michael.Maes] 30/06/2004

''' </history>

''' -----------------------------------------------------------------------------

<Serializable()> Public Class TestData

Inherits BaseSettings

Public Sub New()

MyBase.New()

' CallingAssembly = Me.GetType.Assembly.GetCallingAssembly

' MyBase.Instanciate()

End Sub

#Region " Load "

Public Shadows Function Load(ByVal path As String, Optional ByVal user As String = Nothing) As TestData

Try

StartMarkingDirty()

Return DirectCast(PerformDeserialization(path, user), TestData)

Catch ex As Exception

Return Nothing

End Try

End Function

#End Region

Private _connectionName As String

Private _dataSource As String

Private _initialCatalog As String

Private _DataLibrary As String

Private _DataLibraryUrl As String

Private _password As String

Private _persistSecurityInfo As Boolean = False

Private _userID As String

Public Property ConnectionName() As String

Get

Return _connectionName

End Get

Set(ByVal Value As String)

_connectionName = Value : MarkDirty()

End Set

End Property

Public Property DataLibrary() As String

Get

Return _DataLibrary

End Get

Set(ByVal Value As String)

_DataLibrary = Value : MarkDirty()

End Set

End Property

Public Property DataLibraryUrl() As String

Get

Return _DataLibraryUrl

End Get

Set(ByVal Value As String)

_DataLibraryUrl = Value : MarkDirty()

End Set

End Property

Public Property DataSource() As String

Get

Return _dataSource

End Get

Set(ByVal Value As String)

_dataSource = Value : MarkDirty()

End Set

End Property

Public Property InitialCatalog() As String

Get

Return _initialCatalog

End Get

Set(ByVal Value As String)

_initialCatalog = Value : MarkDirty()

End Set

End Property

Public Property Password() As String

Get

Return _password

End Get

Set(ByVal Value As String)

_password = Value : MarkDirty()

End Set

End Property

Public Property PersistSecurityInfo() As Boolean

Get

Return _persistSecurityInfo

End Get

Set(ByVal Value As Boolean)

_persistSecurityInfo = Value : MarkDirty()

End Set

End Property

Public Property UserID() As String

Get

Return _userID

End Get

Set(ByVal Value As String)

_userID = Value : MarkDirty()

End Set

End Property

Public Function ConnectionString() As String

Dim cn As String = _

"workstation id=" & SystemInformation.ComputerName.ToString & ";" & _

"packet size=4096;" & _

"User ID=" & MyClass.UserID & ";" & _

"Password=" & MyClass.Password & ";" & _

"data source=" & MyClass.DataSource & ";" & _

"persist security info=" & MyClass.PersistSecurityInfo & ";" & _

"initial catalog=" & MyClass.InitialCatalog

Return cn

End Function

End Class

End Namespace

******************************

******************************

EXE:

''' -----------------------------------------------------------------------------

''' <summary>

''' Just a test

''' </summary>

''' <remarks>

''' </remarks>

''' <history>

''' [Michael.Maes] 1/07/2004 Created

''' </history>

''' -----------------------------------------------------------------------------

Module StartUp

Sub Main()

Dim DataConnection As New Stegosoft.Settings.TestData

DataConnection = DataConnection.Load("..\", "Jef")

Dim s As String = Nothing

s += "ConnectionName: " & DataConnection.ConnectionName & vbCrLf

s += "DataLibrary: " & DataConnection.DataLibrary & vbCrLf

s += "DataLibraryUrl: " & DataConnection.DataLibraryUrl & vbCrLf

s += "DataSource: " & DataConnection.DataSource & vbCrLf

s += "InitialCatalog: " & DataConnection.InitialCatalog & vbCrLf

s += "Password: " & DataConnection.Password & vbCrLf

s += "PersistSecurityInfo: " & DataConnection.PersistSecurityInfo.ToString & vbCrLf

s += "UserID: " & DataConnection.UserID & vbCrLf

s += vbCrLf

s += "IsDirty: " & DataConnection.IsDirty.ToString & vbCrLf

s += vbCrLf

' Changing Values => Object SHOULD become dirty (but it doesn't)

With DataConnection

..ConnectionName = "Jeff_BlaBlaBla"

..DataLibrary = "Jeff_DataLibrary"

..DataLibraryUrl = "Jeff_DataLibraryUrl"

..DataSource = "Jeff_DataSource"

..InitialCatalog = "Jeff_InitialCatalog"

..Password = "Jeff_Password"

..PersistSecurityInfo = True

..UserID = "Jeff_UserID"

End With

s += "ConnectionName: " & DataConnection.ConnectionName & vbCrLf

s += "DataLibrary: " & DataConnection.DataLibrary & vbCrLf

s += "DataLibraryUrl: " & DataConnection.DataLibraryUrl & vbCrLf

s += "DataSource: " & DataConnection.DataSource & vbCrLf

s += "InitialCatalog: " & DataConnection.InitialCatalog & vbCrLf

s += "Password: " & DataConnection.Password & vbCrLf

s += "PersistSecurityInfo: " & DataConnection.PersistSecurityInfo.ToString & vbCrLf

s += "UserID: " & DataConnection.UserID & vbCrLf

s += vbCrLf

s += "IsDirty: " & DataConnection.IsDirty.ToString & vbCrLf

s += vbCrLf

'Explicitly trigger the dirty-checking (For Testing)

s += "StartMarkingDirty: " & vbCrLf

DataConnection.StartMarkingDirty()

With DataConnection

..ConnectionName = "BliBliBli"

End With

s += "ConnectionName: " & DataConnection.ConnectionName & vbCrLf

s += "DataLibrary: " & DataConnection.DataLibrary & vbCrLf

s += "DataLibraryUrl: " & DataConnection.DataLibraryUrl & vbCrLf

s += "DataSource: " & DataConnection.DataSource & vbCrLf

s += "InitialCatalog: " & DataConnection.InitialCatalog & vbCrLf

s += "Password: " & DataConnection.Password & vbCrLf

s += "PersistSecurityInfo: " & DataConnection.PersistSecurityInfo.ToString & vbCrLf

s += "UserID: " & DataConnection.UserID & vbCrLf

s += vbCrLf

s += "IsDirty: " & DataConnection.IsDirty.ToString

MsgBox(s)

DataConnection.Persist("..\", "Jef")

End

End Sub

End Module
 
P

Peter Huang

Hi Michael,

I understand that you have three Assembly , A,B and C.
A has the method WhoIsCallme which calls GetCallingAssembly
B inhertis A
C call the method WhoIsCallme.

I think in this way the behavior is by design, if you need to reach your
goal, you may try to override the function in the class B.

As for the second question,
I think why firsttime we change the DataConnection's property, the IsDirty
is always false is because the TrackDirty is false.
I think we may try to change the code as below in the baseclass.

Dim _TrackDirty As Boolean = False
Public Property TrackDirty() As Boolean
Get
Return _TrackDirty
End Get
Set(ByVal Value As Boolean)
_TrackDirty = Value
End Set
End Property

Best regards,

Peter Huang
Microsoft Online Partner Support

Get Secure! - www.microsoft.com/security
This posting is provided "AS IS" with no warranties, and confers no rights.
 
P

Peter Huang

Hi Michael,

Yes, the constructor of TestData will be called twice.
1. first time we new the testdata
2. we deserialized the object from the file

But the two constructors will construct two instance of testdata.
Dim DataConnection As New Settings.Settings.TestData
Dim bdc As Settings.Settings.TestData
bdc = DataConnection.Load(Path, User)
Console.WriteLine(Object.ReferenceEquals(DataConnection, bdc))

If we run the code above, we will find that the instance created by New and
the one created by Load are two different objects.
If you still have any concern, please feel free to let me know.

Best regards,

Peter Huang
Microsoft Online Partner Support

Get Secure! - www.microsoft.com/security
This posting is provided "AS IS" with no warranties, and confers no rights.
 
M

Michael Maes

Hi Peter,

I do have soome questions left:
a.. Is it possible to determine the CallingAssembly of the CallingAssembly (so I can have the 'CallerAssembly'-Definition in the BaseForm instead of in every Derived Class)
b.. Any suggestions on how to track the Dirtyness of the Object? You suggested to make a Property out of the field TrackDirty. I don't see how that would influence the good operation. It still triggers True all the time :-(
TIA & Kind regards,

Michael
 
P

Peter Huang

Hi Michael,
a.. Is it possible to determine the CallingAssembly of the
CallingAssembly (so I can have the 'CallerAssembly'-Definition in the
BaseForm instead of in every Derived Class)

From the document said,
Assembly.GetCallingAssembly Method
Returns the Assembly of the method that invoked the currently executing
method.
The method is used to retrieve the assembly which is calling the currently
running code so the GetCallingAssembly.GetCallingAssembly will not work.
But we can simplified the code to call GetCallingAssembly in every derived
class and then pass the returned assembly to a method in the base class so
that we can save a lot of coding job.
b.. Any suggestions on how to track the Dirtyness of the Object? You
suggested to make a Property out of the field TrackDirty. I don't see how
that would influence the good operation. It still triggers True all the
time :-(

It seems that your design is OK that setting a flag to indicate if object
is dirty and changing the flag in every set operation of property. In
general, we think any change to the object's property will make the object
dirty. And you can set the flag to false when consider it has been reset to
be not dirty.

If you still have any concern, please feel free to post here.


Best regards,

Peter Huang
Microsoft Online Partner Support

Get Secure! - www.microsoft.com/security
This posting is provided "AS IS" with no warranties, and confers no rights.
 
M

Michael Maes

Hi Peter,

I think we can "Close" point a.
Point b however remains a big problem. I think we better switch to my thread "BaseClass + DerivedClass + Serialization -- Dirty"

Kind regards,

Michael
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top