Chapter 7. Object Inspection

only for RuBoard

Chapter 7. Object Inspection

At this point, you should be familiar with the fundamentals of object-oriented programming and design within the .NET Framework. You've learned about classes, methods , and properties. You've seen inheritance, containment, and polymorphism. You have also tackled structured exception handling.

Hopefully, you have learned not only how to use these constructs but when to use them and why. Whether you know it or not, much of what you have gained is portable to other languages within the .NET Framework and beyond.

Now is the time to take what you learned and push forward. This chapter will discuss a few .NET-centric technologies that will be paramount when you consider architectures for your next big project. They include reflection (which you saw briefly in the last chapter), attributes, streams, and serialization. If you understand these technologies, you will have a solid foundation in programming applications for .NET.

only for RuBoard
only for RuBoard

7.1 Reflection

Assemblies (either an .exe or a .dll ) can contain one or more modules, though they usually contain just one. In turn , these modules contain one or more types; types in .NET include classes, interfaces, arrays, structures, delegates, and enumerations. Types contain members : fields, methods , properties, events, and parameters. The .NET class library supplies objects that encapsulate all these entities, letting you load an assembly at runtime (over the network, if you need to) and inspect all types in that assembly. Using the introspective properties of these objects, you can determine if a class is abstract, a method is public, or how many class constructors are available. In fact, anything you can describe with VB.NET code can be discovered at runtime. This process is called reflection .

Reflection is one of the .NET Framework's core technologies. In addition to runtime type discovery, you can use it to create assemblies, modules, and types at runtime and persist them to disk as .exe or .dll files. JScript .NET uses reflection to build symbol tables. The .NET Framework SDK, in fact, contains C# source code for two compilers that are written using reflection: a LISP compiler and a compiler for a subset of C called MyC. Serialization and remoting, both major players in .NET, also rely heavily on reflection.

only for RuBoard
only for RuBoard

7.2 Runtime Type Discovery

Example 7-1 contains a listing for a class called ServerInfo . Consider it the humble beginnings of a load balancer. It can provide the machine name , an IP address, the processor usage, and the available memory of the machine on which it runs. However, in this chapter, it serves more form than function, so for now, forget about what it does and look at what it contains: an event, an enumeration, three methods (a sub, a function, and a shared function), and two read-only properties. While pondering these contents deeply, save it to a file named ServerInfo.vb and compile it to a class library. You will need this assembly as the basis of the rest of the chapter.

Example 7-1. Reflection test class
 'vbc /t:library serverinfo.vb /r:system.dll
   
Imports System
Imports System.Diagnostics
Imports System.Net
Imports System.Threading
   
Public Class ServerInfo
   
    Private machine As String
    Private ip As IPAddress
   
    Public Enum Tasks
        EnterInfiniteLoop = 1
        WasteMemory = 2
        Allocate2GigForTheBrowser = 3
        RandomlyDestroyProcess = 4
    End Enum
   
    Public Event TaskCompleted As EventHandler
   
    Public Sub New( )
        'Get machine info when object is created
        machine = Dns.GetHostName( )
        Dim ipHost As IPHostEntry = Dns.GetHostByName(machine)
        ip = ipHost.AddressList(0)
    End Sub
   
    'This routine only fires an event right now
    Public Sub DoTask(ByVal task As Tasks)
        RaiseEvent TaskCompleted(Me, EventArgs.Empty)
    End Sub
   
    'Shared method
    Public Shared Function GetMachineTime( ) As DateTime
        Return DateTime.Now
    End Function
   
    'Get % of process currently in use
    Public Function GetProcessorUsed( ) As Single
        If PerformanceCounterCategory.Exists("Processor") Then
            Dim pc As New PerformanceCounter("Processor", _
                "% Processor Time", "_Total", True)
            Dim sampleA As CounterSample
            Dim sampleB As CounterSample
   
            sampleA = pc.NextSample( )
            Thread.Sleep(1000)
            sampleB = pc.NextSample( )
            Return CounterSample.Calculate(sampleA, sampleB)
        End If
    End Function
   
    'Get MBytes of free memory
    Public Function GetAvailableMemory( ) As Long
        If PerformanceCounterCategory.Exists("Memory") Then
            Dim pc As New PerformanceCounter("Memory", "Available MBytes")
            Return pc.RawValue( )
        End If
    End Function
   
    Public ReadOnly Property MachineName( ) As String
        Get
            Return machine
        End Get
    End Property
   
    Public ReadOnly Property IPAddress( ) As IPAddress
        Get
            Return ip
        End Get
    End Property
   
