only for RuBoard |
C# excels over VB as a .NET language in one particular area: XML documentation comments. C# allows you to embed XML comments into your code that can be compiled to documentation independently at a later time. Example 7-6 shows a simple class written in C# that contains XML comments.
using System; /// <summary> /// Summary for the BestLoveSong class. /// </summary> /// <remarks> /// This is a longer description for the BestLoveSong class, /// which is used as an example for Chapter 7. /// </remarks> public class BestLoveSong { /// <summary> /// Name property </summary> /// <value> /// Returns the name of the best love song.</value> public string Name { get { return "Feelings"; } } /// <summary> /// Plays the best love song.</summary> /// <param name="volume"> Volume to play the best love /// song.</param> public void Play(byte volume) { if (volume > 11) volume = 11; } }
You can compile this example from the command line using the /doc switch of the C# compiler as follows :
C:\>csc /target:library comments.cs /doc:comments.xml
Using this switch produces an XML file named comments.xml , as shown in Example 7-7.
<?xml version="1.0"?> <doc> <assembly> <name>comments</name> </assembly> <members> <member name="T:BestLoveSong"> <summary> Summary for the BestLoveSong class. </summary> <remarks> This is a longer description for the BestLoveSong class, which is used as an example for Chapter 7. </remarks> </member> <member name="M: BestLoveSong.Play(System.Byte)"> <summary> Plays the best love song.</summary> <param name="volume"> Volume to play the best love song. </param> </member> <member name="P: BestLoveSong.Name"> <summary> Name property </summary> <value> Returns the name of the best love song.</value> </member> </members> </doc>
This XML can now be processed in a variety of ways to create professional-looking documentation. In fact, this is how the class library documentation for the .NET Framework is created.
Example 7-6 demonstrates only a few of the most important XML comment tags: <summary> , <remarks> , <param> , and <value> . Pay close attention to the <assembly> and <member> tags. They were obtained using reflection and are not a direct part of the XML comments. There are actually quite a few more tags, but there is no time or space available to discuss them all.
Implementing XML comments in VB.NET by using custom attributes is possible. While similar results can be achieved, there is one major drawback to this approach. In C#, the XML tags are comments; they are not stored in the executable. The same cannot be said for attributes, which means that executable size could be significantly larger when using attributes in this manner. However, the degree to which executables expand can be offset greatly by using conditional compilation constants. The attributes might be added only in a debug build, for instance.
There is no doubt that XML comments will be available to VB.NET at some point in the future (if enough people scream for it), so consider this an interim solution. Or just consider it a fun lesson for custom attributes.
In addition to deriving from the System.Attribute class, custom attributes use an attribute: AttributeUsage .
|
AttributeUsage describes three things about the custom attribute that is being created:
What class elements can the attribute be applied to?
Will derived classes inherit the attributes once they are applied to the base class?
Can the attribute be applied to the same class element more than once?
The first question is answered by the AttributeTargets enumeration, which contains all elements that can have attributes applied to them. The members of this enumeration, which are self-explanatory, are as follows: All , Assembly , Class , Constructor , Delegate , Enum , Event , Field , Interface , Method , Module , Parameter , Property , ReturnValue , and Struct .
Here is the basis of an attribute that can be applied to any element:
Imports System <AttributeUsage(AttributeTargets.All)> _ Public Class SomeAttribute : Inherits Attribute End Class
The target values can also be OR ed together to provide a more granular selection. Here is a custom attribute that can be applied only to a class or a method:
Imports System <AttributeUsage( AttributeTargets.Class Or AttributeTargets.Method )> _ Public Class SomeAttribute : Inherits Attribute End Class
By default, attributes are inherited. However, this behavior can be changed using the Attribute.Inherited property:
Imports System <AttributeUsage(AttributeTargets.All, Inherited:=False )> _ Public Class SomeAttribute : Inherits Attribute End Class
Check out the := operator. When an attribute is applied, the constructor for the attribute class is actually called. For instance, the documentation for AttributeUsage looks like this:
Public Sub New(ByVal validOn As AttributeTargets)
The constructor has only one parameter, a member of the AttributeTargets enumeration. Inherited is a property of the AttributeUsage class. The := syntax is just a mechanism that allows an instance of an attribute to be created and a property to be set on that instance all in one go.
Attributes can be applied only once per element unless the AllowMultiple property is set to True :
Imports System <AttributeUsage(AttributeTargets.All, _ Inherited:=False, _ AllowMultiple:=True )> _ Public Class SomeAttribute : Inherits Attribute End Class
At this point, the concept of creating a custom attribute might still be nebulous. It's time to remedy that and start building the SummaryAttribute class used to imitate the <summary> comment tag defined by C#.
The process is actually quite simple. First, create a class that is derived from attribute:
Public Class SummaryAttribute : Inherits Attribute End Class
The summary document comment is just a string. The desired behavior is essentially the following:
<Summary("This class does something really vague")> _ Public Class ThisClass
The attribute's string argument is actually the single argument passed to the constructor of SummaryAttribute :
Public Class SummaryAttribute : Inherits Attribute Public ReadOnly Text As String Public Sub New(ByVal text As String) Me.Text = text End Sub End Class
A read-only field named Text is added to hold the string. Since the field is read-only, we can make it publicly accessible without difficulty. Later, when using reflection to get the summary attribute, it will be obtained through this field.
Adding the AttributeUsage attribute to the class is the final task. This step is shown in Example 7-8. The attribute can be applied to any element, so the AttributeTargets.All is used.
However, the SummaryAttribute class should not be inherited because this would prevent derived classes from having their own summary. Therefore, Inherited is set to False . Finally, the attribute will be used only once per element, so AllowMultiple is set to False as well.
<AttributeUsage(AttributeTargets.All, _ Inherited:=False, _ AllowMultiple:=False)> _ Public Class SummaryAttribute : Inherits Attribute Public ReadOnly Text As String Public Sub New(ByVal text As String) Me.Text = text End Sub End Class
You might wonder why the AllowMultiple property is set to False because of this setting, the attribute is allowed only one application per element. After all, if multiple attributes were allowed, something like this would be possible:
<Summary("This class is used to demonstate the SummaryAttribute"), _ Summary("class that has been created for Chapter 7."), _ Summary("Unfortunately we cannot use multiple attributes like this")"> _
The problem is that attributes are not stored in the assembly in any particular order. It would be nice if they were stored in the order in which they were applied, but that is not the case. Using the summary attribute multiple times would require that we write an alternative sorting method, and possibly that we add an additional parameter in the constructor that specifies the sort order.
This leads to another issue: the formatting of the text stored with the Summary attribute (and the other attributes that will be defined in this chapter). Because the summary is stored as one block of text, a method of formatting it is needed (if, in fact, formatting is necessary).
The remaining documentation attributes are about the same as Summary . The differences lie in where the attributes can be applied. The Remarks attribute is the same as Summary ; it can be applied everywhere. The Param attribute can be applied only to a parameter. The Value attribute can be used only to describe a property's value.
The Param attribute is somewhat different from the others, since it can be applied multiple times. It contains an additional constructor argument that allows a name to be associated with the parameter:
<Param("key", "This is the key description"), _ Param("entry", "This is the entry description")> _ Public Sub(ByVal key As Integer, ByVal entry As String)
Example 7-9 shows the code for the remaining attributes. Each class is implemented in basically the same manner; as in the case of exceptions, it is the name of the class that is important.
By distinguishing among the various types of attributes (rather than using the Summary attribute for everything), you can position and format each attribute based on its type.
<AttributeUsage(AttributeTargets.All, Inherited:=False, AllowMultiple:=False)> _ Public Class RemarksAttribute : Inherits Attribute Public ReadOnly Text As String Public Sub New(ByVal text As String) Me.Text = text End Sub End Class <AttributeUsage(AttributeTargets.Method Or _ AttributeTargets.Constructor, _ Inherited:=False, AllowMultiple:=True)> _ Public Class ParamAttribute : Inherits Attribute Public ReadOnly Name As String Public ReadOnly Text As String Public Sub New(ByVal name As String, ByVal text As String) Me.Name = name Me.Text = text End Sub End Class <AttributeUsage(AttributeTargets.Property, Inherited:=False, AllowMultiple:=False)> _ Public Class ValueAttribute : Inherits Attribute Public ReadOnly Text As String Public Sub New(ByVal text As String) Me.Text = text End Sub End Class
Before using these attributes, we can add one more attribute that is not based on some C# document tag. Since tracking changes to a class throughout the development cycle is useful, an attribute that will help with the process could be very helpful. This attribute can be implemented similarly to the other attributes; it will just contain more parameters in its constructor. It needs a name field to track who made the change, a date field to mark when the change was made, and a text field to hold the description of the change that was made.
Given the specifics of what the attribute needs to contain (and the previous examples), this attribute is trivial to implement. There is nothing to say that has not already been said, which is why the HistoryAtribute class is listed now, in its entirety, in Example 7-10.
<AttributeUsage(AttributeTargets.Class, _ Inherited:=False, _ AllowMultiple:=True)> _ Public Class HistoryAttribute : Inherits Attribute Implements IComparable Public Author As String Public [Date] As DateTime Public Change As String Public Sub New(ByVal Author As String, _ ByVal ChangeDate As String, _ ByVal Change As String) Me.Author = Author Me.[Date] = Convert.ToDateTime(ChangeDate) Me.Change = Change End Sub Public Function CompareTo(ByVal obj As Object) As Integer _ Implements IComparable.CompareTo Dim ha As HistoryAttribute If TypeOf obj Is HistoryAttribute Then ha = CType(obj, HistoryAttribute) Return DateTime.Compare(Me.Date, ha.Date) End If End Function End Class
This class also implements IComparable (see Chapter 5), which allows an array of History attributes to be sorted based on the entry date. The implementation checks to make sure the object being passed in is a HistoryAttribute . If it is, the call is delegated to DateTime.Compare , which determines how the dates should be sorted. This will be put to use in the next example.
Save all documentation attributes to a file named myattributes.vb and compile it to a class library. Then look at Example 7-11it shows the ServerInfo class from the beginning of the chapter with several custom documentation attributes applied to it. Add the attributes and recompile.
Imports System Imports System.Diagnostics Imports System.Net Imports System.Threading 'Assumes attribute have been compiled to a library 'called myattributes.dll which is referened at compile time ' vbc /t:library /r:system.dll /r:myattributes.dll serverinfo.vb <Summary("The Amazing ServerInfo Class(tm)"), _ Remarks("This class provides simple machine info."), _ History("JPH", "04/05/2002", _ "Added ability to dump custom attributes"), _ History("JPH", "04/06/2002", _ "Broke all of the code suddenly"), _ History("JPH", "04/07/2002", _ "Fixed everything that I broke.")> _ 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(ByVal obj As Object, ByVal e As EventArgs) 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 <Param("task", "The task you want to perform")> _ Public Sub DoTask(ByVal task As Tasks) RaiseEvent TaskCompleted(Me, EventArgs.Empty) End Sub <Summary("Gets date and time on machine")> _ Public Shared Function GetMachineTime( ) As DateTime Return DateTime.Now End Function <Summary("Returns percentage of processor being used")> _ 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 <Summary("Returns available memory is megabytes")> _ Public Function GetAvailableMemory( ) As Long If PerformanceCounterCategory.Exists("Memory") Then Dim pc As New PerformanceCounter("Memory", "Available MBytes") Return pc.RawValue( ) End If Return 0 End Function <Value("Return the name of the machine")> _ Public ReadOnly Property MachineName( ) As String Get Return machine End Get End Property <Value("Returns the IP address of the machine")> _ Public ReadOnly Property IPAddress( ) As IPAddress Get Return ip End Get End Property End Class
In comparison to gathering type information using reflection, retrieving information on custom attributes using reflection is quite easy. Only two methods are needed for retrieving custom attributes:
GetCustomAttribute
GetCustomAttributes
These methods are members of System.Type and of each of the Info classes (like ConstructorInfo , MethodInfo , and PropertyInfo ). In addition, they are shared methods of the Attribute class.
Much of the code from previous examples is already in place for navigating types in an assembly (it's somewhat lacking, but it will work here), so Example 7-3 can be modified to handle custom attributes. The real work will be ordering the attribute, which are not returned in any particular order. Unfortunately, ordering the attribute means that the arrays of returned custom attributes will have to be iterated several times to get the needed information. The custom attributes are extracted in the following order:
Summary
Remarks
History
Summary
Remarks
Param
Summary
Remarks
Value
Using the ObjectInfo class from Example 7-3, a method can be added to handle each custom attribute. The first is DumpSummary , which is shown here:
Private Sub DumpSummary(ByVal o As Object) Dim summary As SummaryAttribute If TypeOf o Is Type Then summary = Attribute.GetCustomAttribute( _ CType(o, Type), GetType(SummaryAttribute), False) ElseIf TypeOf o Is MemberInfo Then summary = Attribute.GetCustomAttribute( _ CType(o, MemberInfo), GetType(SummaryAttribute), False) End If If Not summary Is Nothing Then Console.WriteLine("{0}{1}", vbTab, summary.Text) End If End Sub
DumpSummary takes an Object parameter because it must be able to handle retrieval of attributes from a Type (a class) or some derivative of MemberInfo : ConstructorInfo , MethodInfo , or PropertyInfo .
Several overloaded GetCustomAttribute methods are available, depending on the program element to which the attribute is applied. The one that works best here allows the attribute to be retrieved by name. The first parameter to the call is the Type where the custom attribute is located. The second is the Type of the attribute itself. And the third is a Boolean flag that indicates whether or not the inheritance chain should be searched for the attribute. If the summary attribute exists, the SummaryAttribute.Text property can be written out to the console.
The Remarks and Value attributes are implemented in just about the same manner as Summary because each attribute can be applied only once per entity. Value is slightly different because it can be applied only to a property, so the method responsible for it will accept only a PropertyInfo object as a parameter. The code used to extract custom Remarks and Value attributes is:
Private Sub DumpRemarks(ByVal o As Object) Dim remarks As RemarksAttribute If TypeOf o Is Type Then remarks = Attribute.GetCustomAttribute( _ CType(o, Type), GetType(RemarksAttribute), False) ElseIf TypeOf o Is MemberInfo Then remarks = Attribute.GetCustomAttribute( _ CType(o, MemberInfo), GetType(RemarksAttribute), False) End If If Not remarks Is Nothing Then Console.WriteLine("{0}{1}", vbTab, remarks.Text) End If End Sub Private Sub DumpValue(ByVal p As PropertyInfo) Dim value( ) As ValueAttribute = _ p.GetCustomAttributes(GetType(ValueAttribute), False) If value.Length Then Console.WriteLine("{0}{1}", vbTab, value(0).Text) End If End Sub
The method responsible for Param attributes requires a little more work because attributes are not ordered in the assembly. However, the Param attributes can be matched on the parameter name using reflection, so this is not really a big problem. The code for the DumpParameters method is:
Private Sub DumpParameters(ByVal m As MemberInfo, _ ByVal params( ) As ParameterInfo) Dim param As ParameterInfo Dim attribute As ParamAttribute Dim attributes( ) As ParamAttribute attributes = m.GetCustomAttributes( _ GetType(ParamAttribute), False) If attributes.Length Then Console.WriteLine(String.Format("{0}Parameters:", vbTab)) Else Return End If For Each param In params For Each attribute In attributes If String.Compare(attribute.Name, param.Name) = 0 Then Console.WriteLine("{0}{1}{2}", _ vbTab, vbTab, attribute.Name) Console.WriteLine("{0}{1}{2}", _ vbTab, vbTab, attribute.Text) End If Next attribute Next param End Sub
DumpParameters needs to handle constructors and methods, which is why the first parameter to the method is a MemberInfo object (the parent to both ConstructorInfo and MethodInfo ). The second parameter is an array of ParameterInfo objects that are retrieved before the method is called, by either DumpMethods or DumpConstructors .
This section concludes the discussion on reflection and attributes. Example 7-12 contains the final listing with all calls to the custom attribute retrieval methods in place. It is a monster of a listing! It also includes DumpHistory , which handles the history attribute from earlier in the chapter. When run, the output generated from the ServerInfo.dll assembly should look like this:
--------------------------------------------------------------------------- Class: ServerInfo2.ServerInfo The Amazing ServerInfo Class(tm) This class provides simple machine info. History: 4/5/2002 JPH Came up with this a mere two months before publishing. 4/6/2002 JPH Tried to load assembly remotely but failed 4/7/2002 JPH No one seems to know anything about auto deploy. Constructors: Public Sub New( ) Events: TaskCompleted Properties: Public ReadOnly Property IPAddress( ) As System.Net.IPAddress Returns the IP address of the machine Public ReadOnly Property MachineName( ) As System.String Return the name of the machine Methods: Public Sub DoTask(task As ServerInfo2.ServerInfo+Tasks) Parameters: task The task you want to perform Public Shared Function GetMachineTime( ) Gets date and time on machine Public Function GetProcessorUsed( ) Calculates percentage of processor being used This routine uses a sampling performance counter Public Function GetAvailableMemory( ) Returns available memory is megabytes
This example is not a pretty formatting job, but that's life in the fast lane. If everything is done already, anyone could program a computer, right?
The ObjectInfo class is woefully incomplete, mainly due to lack of space. Look at all the code in this chapter for the partial implementation. The code doesn't do events or member data, so look at EventInfo and FieldInfo if curiosity takes hold. It also lacks support for reflecting inheritance or polymorphic attributes. Furthermore, it cannot determine what interfaces are implemented by a class.
All the classes involved in pursuing a full implementation were discussed. That's the good news. Here's some more good news: these classes, and especially System.Type , are incredibly full featured (and large).
'assumes serverinfo.dll is in the same directory Imports Microsoft.VisualBasic Imports System Imports System.Reflection 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")) DumpSummary(c) DumpRemarks(c) DumpParameters(c, c.GetParameters( )) 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( )) DumpSummary(p) DumpRemarks(p) DumpValue(p) 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(")") DumpParameters(m, parms) 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) DumpSummary(m) DumpRemarks(m) End If Next m End Sub Private Sub DumpClassInfo(ByVal t As Type) Console.WriteLine(New String("-"c, 80)) Console.WriteLine("Class: {0}{1}", vbTab, t.FullName) DumpSummary(t) DumpRemarks(t) DumpConstructors(t) DumpEvents(t) DumpProperties(t) DumpMethods(t) Console.WriteLine( ) End Sub Private Sub DumpSummary(ByVal o As Object) Dim summary As SummaryAttribute If TypeOf o Is Type Then summary = Attribute.GetCustomAttribute( _ CType(o, Type), GetType(SummaryAttribute), False) ElseIf TypeOf o Is MemberInfo Then summary = Attribute.GetCustomAttribute( _ CType(o, MemberInfo), GetType(SummaryAttribute), False) End If If Not summary Is Nothing Then Console.WriteLine("{0}{1}", vbTab, summary.Text) End If End Sub Private Sub DumpRemarks(ByVal o As Object) Dim remarks As RemarksAttribute If TypeOf o Is Type Then remarks = Attribute.GetCustomAttribute( _ CType(o, Type), GetType(RemarksAttribute), False) ElseIf TypeOf o Is MemberInfo Then remarks = Attribute.GetCustomAttribute( _ CType(o, MemberInfo), GetType(RemarksAttribute), False) End If If Not remarks Is Nothing Then Console.WriteLine("{0}{1}", vbTab, remarks.Text) End If End Sub Private Sub DumpValue(ByVal p As PropertyInfo) Dim value( ) As ValueAttribute = _ p.GetCustomAttributes(GetType(ValueAttribute), False) If value.Length Then Console.WriteLine("{0}{1}", vbTab, value(0).Text) End If End Sub Private Sub DumpParameters(ByVal m As MemberInfo, _ ByVal params( ) As ParameterInfo) Dim param As ParameterInfo Dim attribute As ParamAttribute Dim attributes( ) As ParamAttribute attributes = m.GetCustomAttributes( _ GetType(ParamAttribute), False) If attributes.Length Then Console.WriteLine(String.Format("{0}Parameters:", vbTab)) Else Return End If For Each param In params For Each attribute In attributes If String.Compare(attribute.Name, param.Name) = 0 Then Console.WriteLine("{0}{1}{2}", _ vbTab, vbTab, attribute.Name) Console.WriteLine("{0}{1}{2}", _ vbTab, vbTab, attribute.Text) End If Next attribute Next param End Sub Private Sub DumpHistory(ByVal t As Type) If Not t.IsClass Then Return End If Dim history As HistoryAttribute Dim histories( ) As HistoryAttribute = _ Attribute.GetCustomAttributes _ (t, GetType(HistoryAttribute), False) If histories.Length Then Array.Sort(histories) Console.WriteLine( ) Console.WriteLine("{0}History:", vbTab) For Each history In histories Console.WriteLine("{0} {1} {2} {3}", _ vbTab, _ history.Date.ToShortDateString( ), _ history.Author, _ history.Change) Next End If 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 Public Class Application Public Shared Sub Main( ) Dim oi As New ObjectInfo("ServerInfo.dll") Console.ReadLine( ) End Sub End Class
only for RuBoard |