End Class 

The listing for ServerInfo.dll is in plain sight, so seeing its contents is easy. However, pretend that the source code doesn't exist. Can the assembly be queried programmatically for its type information? Sure it can. That's what reflection is all about.

The System.Reflection namespace and the System.Type class together will meet most reflection needs. This chapter will not discuss everything that can be accomplished with these classes. By the end of the chapter, though, your handle on reflection should be tighter than a G.I. Joe with Kung-Fu Action Grip.

The first step in type discovery is loading the assembly (the executable or library) in question; Example 7-2 shows the code that does this. Note that " Assembly " is a reserved word, so it is enclosed in square brackets in Example 7-2. If this were not done, System.Reflection.Assembly would have to be used instead. Who wants to type all that? The example also assumes that ServerInfo.dll is located in the same directory where the compiled executable will reside. If it is not, you will have to provide a full path to the Assembly.LoadFrom method.

Example 7-2. Runtime type inspection
 Imports System
Imports System.Reflection
   
Public Class ObjectInfo
    
    Public Sub New (ByVal assemblyName As String)  Dim a As [Assembly] = [Assembly].LoadFrom(assemblyName)  End Sub
   
End Class
   
Public Class Application
    Public Shared Sub Main( )
        Dim oi As New ObjectInfo("ServerInfo.dll")
        Console.ReadLine( )
    End Sub
End Class 

Save this example to a file called reflect.vb . Or, as an alternative, add the Application class and the ObjectInfo class to the ServerInfo.dll assembly and recompile it as an executable. In that case, the constructor to ObjectInfo needs modification so it obtains a reference to the current assembly instead of ServerInfo.dll . Modify it by replacing the call to Assembly.LoadFrom with Assembly.GetExecutingAssembly :

 'Get the currently executing assembly
Dim a As [Assembly] = [Assembly].GetExecutingAssembly( ) 

At this point, the ObjectInfo class does not do much more than load an assembly. Before compiling, remedy the situation by grabbing all the types in ServerInfo.dll and writing them out to the console. Doing so will get the ball rolling. The code is as follows :

 Public Class ObjectInfo
    
    Public Sub New (ByVal assemblyName As String)
        Dim a As [Assembly] = [Assembly].LoadFrom(assemblyName)  Dim t As Type   Dim types As Type( ) = a.GetTypes( )   For Each t In  types   Console.WriteLine(t.FullName)   Next t  End Sub
   
End Class
   
Public Class Application
    Public Shared Sub Main( )
        Dim oi As New ObjectInfo("ServerInfo.dll")
        Console.ReadLine( )
    End Sub
End Class 

Assembly.GetTypes returns an array of System.Type . This array represents every type in the assembly. The types contained in ServerInfo.dll will come back as follows:

 ServerInfo.ServerInfo
ServerInfo.ServerInfo+Tasks 

There are two types in the assembly: the ServerInfo class and an enumeration named Tasks . To avoid confusion, properties, methods, and constructors are not categorized as types. They are considered elements of types.

Theoretically, the ServerInfo.dll assembly could contain more classes. As the types contained in the assembly are iterated in the ObjectInfo class, it might be beneficial to know what is what. System.Type contains several Boolean properties, shown in Table 7-1, that can help determine what a Type object represents.

Table 7-1. Type classification properties in System.Type

Method

Description

IsArray

Type is an array.

IsClass

Type is a class.

IsCOMObject

Type is a COM object.

IsEnum

Type is an enum.

IsInterface

Type is an interface.

IsPointer

Type is a pointer.

IsPrimitive

Type is a primitive data type.

IsValueType

Type is a structure.

It's a good idea to modify the constructor in ObjectInfo to look for class types only, since for the sake of simplicity, this chapter will discuss only classes. If a class is found, we'll pass the type along to a private method named DumpClassInfo (the source code for which is presented later in this chapter):

 For Each t In  types
    If t.IsClass( ) Then
        DumpClassInfo(t)    
    End If
Next t 

Try to follow the discussion for now. Don't worry about coding along. After everything is discussed, a final code listing will contain everything.

The private method, DumpClassInfo , relies on several methods provided by the System.Type class that return Info objects. This is a general name given to the .NET class library classes of the following types: ConstructorInfo , EventInfo , FieldInfo , MethodInfo , ParameterInfo , and PropertyInfo . Each class represents the attributes of a class entity, and each is derived from a common parent, MemberInfo .

You can obtain references for these objects by calling a corresponding Get method on System.Type . For instance, Type.GetConstructors returns an array of ConstructorInfo objects, and Type.GetMethods returns an array of MethodInfo objects. These classes provide information about the constructors and methods of a class, respectively. Each Get method also has a single form: Type.GetConstructor , Type.GetProperty , and so forth. Table 7-2 summarizes these methods.

Table 7-2. Info methods from System.Type

Method

Returns

GetConstructor/GetConstructors

ConstructorInfo

GetEvent/GetEvents

EventInfo

GetField/GetFields

FieldInfo

GetMethod/GetMethods

MethodInfo

GetParameter/GetParameters

ParameterInfo

GetProperty/GetProperties

PropertyInfo

Using the specific classes in Table 7-2 is not necessary, but they do make life easier. Type.GetMembers returns an array of MemberInfo objects. From here, you can determine the specific type of each member by examining the MemberInfo object that corresponds to it. Here's a quick example. Given that t is an instance of Type (that represents a class), the following code is possible:

 Dim member As MemberInfo
Dim members( ) As MemberInfo = t.GetMembers( )
   
For Each member In members
   
    Select Case member.MemberType
        Case MemberTypes.Constructor
        
        Case MemberTypes.Event
        
        Case MemberTypes.Field
        
        Case MemberTypes.Method
        
        Case MemberTypes.Property
    End Select
   
Next member 

However, using the specific Info methods provided through Type is easier.

The time to write ObjectInfo.DumpClassInfo draws nigh. This method, once written, can be used to examine any class. The final listing will be fairly long, so this chapter will examine the code one step at a time. All pretenses aside, the primary motivation of this example is to cover as much of reflection as possible without turning the chapter into a reference manual.

Regardless, the example needs some semblance of credibility. To give it a purpose, let's approach it as if it will be used to generate class documentation. This is a practical for reflection.

DumpClassInfo should first output the name of the class by calling Type.FullName . This step returns the fully qualified name of the type (meaning it includes the namespace):

 Private Sub DumpClassInfo(ByVal t As Type)
    Console.WriteLine(New String("-"c, 80))
    Console.WriteLine("Class: {0}", t.FullName) 

7.2.1 Constructor Information

It's easy to get the constructors by calling Type.GetConstructors . This method returns an array of ConstructorInfo . One of the overrides of GetConstructors accepts a bitmask containing values from the BindingFlags enumeration as a parameter. This enumeration provides a means to restrict what is searched during reflection. Here, the Public and Instance members of the BindingFlags enumeration are used, so only the public instance constructors are asked for. Any nonpublic constructors will not be returned:

 'Get constructors  Dim c As ConstructorInfo   Dim ci( ) As ConstructorInfo = _   t.GetConstructors(BindingFlags.Public Or BindingFlags.Instance)  Console.WriteLine("Constructors:")
        
    For Each c In ci
        Console.WriteLine("    {0}", c)
    Next c 

If BindingFlags.Instance (or BindingFlags.Static ) is not used in conjunction with BindingFlags.Public (or BindingFlags.NonPublic ), no members will be returned.

Examining the output of the previous block of code, note that the enumeration is no longer listed. It is not listed because the call to Type.IsClass wraps the call to DumpClassInfo . The output, thus far, should look similar to this:

 ---------------------------------------------------------------------------
Class: ServerInfo.ServerInfo
Constructors:
    Void .ctor( ) 

Now, this example is definitely not Visual Basic. In C, C++, C#, and Java, void denotes a function that does not return a value. In VB.NET, a function that does not return a value is a Sub . To make this code readable to VB.NET programmers, you only need to replace Void with Public Sub . The .ctor is short for "constructor," or in VB.NET-speak, New .

Translating the output into VB.NET is a fairly straightforward string replacement. Instead of writing the raw constructor signature to the console, use this code:

 For Each c In ci
    Dim s As String = String.Format("{0}{1}", vbTab, c.ToString( ))
    Console.WriteLine(s.Replace("Void .ctor", "Public Sub New"))
Next c 

Even if the BindingFlags enumeration were not used as a filter in this example, the ConstructorInfo class (like all the Info classes) contains all the members needed to determine everything about a class' attributes: Is it public? Is it private? Is it abstract? Is inheritance allowed? And so on.

This chapter does not cover the minutiae of it all. Just understand that all the classes that derive from MemberInfo are very similar in form and function.

7.2.2 Property Information

The code for obtaining public instance properties is similar to that for constructors. Additionally, a call to PropertyInfo.CanRead and PropertyInfo.CanWrite can determine the property's accessibility. The following code, when added to the DumpClassInfo method, displays property information:

 Dim p As PropertyInfo
Dim pi( ) As PropertyInfo = _
    t.GetProperties(BindingFlags.Public Or BindingFlags.Instance)
   
Console.WriteLine( )
Console.WriteLine("Properties:")
   
For Each p In pi
    Console.Write("Public ")
    If p.CanRead And p.CanWrite Then
    ElseIf p.CanRead Then
        Console.Write("ReadOnly ")
    ElseIf p.CanWrite Then
        Console.Write("WriteOnly ")
    End If
    Console.WriteLine("Property {0}( ) As {1}", p.Name, _
        p.PropertyType.ToString( ))
Next p 

This code returns the properties as VB.NET declarations:

 ---------------------------------------------------------------------------
Class: ServerInfo.ServerInfo
Constructors:
        Public Sub New( )  Properties  :  Public ReadOnly Property MachineName( ) As System.String   Public ReadOnly Property IPAddress( ) As System.Net.IPAddress  

Apparently, the PropertyInfo class does not return as much information about a property as some of the other Info classes do for their counterparts. For instance, if you look at the class definition, there does not appear to be a way to do much beyond determining whether a property is read-only, write-only, or both (using the CanWrite and CanRead properties).

However, this impression is not completely accurate. Behind the scenes, VB.NET properties are implemented with corresponding Get and Set methods (see Section 3.2 in Chapter 3). To get additional information about the property (such as whether it is private, inheritable, or shared), you must work with these methods rather than with the PropertyInfo class. The next section details the type of information you can obtain by examining a method. First, though, you need to get to the accessor methods.

You can access the accessor methods in one of two ways: by calling either PropertyInfo.GetGetMethod or PropertyInfo.GetSetMethod . Each returns a MethodInfo class that represents the appropriate accessor. Also, it is possible to call PropertyInfo.GetAccessors , which returns an array of MethodInfo objects. This class is very similar to ConstructorInfo and contains all the functionality necessary to describe the method in question.

7.2.3 Method Information

To get the methods of a class, call Type.GetMethods , which returns an array of MethodInfo objects. This time, however, in the name of pure, unadulterated entertainment, the BindingFlags bitmask will not be used. Our goal is to demonstrate how the information provided by BindingFlags can be determined without using the bitmask.

While iterating through each method, you can see if the method is declared in the current class by calling MethodInfo.DeclaringType . Calling it filters out all methods that are inherited from System.Object or from another base class, had we used inheritance explicitly when creating the ServerInfo class. This way, the listing reflects the only current type, the ServerInfo class, and its unique methods and events. The following code accomplishes this:

 'Get public shared methods
Dim i As Integer
Dim returnType As Type
Dim returnString As String
Dim m As MethodInfo
Dim mi( ) As MethodInfo = t.GetMethods( )
   
Console.WriteLine("Methods:")
   
For Each m In mi
   
    If m.DeclaringType.Equals(t) Then 

You can filter out inherited members in the call to GetMethods by supplying the BindingFlags.DeclaredOnly constant using a method call like the following:

 mi = t.GetMethods(BindingFlags.Public Or _
                  BindingFlags.Instance Or _
                  BindingFlags.DeclaredOnly) 

To determine the accessibility of a method, various properties are available from MethodInfo . See how these property names map IL to VB.NET:

 If m.IsPublic Then
            Console.Write("Public ")
        ElseIf m.IsPrivate Then
            Console.WriteLine("Private ")
        ElseIf m.IsFamily Then
            Console.WriteLine("Protected ")
        ElseIf m.IsAssembly Then
            Console.WriteLine("Friend ")
        ElseIf m.IsFamilyAndAssembly Then
            Console.WriteLine("Protected Friend ")
        End If
                                
        If m.IsStatic Then
            Console.Write("Shared ")
        End If 

These properties, such as IsPublic , IsFamily , and IsStatic , are defined in a class named MethodBase , which is the parent class of both MethodInfo and ConstructorInfo . ConstructorInfo functions the same way as MethodInfo , except it is focused on constructors.

To cut the example's size , polymorphic attributes like MustOverride , Overridable , and Overrides were left out. However, MethodInfo allows you to ascertain this information.

The return type for the reflected method can be snagged by calling, oddly enough, the property named ReturnType . Who could have guessed? If the return type is equal to System.Void , the method is a Sub ; otherwise , it's a Function . After determining which is which, our code calls MethodInfo.Name to retrieve the name of the method.

 returnType = m.ReturnType
   
        If String.Compare(returnType.ToString( ), "System.Void") <> 0 Then
            Console.Write("Function {0}(", m.Name)
            returnString = String.Format(" As {0}", returnType.ToString( ))
        Else
            Console.Write("Sub {0}(", m.Name)
        End If 

Divining the parameters for the method is as simple as calling MethodInfo.GetParameters , which returns an array of ParameterInfo objects. This class can query the parameter's name and type, as shown in the next code fragment for DumpClassInfo :

 Dim parms( ) As ParameterInfo = m.GetParameters( )
   
        For i = 0 To parms.Length - 1
            Console.Write(parms(i).Name)
            Console.Write(" As ")
            Console.Write(parms(i).ParameterType( ))
            If (i < parms.Length - 1) Then
                Console.Write(", ")
            End If
        Next i
   
        Console.Write(")")
        Console.WriteLine(returnString)
   
    End If
   
Next m 

Up to this point, the output of ServerInfo looks like this (if you reformatted it to fit in a book):

 Methods:
Public Sub remove_TaskCompleted(obj As
    ServerInfo.ServerInfo+TaskCompletedEventHandler)
Public Sub add_TaskCompleted(obj As
    ServerInfo.ServerInfo+TaskCompletedEventHandler)
Public Sub DoTask(task As ServerInfo.ServerInfo+Tasks)
Public Shared Function GetMachineTime( ) As System.DateTime
Public Function GetProcessorUsed( ) As System.Single
Public Function GetAvailableMemory( ) As System.Int64
Public Function get_MachineName( ) As System.String
Public Function get_IPAddress( ) As System.Net.IPAddress 

get_MachineName and get_IPAddress are the read-only accessor methods of the MachineName and IPAddress properties, so they should probably not be listed here because they were already handled in the properties section. An additional property of the MethodInfo class called IsSpecialName can prevent the display of accessor methods. To use it, modify the If statement that tests to eliminate inherited members as follows:

 If m.DeclaringType.Equals(t)  AndAlso m.IsSpecialName = False Then  

If you disassemble ServerInfo.dll , you will see that both accessor methods have the specialname IL attribute associated with them. This flag is provided for compiler writers and tool vendors and allows a member to be treated specially. For instance, IntelliSense uses it to prevent the display of accessor methods. However, it doesn't do anything that affects the way code is run.

Once we eliminate methods with special names, the output appears as follows:

 Methods:
Public Sub DoTask(task As ServerInfo.ServerInfo+Tasks)
Public Shared Function GetMachineTime( ) As System.DateTime
Public Function GetProcessorUsed( ) As System.Single
Public Function GetAvailableMemory( ) As System.Int64 

7.2.4 The Complete ObjectInfo Source Code

Example 7-3 contains a revised listing of the ObjectInfo class. It was rewritten slightly, but all the functionality is there (including events). Your assignment, should you choose to accept it, is to add support for other types, such as enumerations and value types. Also, nested types (that is, classes contained within classes) are not handled at all. To handle them, look at Type.GetNestedTypes , if possible. There is really nothing to it. Everything in reflection is aptly named. You can learn a lot just by hacking the example.

Example 7-3 is long, but don't run away. The wonderful world of reflection is far from fully explored. More needs to be covered before the topic can be put to rest. But check out the example and spend some quality time with it.

Example 7-3. ObjectInfo reflection class
 Public Class ObjectInfo
   
    Private Sub DumpConstructors(ByVal t As Type)
   
        Dim c As ConstructorInfo
        Dim ci( ) As ConstructorInfo = _
            t.GetConstructors(BindingFlags.Public Or BindingFlags.Instance)
   
        Console.WriteLine( )
        Console.WriteLine("Constructors:")
   
        For Each c In ci
            Dim s As String = c.ToString( )
            Console.WriteLine(s.Replace("Void .ctor", "Public Sub New"))
        Next c
   
    End Sub
   
    Private Sub DumpEvents(ByVal t As Type)
        Dim e As EventInfo
        Dim ei As EventInfo( ) = _
                    t.GetEvents(BindingFlags.Public Or _
                                BindingFlags.Instance)
   
        Console.WriteLine( )
        Console.WriteLine("Events:")
   
        For Each e In ei
            Console.WriteLine(e.Name)
        Next e
   
    End Sub
   
    Private Sub DumpProperties(ByVal t As Type)
   
        Dim p As PropertyInfo
        Dim pi( ) As PropertyInfo = _
            t.GetProperties(BindingFlags.Public Or BindingFlags.Instance)
   
        Console.WriteLine( )
        Console.WriteLine("Properties:")
   
        For Each p In pi
            Console.Write("Public ")
            If p.CanRead And p.CanWrite Then
            ElseIf p.CanRead Then
                Console.Write("ReadOnly ")
            ElseIf p.CanWrite Then
                Console.Write("WriteOnly ")
            End If
            Console.WriteLine("Property {0}( ) As {1}", _
                p.Name, p.PropertyType.ToString( ))
        Next p
   
    End Sub
   
    Private Sub DumpMethod(ByVal m As MethodInfo)
   
        Dim i As Integer
        Dim returnType As Type
        Dim returnString As String
   
        If m.IsPublic Then
            Console.Write("Public ")
        ElseIf m.IsPrivate Then
            Console.WriteLine("Private ")
        ElseIf m.IsFamily Then
            Console.WriteLine("Protected ")
        ElseIf m.IsAssembly Then
            Console.WriteLine("Friend ")
        ElseIf m.IsFamilyAndAssembly Then
            Console.WriteLine("Protected Friend ")
        End If
   
        If m.IsStatic Then
            Console.Write("Shared ")
        End If
   
        returnType = m.ReturnType
   
        If String.Compare(returnType.ToString( ), "System.Void") <> 0 Then
            Console.Write("Function {0}(", m.Name)
            returnString = String.Format(" As {0}", returnType.ToString( ))
        Else
            Console.Write("Sub {0}(", m.Name)
        End If
   
        Dim parms( ) As ParameterInfo = m.GetParameters( )
        For i = 0 To parms.Length - 1
            Console.Write(parms(i).Name)
            Console.Write(" As ")
            Console.Write(parms(i).ParameterType( ))
   
            If (i < parms.Length - 1) Then
                Console.Write(", ")
            End If
        Next i
   
        Console.WriteLine(")")
   
    End Sub
   
    Private Sub DumpMethods(ByVal t As Type)
   
        Dim m As MethodInfo
        Dim mi( ) As MethodInfo = t.GetMethods( )
   
        Console.WriteLine( )
        Console.WriteLine("Methods:")
   
        For Each m In mi
            If m.DeclaringType.Equals(t) AndAlso _
               m.IsSpecialName = False Then
                DumpMethod(m)
            End If
        Next m
   
    End Sub
   
    Private Sub DumpClassInfo(ByVal t As Type)
   
        Console.WriteLine(New String("-"c, 80))
        Console.WriteLine("Class: {0}", t.FullName)
   
        DumpConstructors(t)
        DumpEvents(t)
        DumpProperties(t)
        DumpMethods(t)
   
        Console.WriteLine( )
   
    End Sub
   
    Public Sub New(ByVal assemblyName As String)
   
        Dim a As [Assembly]
        If (assemblyName = Nothing) Then
            a = [Assembly].GetExecutingAssembly( )
        Else
            a = [Assembly].LoadFrom(assemblyName)
        End If
   
        Dim t As Type
        Dim types As Type( ) = a.GetTypes( )
   
        For Each t In types
            If t.IsClass Then
                DumpClassInfo(t)
            End If
        Next t
   
    End Sub
   
End Class 
only for RuBoard