Main Menu Knowledgebase Serial COM Port Application.DoEvents Communication Methods Control. Invoke/BeginInvoke Delegates Download Enumeration Event Driven Sample Program Event Keywords Events and Messages Forms and Controls Interrupts Invoke, BeginInvoke and EndInvoke I/O Signals Message Posting Message Structure Multitasking and Multithreading Object Oriented Programming Program Description ReadMethod RS-232 Serial Port Class Serial Port Events Serial Port Receive Sleep Standard Delegates UART Windows COM Port |
|
||
Unfortunately Microsoft has never paid much attention to the serial port. In the Windows API it is just regarded as a file, and in the first version (1.1) of the .NET framework (managed code) there was no support for serial communication. Fortunately, a new namespace - System.IO.Ports - has been added in version 2.0, which has made things much easier although there are still some problems. For example, it is not possible to control the FIFO in the UART. It is also not possible to tell when the transmitter serial register is empty, so it is almost impossible to control the modem control signals and send a break condition, but worst of all, Microsoft has put an 8 bit wide buffer on top of the 11-bit receiver FIFO and therefore destroyed the possibility for a precise Break, 9th bit and error detection. Besides, many of the examples in the help files are directly misleading and unnecessary complicated. For example, it is recommended to use My.Computer.Ports.OpenSerialPort("COMx") to open a serial port, but if it is done that way, it is not possible to set many of the properties of the port like for example the length of the receive buffer and it is not possible to tell when a port is open (IsOpen). In many developer forums there has been a lot of questions concerning serial port communication, but unless you are
lucky enough to get in touch with the one, who has designed the serial port, it is usually very hard to get precise and
helpful answers. For example, when we wanted to use very high speed communication (up to 921.6 Kbit/s), we only got the
answer that it couldn't be done or we needed to write our own drivers!
However, it is in fact possible to use System.IO.Ports even up to 921.6 Kbit/s if a UART with a 128 byte FIFO is
used (16C850 or 16C950). As a service to others with the same problems as we have been through we have chosen to publish
a small program, which is able to communicate through the serial port. The sample program is written in Visual Basic
.NET (in the following just VB), because this language is as close as you get to our own suggestion for a simple and
efficient programming language (see: Programming). In .NET there is no longer any
performance difference between VB, C++ and C#.
|
|||
|
WARNING! This description is based on VS 2005 and .NET 2.0. Unfortunately, SerialPort does not work in all versions of .Net. To clarify the various .Net releases:
Only a few bug fixes were made to Serial Port in .Net 2.0 SP1:
And only one change were made to Serial Port in .Net 2.0 SP2:
The big problem is .Net 3.5 RTM, which includes .Net 2.0 SP1. Almost nothing in SerialPort seems to work in that version! In fact, there seems to be so many errors that .Net 3.5 RTM may be regarded as completely useless for all serial port applications! For the moment the following problems have been reported:
Microsoft thinks that the many problems with serial port in .Net 3.5 RTM may be coursed by other, unrelated(!), bug fixes that went into .Net 2.0 SP1.
Most of these problems seems to be fixed in .Net 3.5 SP1, but the problem is that nobody knows what coursed all the problems so nobody knows
whether the problems are fixed or not! For the moment it cannot be guaranteed that 3.5 SP1 works as well as 2.0.
VS 2008 allow you to select the wanted .NET version when you build your application. This is done in Advanced Compilation Options. Be sure to
select either 2.0 or 3.5 SP1!
|
||
Basically, there are two ways to do serial communication - polled and event driven. In the polled mode, the transmitter sends one or more bytes, which are typically the address of the unit to be polled. The polled unit then returns the answer, which is then displayed. If the answer is not received within a given amount of time, an error message should be displayed. If the poll and the answer are short and the polled unit answers immediately, a poll program does not necessary need multithreading as everything is done in a precise order. The polled unit does not answer before the poll has been transmitted and a new poll is not initiated before an answer is received or a timeout situation occurs. The program may therefore consist of a single subroutine with a loop, which transmits the poll (unit address), waits for the answer or a timeout, converts any non-text answer to text and then display the text. The polled mode is very simple to program, but since both the poll and the answer depends on the application it is not possible to make a general-purpose program. If you need this mode, you cannot use our program directly, but it may serve as an inspiration - especially in situations where multithreading is desiable to awoid blocking the UI thread. In the event driven mode, the transmitter and the receiver are completely asynchronous, that is, there is no connection
between the transmitter and the receiver. The receiver just displays everything, which is received. This is the mode
our program is intended for, but it may also be used in polled mode if the transmitter is looped back to the receiver
so that the poll is also received, and the transmission uses a protocol, which makes it possible to tell the start of
the telegram (distinguish between poll and answer).
|
|||
When the program is loaded, a list of the available COM ports is generated and put into a ComboBox. One of these possibilities must be selected before any communication can take place. At any time it is possible to change the COM port selection and the Baud rate. The Transmitter TextBox accept any mix of ASCII characters embedded in quotation marks and hexadecimal numbers with an even number of digits like for example """Test""" 78 90 "UU" FF76. A Carriage Return (CR or Enter) is specified as its hexadecimal value 0D and a Line Feed (LF) as 0A. Note that to transmit " in an ASCII string it is necessary to write "" - exactly as in Visual Basic. In hexadecimal mode, all spaces, CR's and LF's are ignored. In case of hexadecimal numbers with four or more digits, the numbers are transmitted with the big-endian model, that is, with the most significant part first. The Send button transmits the content of the Transmitter TextBox and the Break button generates a Break condition. The receiver TextBox always writes hexadecimal characters except for a detected Break condition, which is written as "Break". Note that because both the transmitter and the receiver regard all numbers as hexadecimal, no hex specifier is used! 63 means 63 hex (99 decimal) - not 63 decimal! There is no decimal to hex conversion in the program. The maximum telegram length is limited to 2048 bytes, but it is very easy to change this if longer telegrams are needed. The sample program is very simple and the code speaks very much for itself, but because we know that it may still seem overwhelming to absolute beginners we will describe it step by step in the following together with a detailed description of the used .NET methods and properties. Note however that some basic knowledge about subroutines, functions, data types and declarations etc. are needed to understand the description. If you are a beginner, it may easily take you days or weeks to go through and understand this totorial, but don't step to a new chapter before you understand the previous one! Take it easy, step by step. The user interface (UI) of .Net is entierly event driven and serial port uses multithreading. Unless you understand how this works, you will never be a good .Net programmer and will never be able to make reliable programs. No matter what .Net programming you need to do, you will need most of the knowledge in this totorial so forget your serial port problems for a while (the reason why you came here) and get the necessary background knowledge first. Then your serial port application will be a piece of cake to program. SerialPort is really not that difficult. For example, this is all you need to read an ASCII string and append it to the text in the Received textbox. The difficult part is to understand what you are doing! Imports System.IO.Ports Public Class MyFirstCOMProgram Public Delegate Sub StringSubPointer(ByVal Buffer As String) Dim WithEvents COMPort As New SerialPort Private Sub Receiver(ByVal sender As Object, _ ByVal e As SerialDataReceivedEventArgs) Handles COMPort.DataReceived Me.BeginInvoke(New StringSubPointer(AddressOf Display), COMPort.ReadLine) End Sub Private Sub Display(ByVal Buffer As String) Received.AppendText(Buffer) End Sub ' Initialization somewhere in the program where you open the port. COMPort.PortName = "COM1" COMPort.BaudRate = 19200 COMPort.ReadTimeout = 2000 ' COMPort.NewLine = Chr(xx) in case the telegram is not terminated with LF. Try COMPort.Open() Catch ex As Exception MsgBox(ex.Message) End Try Private Sub MyFormClosing(ByVal sender As Object, _ ByVal e As ComponentModel.CancelEventArgs) Handles MyBase.Closing If COMPort.IsOpen Then COMPort.Close() End Sub End Class |
|||
In .NET it is possible to assign values to names with the structure GroupName.Member like this: Public Enum MyColors MistyRose = &HE1E4FF& SlateGray = &H908070& DodgerBlue = &HFF901E& DeepSkyBlue = &HFFBF00& SpringGreen = &H7FFF00& ForestGreen = &H228B22& Goldenrod = &H20A5DA& Firebrick = &H2222B2& End Enum This is called an enumeration. When the compiler sees for example MyColors.MistyRose, it replaces it with the RGB value &HE1E4FF (Red = &HE1, Green = &HE4, Blue = &HFF). Note that you can only assign values like integers - not other enumerations. You can therefore not assign something like this: MyRed = colors.Red You can also use enumeration to get the value corresponding to a text string although it is more complicated. For example, you can load a ComboBox (ComboBox1) with all the names in the enumeration list like this: For Each s As String In [Enum].GetNames(GetType(MyColors)) ComboBox1.Items.Add(s) Next When you select an item from the comboBox, you can convert to the equivalent RGB value like this: Dim RGBValue As Integer = [Enum].Parse(GetType(MyColors), ComboBox1.Text) Note that in both cases, you use GetType(MyColors) to specify the group name. Constructions like this are used in the serial port program for example to select the parity and the software flow control.
It is also very useful in case of a Select-Case construction.
|
|||
.NET is based on the so-called object oriented programming (OOP). If you want to build a machine, you start by making a drawing to define how the machine shall look like and what it is able to do. You can then use this drawing to build many machines of the same type. OOP works the same way. Each machine type is called a class. When you make the "drawing", it is called to define the class, and when you build the machine, it is called to declare the object. Note that the "drawing" is the class and the actual machine is the object. Also note the difference between definition and declaration. Each new machine you build from the same drawings is called an instance of that class. Everything what the machine is able to do is called methods and all the parameters you set up on the machine to be able to define how the product shall look like are called properties. For example, a machine may be able to drill holes and cut. This is the methods. The size and position of the holes are selected by means of the properties. In fact, everything is done by means of methods. When you set or read a property, you actually call some hidden methods to do the job. For example, myControl.BackColor = Colors.Green is really a shorthand for myControl.Set_BackColor(Colors.Green). You cannot access these methods directly, but what is important to note is that setting a property is equivalent to calling a method (Set_BackColor). This is important in case of cross thread calls (described later). The argument for the method (Colors.Green) is an example of an enumeration. If you want to build a new machine similar - but not identical - to an existing one, you don't need to start all over by making a complete new set of drawings. This would also confuse things because it is difficult to tell what the difference between the two models is, and you have to change both set of drawings if a common detail is changed. Instead, you can just take an existing drawing and then specify the changes or additions you want to make. This is called to inherit a class from another class, and the new class is said to be derived from the old one. All data types are actually classes with a predefined definition. For example, a byte is defined as a storage location big enough to hold 8 bits of data. Some of the more complex data types like arrays and strings have methods. For example, you can get the length of a sting by calling the Length method like this: LengthOfString = YourString.Length VB use the keyword "As" to declare the class, that is, to specify which "drawing" to use, and it uses the keyword "New" to actually build the object. Dim MyControl As Control specifies that MyControl is a member of the control class, but it does not build the object and therefore does not consume any memory! It is basically an information to the compiler. If you try to use MyControl before the object is build, you will get a NullReference exception. MyControl = New Control builds the object MyControl. Note that MyControl has to be declared by means of an "As" statement before you can build the object. If the class has a constructor method called "New" to initialize the object, this method is called when the object is build. This is used to load a new object with initialization data like this: TextString = New String("Hello World!") In this case, "Hello World!" is passed to the constructor. The initialization data must be enclosed in parentheses following the class name. However, this is the same syntax as a function call, so in this case, the keyword "New" is also used to be able to destinguish between the returned value from a function and the build of a new object. Without this keyword, it would be impossible to tell whether String("Hello World!") is a function (String) with "Hello World!" as argument or a declaration of a new instance of the String class with "Hello World!" as (initialization) value. A constructor is always a subroutine and it is possible to have more constructors to make the input and initialization more flexible. If you for example have a class - MyClass, which may or may not require a string as input, the constructors may look like this: Public Class MyClass Public Sub New() ' Executed in case of no input Me.New("Hello World!") ' Call the other contructor with a default input End Sub Public Sub New(ByVal TextInput As String) ... ' Make any further initialization common to the two constructors End Sub .... End Class In this case, the default input "Hello World!" is used if no input is specified when the object is build. In the chapter "Delegates" of this tutorial, you will see another example of a constructor. If you wish to build an object at the time of declaration, it is possible to combine the "As" and "New" statements like this: Dim MyControl As New Control If you for example want to make a new instance of SerialPort, you can for example do it like this: Dim COMPort As New SerialPort() ' Use default initialization Dim COMPort As New SerialPort("COM3", 19200, Parity.Even, 7, StopBits.Two) Dim WithEvents COMPort As New SerialPort() ' Make it possible to fire events Dim WithEvents COMPort As New SerialPort("COM3", 19200, Parity.Even, 7, StopBits.Two) Because of multiple constructors, you can choose to use more or less of the default inputs "COM1", 9600 bit/s, No parity, 8 data bits and 1 stop bit, but only in that order. Note the use of enumeration to specify the parity and the number of stop bits. The keyword "WithEvents" makes it possible for subroutines to subscribe to events from this object (COMPort) by means of the keyword "Handles". This is described later in the chapter "Event Keywords". For simple VB data types you don't need the keyword "New". The object is build when you assign a value to it, but not before! For example: Dim TextString As String ' Declare TextString as a member of the string class Dim MyInteger As Integer ' Declare MyInteger as an integer ... TextString = "Hello World!" ' Build the string object and load it with "Hello World!" MyInteger = 361 ' Build the storage location and load it with 361 As with the "New" keyword it is possible to combine the two statements if you wish to build the object at the time of declaration and save some typing. Dim TextString As String = "Hello World!" Dim MyInteger As Integer = 361 WARNING! In .Net, all strings are immutable, meaning you cannot change its length or contents once it is declared. Therefore, all string manipulations has to be done by making a new instance of the String class, copying the string(s) and then abandon the previous string. This is a very time consuming procedure, which also leads to a serious fragmentation of the memory. Therefore, constructions like the one below should be avoided - especially for repetitive calls - even though they are possible: NewString = OldString1 & OldString2 & vbCrLf Instead, it is recommended to use a Char array to build and expand the string and then at last convert this to a string like this (some declarations and initializations not shown): Dim ReceiverArray(2047) As Char ... ReceiverArray(I) = ReceivedChar I = I + 1 ... Dim ReceivedString As New String(ReceiverArray, Offset, Length) You can also use the StringBuilder class. The immutable strings is actually one of the less clever things of .Net. A string declaration could easily
have a maximum length like arrays, but unfortunately this is not the way it is done.
|
|||
In .NET, all applications and services are build by means of one or more Forms. A form may be visible in case of applications or invisible in case of services. Each visible form represents a top-level window on the screen (TopLevel property = true). When working in the VB IDE (Integrated Development Environment), a form is the "designer" that is used to design the UI. Controls are used in the designer to create the look of the UI. A control is an object that has a predefined appearance and behavior. For example, a Button control looks and behaves like a push button. Each control in VB has its own purpose. For example, a TextBox control is used for entering and showing text, while a PictureBox control is used for displaying pictures. When designing your UI, you drag controls from the Toolbox, drop them onto a form, and then position them and resize them to create the desired look. You can further change the name, appearance and behavior of forms and controls by setting properties in the Properties window. For example, the ShowInTaskbar property of a form determines if the form will appear in the Windows taskbar when the program is running. You can also make your own controls. The object Inheritance hierarchy of forms and controls is shown below: System.Object System.MarshalByRefObject System.ComponentModel.Component System.Windows.Forms.Control System.Windows.Forms.ScrollableControl System.Windows.Forms.ContainerControl System.Windows.Forms.Form (Derived Classes) You can forget all classes except for controls and forms, but it is important to notice that forms are actually just an advanced control, so basically everything is just windows on top of windows or even more correct - controls on top of controls. In the following "window" means form or control and a "form" is a top-level window. Because all controls are derived from System.Windows.Forms.Control, they all have at least the same basic properties, methods and events. There are numerous, but a few of these are very commonly used and may be used and/or described in this tutorial: Public Properties: BackColor Public methods: BeginInvoke Protected methods: OnClick Public Events: Click |
|||
Windows is capable of keeping many programs in memory at once and give each of them an opportunity to run. This is called multitasking. Windows NT, 2000 and XP runs each program in a separate process. This is an artificial division that gives the programs mutual isolation so that no problem can indirectly affect the operation of other programs. The process is started when the program starts and exists for as long as the program is running. Each process manifest itselves as an application or a service. The difference between applications and services is that services don't usually have a user interface (in the following just UI), whereas applications do. Windows is able to run more parts (threads) of the various processes virtually simultaneously. This is called multithreading. A thread is in effect an execution pointer, which allows Windows to keep track of which line of your program is running at any one time. This pointer starts at the top of the program and moves through each line, branching and looping when it comes across decisions and loops and, at a time when the program is no longer needed, the pointer steps outside of the program code and the program is effectively stopped. Application software uses more threads primarily to deliver a better user experience, because it allows some programs like spell checkers, print spoolers etc. to run in the background. Service software uses more threads to deliver scalability and improve the service offered, for example to be able to handle more channels in parallel. Windows uses timeslicing to switch between the various threads. Because the execution of a thread is interrupted due to an external factor such as time-slicing, this is called preemptive multithreading. The run-time each thread gets before a new thread is selected is not fixed, but depends on the priority and varies dynamically to obtain the best possible performance. It is usually either one to two time slices where each time slice is approximately 10 milliseconds on a uniprocessor system and 15 milliseconds on a multiprocessor/multicore system. The run-time for each thread does not depend on the process, so a process with many threads will get more execution time than a process with only a few threads. To keep the UI responsive, all UI threads (described below) have higher priority than for example thread pool threads and most background threads. A high priority thread always preempts a lower priority thread, but to prevent low priority threads from getting starved and solve nasty priority-inversion problems, the scheduler continuously adjusts the actual priority from the programmed priority. The advantage of timeslicing is that the processes don't have to actively participate in this process and that it is not possible for one process to block the operation of other processes. The disadvantage is a fairly low efficiency compared to cooperative multitasking/multithreading where the programmer selects the ideal switching points. For example, a timeslice system may waist a lot of time on threads, which cannot utilize the full timeslot, but even worse - there is no guarantee that a subroutine or function has finished the use of some data before another part of the program can change these data. Therefore, a subroutine of function cannot just be called with a reference to the data (ByRef), but must be called with its own copy of the data (ByVal). All this data copying takes a lot of time and increases the necessary stack size. It may even lead to a serious fragmentation of the memory if the data cannot be located on the stack. Therefore, preemptive multitasking is usually used in the PC world where there is a lot of memory and a very fast CPU and where programs from many different vendors must work together, and cooperative multitasking is used in the embedded world where speed and efficiency is a primary concern. No process can write directly to the data area of another process, but threads within the same process may share the same global variables. However, more threads should of course not write to the same data area at the same time as this may create unpredictable results. In a time-sliced system there is also a risk that a method called from one thread does not finish its job before it is called again from another thread. To prevent this, .NET (unlike previous versions of VB) simply do not allow such a cross thread call except for four so-called thread safe methods - Invoke, BeginInvoke, EndInvoke and CreateGraphics - plus some hiden methods to set some properties. A piece of code is thread-safe if it functions correctly during simultaneous execution by multiple threads. In particular, it must satisfy the need for multiple threads to access the same shared data, and the need for a shared piece of data to be accessed by only one thread at any given time. Because shared data must be located somewhere and everything is running on a thread, you may also say that a method is thread safe if it is able to safely use data area (shared data) of another thread. It is not defined anywhere which properties you are allowed to set from another thread, but it is probably the ones, which may be contained in a single 32-bit value (like BackColor), since there is now way such a property can be half updated on a 32-bit system. One of the cardinal rules of Windows GUI programming is that only the thread that created a control like for example a TextBox can access its methods or should modify its contents except for simple properties like for example the background color etc. Try doing it from any other thread and you'll get unpredictable behavior ranging from deadlock, to exceptions to a half updated UI. It is very important to notice that all controls and forms have the four thread safe methods (see last chapter), so if you for example want to call BeginInvoke to invoke a method on the UI thread, you can use any of your controls on the UI thread like for example TextBox1.BeginInvoke(...) or Button2.BeginInvoke(...) or you can use your form and simply write Me.BeginInvoke(...). Every application and service has one primary thread - Main. This thread serves as the main process thread through the application or service. It's this thread that calls the Main method, which is the first code to run when your application or service has been loaded. Main serves as the starting point and overall control for your application or service. A file that runs on its own (usually with extension .exe) must contain a Main method. For a console application, you can write Main to accept user input and process them. For a GUI (Graphical User Interface) application (Windows Forms) like VB, which is entierly event driven, the compiler generates the Main method automatically. All windows (forms or controls) belong to the thread on which they are created. This thread is called the UI thread and it is usually the main thread. Threads are a limited resource. They take approximately 200,000 cycles to create and about 100,000 cycles to destroy.
By default they reserve 1 megabyte of virtual memory for its stack and use 2,000-8,000 cycles for each context switch.
It is possible to make and control threads yourself, but usually it is neither necessary nor appropriate due to the huge
use of resources. Windows has a pool of 25 general useable threads called thread pool threads, which may be used instead
for background jobs. The pool is generated the first time it is used. You may regard this pool as a company with 25
workers, which you can hire to do a job for you. If you request a thread from this pool and no thread is available,
Windows generates a new pool thread up to a total of 100 threads. Requests beyond that limit is queued until a thread
becomes available. The extra threads are destroyed as soon as they are no longer needed until the number of threads in
the pool reaches the minimum level of 25. Because it takes approximately 300,000 cycles to create and destroy a thread,
it is obvious that you should try to avoid that more thread pool threads than 25 are needed.
|
|||
It is possible to stop a thread temporary by means of System.Threading.Thread.Sleep(time). The time is specified in milliseconds, but the accuracy is much lower. If you for example try to make a 1 mS delay by means of Sleep(1), the thread is temporary suspended. 1 mS after it is put on the active queue again, but in the meantime another thread may be running and there may be more threads in the queue - even with higher priority, so it can easily take 50 mS or longer before the thread starts running again. Even if there are no waiting threads, it may take up to 15 mS - the next time slice tick - before the thread runs again. Therefore, you won't be sure when you get control back, so Sleep cannot be used for accurate timing. Sleep(0) can be useful to tell the system that you want to forfeit the rest of the thread's timeslice and let another, waiting, thread run. If there are no other threads waiting to run you still relinquish your timeslice. This is similar to the typical Release call in a cooperative multitasking system. Sleep(0) may be very useful in a game loop. It gives you the maximum possible frame rate that still doesn't completely hog the processor, allowing the system to stay responsive to for example Alt+Tab and ding-dong for email deliveries. However, you'll burn 100% CPU time, which many PC's are not designed for. Sleep(1) is similar, but keeps the CPU cool. Sleep(1) is very useful in soft real-time systems. You'll need this when you have to rely on polling to discover anything happening in the system. This is very common with imperfect drivers, which can't generate an event. If Sleep is called from the UI thread, the user interface will be completely dead for the specified amount of time.
Therefore, calling Sleep() from the UI thread is not recommended except to solve "don't know enough about
the state of the object" problems. For example, the SerialPort class has a nasty problem where you can't open a port too
quickly after closing it. There is a background thread, which needs to exit before you can successfully open the port
again. This is a flaw in the class design. Sleep() can solve this, but the solution is not perfect. The exact amount of
time you need to be sure the thread exits (the amount of time needed before the background thread gets scheduled again)
depends on system load, but because all time consuming, high priority UI jobs are
temporary suspended, Sleep(200) is usually enough although Sleep(500) is recommended for high reliability applications.
If you don't sleep the UI thread, but for example use a timer instead, you must use a considerably longer time to ensure that
the background thread gets enough time to close down - for example 2 seconds!
|
|||
It is possible to interrupt a thread by means of a so-called interrupt. When you make any action such as to click a mouse button, move the mouse, type on the keyboard, resize or move a window etc., or if you receive something on a communication port or a hardware timer runs out or generates a timer tick, the hardware raises an interrupt signal. This start a long, rather complicated chain of actions. If you are a beginner, just skip this chapter. It will just confuse you.
Thread switching is also done by means of a DPC routine. If the clock ISR (IRQL 28) has determined that a thread has reached its maximum execution time (time slice), it also calls KeInsertQueueDpc, to put the thread switching routine on the DPC queue. All thread synchronization is therefore done at dispatch level (IRQL 2), so no other thread related jobs must take place at this or higher levels. If a thread has been waiting for a timer tick, the ISR routine on IRQL 28 is therefore not allowed to release the thread directly. Instead it must post an APC routine, which can do the job. In practice, interrupt handling is even more complicated than described here - especially on a multi processor system, but these are the basic principles on a 32-bit system with all minor details omitted. On a 64-bit system, the interrupt system is similar, but with some modifications. The interrupt latency, that is, the time it takes from a hardware interrupt occur until the DPC routine is finished
(has performed its I/O), is usually below 1 mS. This is important when working with serial port because you must have a
hardware FIFO buffer (First In, First Out), which can hold all characters you receive until the DPC routine in the driver
has transfered these data to a software buffer in the driver. This software buffer must then be big enough to hold the
characters until they can be further processed and/or displayed. As this may easily take 100 mS, the software buffer in
the driver should be at least 100 times as big as the hardware buffer.
|
|||
The interrupt system (DPC routine) is not allowed to perform very time consuming or thread related operations, so all further processing must be done by non-interrupt routines. There are two ways to do this.
All methods and event handlers runs on the same thread as the one from which they are called - either directly, by
means of a delegate (described later) or by means of a raised event, which is actually the same as a delegate call!
Because the message pump runs on the UI thread, the methods it calls will do the same, and if these methods raise events
as the OnPaint method, the event handlers will also run on the UI thread. However, if an event handler
is activated by means of an event, which for example is raised from a thread pool thread for example in case of SerialPort, then the
event handler will also use the thread pool thread and it is therefore not allowed to call methods of the UI thread
except for four thread safe methods - Invoke, BeginInvoke, EndInvoke and CreateGraphics (described later).
|
|||
In two cases, it may be desiable to empty the message queue.
It is possible to empty the message queue by calling System.Windows.Forms.Application.DoEvents(). Note that it is not necessary to specify System.Windows.Forms - just Application.DoEvents(), and you may get a build error if you imports System.Windows.Forms, which is actually quite un-logical. If you use Application.DoEvents, you should be aware of two problems:
|
|||
Each message have the following structure: (hWnd As IntPtr, msg As Integer, wParam As Integer, lParam As IntPtr) HWnd is the handle for the control (window) that shall receive the message. Msg is the message ID number
like WM_CLICK. WParam is used for small pieces of information like flags and LParam is used to points to an object if
more information is needed for the message like BulletedListEventArgs and PaintEventArgs. For example, WM_MOUSEMOVE
uses wParam for the state of the ALT, Shift, CTRL and mouse buttons and lParam for the mouse coordinates.
|
|||
Windows has two basic functions, which are used to post messages - SendMessage and PostMessage. These are Win32 API routines, which you can normally forget all about, but they may be useful in some situations to send messages to controls or forms. If you are a beginner, just skip this chapter. If you for example wants to send the message WM_LBUTTONDOWN to TextBox2, you can do it like this: Imports System.Runtime.InteropServices Public Declare Auto Function SendMessage Lib "user32.dll" Alias "SendMessage" _ (ByVal hWnd As IntPtr, ByVal Msg As Integer, ByVal wParam As Integer, _ ByRef lParam As IntPtr) As IntPtr Const WM_LBUTTONDOWN As Integer = &H201 SendMessage(Me.TextBox2.Handle, WM_LBUTTONDOWN, 0, 0) PostMessage and SendMessage works differently. PostMessage just sends the message to the message queue of the target
window and returns immediately after. SendMessage blocks the caller till the message gets processed by the message pump.
The subtle but important difference is that messages sent using SendMessage aren't queued in the message queue whereas
PostMessage messages are. SendMessage messages are directly "sent" to the message pump. The message pump retrieves and
processes messages sent using SendMessage before looking into those in the message queue. Effectively, there are then two
queues, one for SendMessage messages and one for PostMessage messages (which is what we call the message queue). The
message pump processes all messages in the first queue before starting with the second. An interesting observation is
that if code does a SendMessage from within the message pumping thread, the window procedure (WndProc) gets called
directly, that is, it doesn't go through the message pump. Note that in both cases, the message is executed on the thread
that created the window - not on the thread on which PostMessage or SendMessage is called! SendMessage has a 5 seconds
timeout for the target window to process the message. If the message is not processed within this time, the call
finishes and sets ErrorLevel to the word FAIL.
|
|||
If you want to call more methods with the same set of inputdata (Data1, Data2) you can of course just write: Subroutine1(Data1, Data2) Subroutine2(Data1, Data2) ReturnValue = Function1(Data1, Data2) However, if you want the three methods to be called from a standard method build into .NET, you need another way to do it. Of course standard methods cannot call subroutines and functions, which was unknown when the method was written. You also need another way if you want to change the subroutines and functions to call dynamically while the program is running (not fixed at compilation time). .NET has a general usable way to handle this. This is the so-called delegate. A delegate is an object, which has a list called the invocation list, which is able to hold pointers to one or more methods to call (Subroutine1, Subroutine2 and Function1 in the above example). The delegate also has three methods - Invoke and BeginInvoke/EndInvoke, which you can call when you want the methods in the invocation list to be executed like: NameOfDelegate.Invoke(Data1, Data2) ReturnValue = NameOfDelegate.Invoke(Data1, Data2) Actually, you don't need to write ".Invoke". You can just use the delegate as a proxy for the Invoke method and then just write: NameOfDelegate(Data1, Data2) ReturnValue = NameOfDelegate(Data1, Data2) In this case, the syntax becomes exactly the same as a normal call, and the delegate may be regarded as a synonym for one or more subroutine(s) or function(s). Remember that a delegate may have many methods in its invocation list, which shall be executed with the same data (Data1, Data2). In case of only one method in the list, it makes sense to talk about a delegate being executed as the help files do, but it is of course not the delegate, which is executed, but the method(s) it addresses. Because such a call can only return one value, only the last method in the invocation list can be a function. However, it may be quite difficult to ensure which method is the last one so in practice all methods should be subroutines if there are more methods than one. Invoke, BeginInvoke and EndInvoke are described in more details later. In the definition of the delegate it shall be specified whether the last method to call is a function or a subroutine. In case of a subroutine, the delegate is defined as "Public Delegate Sub ..." or else it is defined as "Public Delegate Function ..." It is also necessary to specify any argument(s) to transfer and whether these should be transferred by value (ByVal) or by reference (ByRef). If we for example want to transfer a string by value to a subroutine, the whole definition becomes: Public Delegate Sub NameOfDelegate(ByVal Buffer As String) When you define a delegate, the compiler actually creates a complete class definition to support this delegate. If you for example define the following delegate: Public Delegate Function TwoInput(ByVal obj1 As Object, ByVal Str1 As String) As Integer The compiler will generate a new class that looks more or less like this code: Public Class TwoInput Inherits System.MulticastDelegate Public Sub New( _ ' Constructor with initialization data target As Object, _ ' Pointer to any object method As Int32) ' Pointer to method End Sub Public Overridable Function Invoke( _ ' Add Invoke method ByVal obj1 As Object, _ ' First argument ByVal str1 As String) _ ' Second argument As Integer ' Return value from invoked method End Sub Public Overridable Function BeginInvoke( _ ' Add BeginInvoke method ByVal obj1 As Object, _ ' First argument ByVal str1 as String, _ ' Second argument ByVal anyCallback As AsyncCallback, _ ' Optional callback delegate ByVal anyObject As Object) _ ' Optional arguments for callback As IAsyncResult ' Any returned value = Present state End Function Public Overridable Function EndInvoke( _ ' Add EndInvoke method ByVal result As IAsyncResult) _ As Integer ' Return value from invoked method End Sub End Class The new class TwoInput is derived from MulticastDelegate and as such it inherits the methods, properties and fields of this base class like the methods Combine, Remove, ToString etc., the properties Target and Method and the Previous field, which is used to link delegates together. This is used to implement the invocation list. The target object field is only used in case the delegate contains a reference to an instance method like e.g. Object1.Method1. In this case, the compiler puts a pointer to Object1 into the target field and a pointer to Method1 into the method field. If the delegate refers a shared method (no object), the compiler sets the target pointer to Null. The two properties Target and Method of the delegate refers to these two fields. AsyncCallback is an optional delegate, which points to a method, which shall be executed when the asynchronous call completes. If the callback method needs any arguments these are transferred in the following object (anyObject). IAsyncResult is an interface object, which represents the present state of an asynchronous operation and therefore can be used to monitor the progress of a BeginXXX call like BeginInvoke. If the method to invoke is a subroutine, the new Invoke and EndInvoke methods are also subroutines. If the method to invoke is a function like in this case, the Invoke and EndInvoke methods are also functions, which returns the return value of the invoked method (integer in this case). The reason why the three methods Invoke, BeginInvoke and EndInvoke are build by the compiler and not inherited from the base class multicastDelegate is that the way you call a method shall be the same whether you call it directly or by means of a delegate, and because the number of arguments varies, you cannot use a standard method or would have to combine all arguments in a single object. Control.BeginInvoke, which is described later, is a very good example of the necessary limitations if the compiler did not tailor cut the three methods to the actual needs. Use of the three methods is described in details below. Delegates are sometimes described as type-safe function pointers because they are similar to function pointers used in other programming languages like C, but unlike function pointers, which can only reference shared methods, that is, subroutines and functions that is called without a specific instance of a class, delegates can also reference instance methods (Instance.Method). "Type-safe" are usually regarded as added safety, because the signature of the delegate must match the arguments of the subroutine(s) or function it addresses, but the signature is not there for safety. As can be seen, the signature is necessary to be able to build the Invoke and BeginInvoke methods (how many arguments are needed and of which type). It is also necessary to be able to decide whether Invoke and EndInvoke shall be subroutines (no returned value) or functions. Because the Invoke and BeginInvoke methods must be able to call the methods in the invocation list in exactly the same way as a direct call, the delegate must have the same signature (number and type of arguments) as the method(s) to call. Therefore, you cannot use a delegate to point to a method with other arguments than specified in the definition. When you declare a delegate, it is in principle not different from the declaration of normal data types like Bytes, Integers, Arrays and Strings. You can even load the address of a method to call in the same way as you can load data into the various data types during declaration: Dim DisplayRoutine As NameOfDelegate = AddressOf Display ' or DisplayRoutine = New NameOfDelegate(AddressOf Display) Note that VB uses the keyword AddressOf to be able to destinguish between the address of a method
and a returned value from a function.
|
|||
.NET has many standard delegates. The most important of these are defined as: Public Delegate Sub MethodInvoker() Public Delegate Sub EventHandler(sender As System.Object, e As System.EventArgs) Public Delegate Sub SerialDataReceivedEventHandler _ (sender As Object, e As SerialDataReceivedEventArgs) MethodInvoker is used to invoke a method with no arguments at all. MethodInvoker increases the efficiency internal in .NET by cutting some corners, so it is recommended to use this delegate if you have no arguments to transfer and does not need information about the sender, which raises the event. EventHandler is used if you need information about the object that raises the event. The signature in the EventHandler delegate defines a subroutine whose first parameter (sender) is of type Object and refers to the instance that raises the event, and whose second parameter (e) is derived from type EventArgs and holds any event data. If the event does not generate event data, the second parameter is simply an instance of EventArgs. Otherwise, the second parameter is a custom type derived from EventArgs and supplies any fields or properties needed to hold the event data. Note however that control.Invoke and control.BeginInvoke (described later) set the sender to the original caller and EventArgs to EventArgs.Empty - even though you have supplied your own EventArgs, so don't use Eventhandler to marshal any call with arguments to another thread (described later)! EventHandler is used a lot in .NET. If you for example in Windows Forms designer drag a button onto your form and double click on it you will see that the designer automatically generates the following event handler for you: Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles Button1.Click End Sub As you can see, it automatically inserts the two arguments of the EventHandler delegate. The reason why these arguments are included is that Button1_Click is going to be invoked with an EventHandler delegate, and a delegate and the method to invoke must always have the same arguments. Usually you have a method, which you want to invoke, and then define a delegate to do this, but this is the other way around. Your subroutine is going to be invoked with the system delegate EventHandler, which cannot be changed, so your subroutine must have the same arguments or else you will get an error message. "Handles Button1.Click" indicates that this subroutine will handle the event Click from the sender Button1. It is possible for many subroutines to handle the same event, and it is possible for one subroutine to handle many events provided that they have the same signature. In this case, the various events are just separated with comma like "Handles Button1.Click, Button2.Click". If the keyword "Handles" is used, the sender(s) must always be declared "WithEvents" like: Friend WithEvents Button1 As System.Windows.Forms.Button This is the way Button1 is declared by Windows Forms designer in the above example. WithEvents is explained in further details in chapter "Event Keywords". SerialDataReceivedEventHandler is the delegate, which holds the event handler(s) for the DataReceived event. You can usually forget all about this delegate except if you want to put your serial port code in its own class. In that case, it may be necessary to declare a new instance of the SerialDataReceivedEventHandler and point it to your event handler to make the DataReceived event work like this: Public WithEvents COMPort As New System.IO.Ports.SerialPort Private COMDelegate As New System.IO.Ports.SerialDataReceivedEventHandler _ (AddressOf Receiver) Private Sub Receiver(ByVal sender As Object, _ ByVal e As System.IO.Ports.SerialDataReceivedEventArgs) Handles COMPort.DataReceived ... End Sub Note that the names of the standard delegates are quite confusing. Microsoft has chosen the name XxxEventHandler
for a delegate that has one or more event handlers in its invocation list - not for the name(s) of the event handler(s)
itself/themselves!
|
|||
To make it easier to work with events, VB has some event keywords such as Event, RaiseEvent, WithEvents, Handles, AddHandler, and RemoveHandler. However, they simply abstract the creation and processing of delegates. Event is just a replacement for "Delegate Sub". The delegate is always defined "Sub" because the invocation list can contain many methods (eventhandlers) so it is impossible to return one value (you never know which method is the last one in the invocation list). Therefore an event cannot be declared "... Event ... As AnythingElseThanASubDelegate". Public Class MyClass Public Event MyEvent1() ' Event with no arguments Public Event MyEvent2(ByVal Inp1 As Integer, ByVal Inp2 As Byte) ' Event with two arguments Public Delegate Sub TextDelegate(ByVal Text As String) ' Delegate with string data Public Event MyEvent3 As TextDelegate ' Event based on sub delegate End Class When the compiler encounters an event keyword in your class, it creates the event delegate (for example MyEvent1) and two public methods Add_EventName and Remove_EventName. These methods allow delegates, which point to eventhandlers, to be combined (added) or removed from the invocation list of the event delegate like this: Private Sub MyEventHandler ... End Sub Private Sub/Function ... ... AddHandler MyEvent, AddressOf MyEventHandler End Sub/Function Private Sub/Function ... ... RemoveHandler MyEvent, AddressOf MyEventHandler End Sub/Function MyEvent is the name of the event and MyEventHandler is the subroutine to be added to the invocation list of MyEvent. As you can see, eventhandlers may be added and removed while the program is running. However, if you add an eventhandler, it is very important that you remember to remove it again if you intend to dispose the object, which raise the event. The reason for this is that the automatic garbage collector of .Net does not remove an object from memory as long as it is referenced by eventhandlers. To ensure that you don't forget and to be able to specify one or more eventhandlers at compilation time, VB has two more keywords "WithEvents" and "Handles", which are used together (Handles cannot be used without WithEvents) as shown below: Public Class MainClass Private WithEvents MyInstance As MyClass ' This class includes the event MyEvent Private Sub MyEventHandler(sender As Object, e As EventArg) Handles MyInstance.MyEvent ... End Sub End Class If the event is defined in your own class, you can use the keyword Me instead of MyInstance: .... Handles Me.MyEvent It is possible for a subroutine to handle many events. In this case, the events are just specified as a comma separated list like this: .... Handles Button1.Click, Button2.Click, Button3.click Handles is just an initialization! You can later use RemoveHandler and AddHandler to remove and add your eventhandler from the invocation list. When an object is declared WithEvents, the get and set methods to reference and build and dispose the object is overrided by two new methods as shown below: Private WithEvents cmd As Button Sub Eventhandler1(...) handles cmd.Click ... End Sub Sub Eventhandler2(...) handles cmd.Click ... End Sub Sub Eventhandler3(...) handles cmd.Command ... End Sub Courses the button declaration to be changed to: Private Shared _cmd As Button ' New object with a slightly different name Private Shared Property cmd As Button ' Original object name changed to a property Get ' New Get method just points to the old one Return _cmd End Get Set(ByVal WithEventsValue As Button) ' New Set method with invocation list handling Dim handler As MouseEventHandler = New MouseEventHandler(AddressOf Eventhandler3) Dim handler2 As EventHandler = New EventHandler(AddressOf Eventhandler2) Dim handler3 As EventHandler = New EventHandler(AddressOf Eventhandler1) If (Not_cmd Is Nothing) Then RemoveHandler _cmd.MouseClick, handler RemoveHandler _cmd.Click, handler2 RemoveHandler _cmd.Click, handler3 End If _cmd = WithEventsValue ' Call the old set method If (Not _cmd Is Nothing) Then AddHandler _cmd.MouseClick, handler AddHandler _cmd.Click, handler2 AddHandler _cmd.Click, handler3 End If End Set End Property Note that the original name is changed to a property with a get method, which points to the object. This courses a slight overhead each time you reference the object, but this is the price to pay for a better program overview and reliability. EventHandler and MouseEventHandler are standard delegates so it is actually delegates (handler, handler2 and handler3), which are added to the invocation list in the AddHandler statements. The invocation list is a list of delegates, which points to the methods to be executed, but when you use AddHandler yourself, you can forget this. VB does this for you. Note what happens if you set Button to nothing, that is, dispose the object. The set method is called with null (nothing) as input, but _cmd has not yet been changed, so the three eventhandlers are removed from the invocation lists of the two events. In the following statement _cmd is set to null so the AddHandler statements are not executed. RaiseEvent is basical a replacement for Delegate.Invoke, but it also adds a check for null so that you don't invoke a delegate, which does not excist. To avoid race conditions in multithreaded environments, the check is done on a temporary variable. In C#, you have to do this check yourself. RaiseEvent MyInstance.MyEvent(AnyData) Is replaced by: Dim TempValue As MyEvent = MyInstance.MyEvent If (Not TempValue Is Nothing) Then TempValue.Invoke(AnyData) End If This executes all methods in the invocation list. Even though an event is just a delegate, you cannot invoke it directly in VB, but need to use the RaiseEvent keyword. This ensures that you always do it right by the book. Public Class MyClass Public Event MyEvent(ByVal Sender As Object) Private Sub MyEventHandler(ByVal Sender As Object) Handles Me.MyEvent ... End Sub Private Sub/Function ... ... RaiseEvent MyEvent(Me) End Sub End Class Note that the signature of the event (delegate) and the eventhandlers (methods) must always be the same.
|
|||
Once we assign an address to a delegate, we can use the Invoke or BeginInvoke method to call the subroutine or function with the wanted input data in the same way as the input data for a normal subroutine or function is also supplied in the call like MethodToCall(SomeData). There are two ways of Invoke and BeginInvoke calls - delegate.Invoke/BeginInvoke and control.Invoke/BeginInvoke.
Control.Invoke and control.BeginInvoke are for example very useful if a worker thread wants to write something to a TextBox like TextBox1.Text = "Hello World!". Because it is not allowed to call the Text method directly, it must invoke a function or subroutine on the UI thread, which can do the job for it. This is called to Marshal the call to the UI thread. Invoke, BeginInvoke, EndInvoke and CreateGraphich are the only methods of another thread that you are allowed to call directly unless you set the control.CheckForIllegalCrossThreadCalls property to false, which is not recommended! You may say that you use delegate.BeginInvoke if you want to hire a worker temporary to do a job for you. You use control.BeginInvoke or control.Invoke if you want the job to be done by a staff worker (Me). You may also say that you use BeginInvoke if you just want to give the order and handle over the key to your home, and you use Invoke if you want to be home until the job is done. Control.BeginInvoke may be called as a subroutine with no returned value or as a function. Delegate.BeginInvoke is always called as a function. In case of a function, they both return an IAsyncResult. As described previously, IAsyncResult is an interface object, which represents the present state of an asynchronous operation. It is useful if you for example want to wait until the operation is finished. The simplest way to do this is to use the EndInvoke method. As shown under delegates, this method uses the IAsyncResult as input. If the method you have invoked with BeginInvoke is a function, the EndInvoke statement will return the result of this function. In case of an exception inside the method, the exception is also passed back out through the EndInvoke call. Dim PresentState As IAsyncResult = _ [delegate or control].BeginInvoke(New Delegate1(AddressOf Display), StringToWrite) ..... ' Some code Try [delegate or control].EndInvoke(PresentState) ' Wait for end of display operation Catch ex As Exception ' Get any exceptions from the Display routine MsgBox(ex.Message & " in Display") ' Display any exception End Try [delegate or control] is the name of a delegate in case of delegate.BeginInvoke/EndInvoke and the name of a control in case of control.BeginInvoke/EndInvoke Invoke is nothing but a BeginInvoke call followed by an EndInvoke call like this: AnyReturnedValue = _ [delegate or control].EndInvoke([delegate or control].BeginInvoke(YourDelegate, AnyObject)) To ensure that control.Invoke do not get higher priority than control.BeginInvoke, Invoke and BeginInvoke both do a Windows API PostMessage call - even though Invoke is similar to a SendMessage call (a message posted with SendMessage is executed immediately and therefore has higher priority than a message posted with PostMessage). So a pair of BeginInvoke/EndInvoke calls are just as synchronous as Invoke. You may say that if you call EndInvoke to wait until the job is done, it is synchronous. If you don't, it is asynchronous. NOTE. Delegate.EndInvoke also has the function that it releases the thread to the pool when the job is done.
Therefore, delegate.BeginInvoke must always be matched with a delegate.EndInvoke call (the worker must back to the
company when the job is done). This is the reason why you cannot call delegate.BeginInvoke as a subroutine because then
there is no IAsyncResult as input for the EndInvoke statement. In case of control.BeginInvoke, it is not necessary
with EndInvoke because it uses the UI thread (or another thread with a message pump) so there is no thread to release
(job done by a staff worker).
|
|||
According to the help file in VB.NET, control.BeginInvoke executes a delegate asynchronously on the thread that the control's underlying handle was created on. It is hard to imagine a more cryptically explanation and it is not even true! First of all, a delegate is not something, which is executed. It is just an object pointing to a method. If you know what really happens, it is OK to use the shortcut and just describe it that way, but it is very confusing to a beginner, who may think of a delegate as some kind of subroutine or function since these are the only ones that are executed, and the sub or function keyword in the definition may also lead your thoughts in that direction. Second, it is not BeginInvoke, which execute the function, but WndProc, which is called from the message pump. This makes a tremendous difference. If it was the BeginInvoke method, the method would be executed immediately and therefore get higher priority than messages already waiting on the message queue, and BeginInvoke would not return before the job was done. Besides, the method would be executed on the worker thread and would therefore not be able to call methods of controls generated on the UI thread, so nothing would be gained. This would be similar to a Windows API SendMessage call, and this is not what happens! Don't feel stupid if you don't understand the help files. How can an intelligent person understand a wrong explanation? All control objects plus the form classes inherit the Invoke and BeginInvoke method from their common base class System.Windows.Forms.Control as mentioned previously, so they all have this method. You can therefore choose any control object or the form itself (Me). It is usually recommended to use the form (Me) since it is less confusing than using a control. If you for example use a control and write TextBox1.BeginInvoke, it looks like you use the BeginInvoke method of the TextBox to write the data. You don't. You just borrow its BeginInvoke method to invoke another method on the UI thread, which may or may not write to the TextBox. What you also do with the selected form or control is to use its the so-called ThreadCallbackList if it exists or else it is generated. This list is used to hold a so-called TME (ThreadMethodEntry) object, which holds all data you want to send to the UI thread including a delegate that points to the method to be executed and an object with the arguments. The TME object is also used to transfer any returned data and exceptions from the method back to the calling method (on the other thread). This is why it is called the ThreadCallbackList. Control.Invoke and control.BeginInvoke have only one or two arguments. The first one is a delegate that points to the method(s) you want to call. The second one is an object, which must hold all arguments to transfer to the method(s). As soon as you have made the Invoke or BeginInvoke call, the argument data are copied to the TME object (descibed below). This allows you to reuse the same argument object for new data. The TME object is then put on the ThreadCallbackList of the control you have selected in the Invoke or BeginInvoke call. If you for example want to send the string "StringToWrite" to the Display routine, which writes to TextBox1, a control.BeginInvoke statement may look like this: TextBox1.BeginInvoke(New NameOfDelegate(AddressOf Display), StringToWrite) ' or Me.BeginInvoke(New NameOfDelegate(AddressOf Display), StringToWrite) If you don't want to transfer anything, but just want to invoke the Display method, you can use the standard delegate MethodInvoker (or EventHandler). In this case, you don't need to define any delegate, but can just write: TextBox1.Invoke(New [MethodInvoker or EventHandler](AddressOf Display)) ' or Me.Invoke(New [MethodInvoker or EventHandler](AddressOf Display)) However, if the subroutine will be invoked with the EventHandler delegate, it must have the same signature, that is, (ByVal sender As System.Object, ByVal e As System.EventArgs). These two methods are the typical way control.BeginInvoke and control.Invoke is used in a serial port program like this one. Behind the scenes, control.Invoke and control.BeginInvoke are rather complicated and time consuming. If you are a beginner, just skip the rest of this chapter. When you call Control.BeginInvoke() or Control.Invoke() the first thing that happens is that the logic looks up the so-called marshaling control by means of the function FindMarshalingControl. The marshaling control is the first control including the current one that has a handle associated with it and therefore can be used in a Windows message (see chapter Message Structure). Usually, the marshaling control will be the one you have used in your BeginInvoke or Invoke statement, but if this control does not have a handle at the time of the BeginInvoke or Invoke call, the logic looks to the parent control, to the parent of the parent, and so on, until it finds a control, which has. If it cannot find any, it still has a way out because all threads, which have a message pump apparently, have a so-called Parking Window, which can be used. The logic will then use the marshaling control in the actual BeginInvoke or Invoke call, that is, call MarshalingControl.BeginInvoke or MarshalingControl.Invoke. The only thing these two calls do is to calls the function MarshaledInvoke. The following code shows what this routine does with all minor details like most definitions, internal flags, locks and exception handling omitted. Private function MarshaledInvoke(_ caller As Control,_ ' Original control in BeginInvoke or Invoke call method As Delegate,_ ' Delegate, which points to the method to execute methodArgs As Object,_ ' Any delegate argument - Note only one synchronous As Boolean)_ ' True if Invoke, False if BeginInvoke As Object Dim Local As Boolean = False Dim CompStack As CompressedStack = null Dim TCList As Queue ' ThreadCallbackList If (not InvokeRequired) And synchronous Then Local = True ' Control.Invoke is called on the UI thread Else CompStack = GetCompressedStack() End If TMEObject = New ThreadMethodEntry(_ ' Build ThreadMethodEntry object caller,_ method,_ methodArgs,_ synchronous,_ ExecusionContext) ' Build ThreadCallbackList if it does not exist TCList = Me.Properties.GetObject(control.PropThreadCallbackList) ' Get ThreadCallbackList If TCList = Null then TCList = new Queue() Me.Properties.SetObject(control.PropThreadCallbackList, TCList) End If ' Build a custom message if it does not excist If Control.ThreadCallbackMessage = 0 Then control.ThreadCallbackMessage = SafeNativeMethods.RegisterWindowMessage(_ Application.WindowsMessageVersion &_ ' WindowsForms20 for version 2.0 "_ThreadCallbackMessage") End If TCList.Enqueue(TMEObject) ' Queue the TME Object If Local Then Me.InvokeMarshaledCallbacks() ' Execute all TME's immediately Else UnsafeNativeMethods.PostMessage(_ ' Post the message to the message queue new HandleRef(Me, Me.Handle),_ ' Handle for the marshaling control (hWnd in message) control.ThreadCallbackMessage,_ ' The custom message 0, 0) ' no wParam or lParam arguments End If If Not synchronous Then ' Control.BeginInvoke returns here Return TMEObject End If If Not TMEObject.IsCompleted ' Wait for operation to finish Me.WaitForWaitHandle(TMEObject.AsyncWaitHandle) End If If TMEObject.Exception <> Null Then ' Throw any exceptions from delegate method Throw TMEObject.Exception EndIf Return TMEObject.retVal ' Return any value from delegate method End Function The caller is the original control used in the BeginInvoke or Invoke statement. If that control had a handle at the time of the call, the caller will be equal to the marshaling control. Me refers to the current control, that is the control used in the call of MarshaledInvoke, so "new HandleRef(Me, Me.Handle)" will return the handle of the marshaling control. It will then be this control (control.WndProc), which will receive the message from the message pump on the UI thread. Note that the delegates and the arguments for the method(s) are not transferred on the message queue. Instead each call of control.Invoke or control.BeginInvoke wraps them into a ThreadMethodEntry object together with the inputdata for the methods and a lot of other information and post this object to the ThreadCallbackList of the marshaling control (the list is build if it does not exist). The call then sends the custom message WindowsForms20__ThreadCallbackMessage to the message queue. The message pump will then dispatch the message to WndProc of the marshaling control, which will then execure the delegates in all ThreadMethodEntry objects in its ThreadCallbackList one by one by means of the InvokeMarshaledCallbacks method. In case of control.Invoke, MarshaledInvoke calls InvokeMarshaledCallBacks directly if the controls are generated on the present thread (message queue not used). Note that InvokeMarshaledCallbacks courses execution of the delegates of all TME objects in the ThreadCallbackList, so the message you post may be executed sooner as you expect if somebody calls control.Invoke on the same control from the UI thread (executed immediately) or there already is a WindowsForms20_ThreadCallbackMessage on the message queue for that control! You need to be aware than any synchronous call can possibly affect your asynchronous calls and cause them to be processed. Also you cannot expect any synchronization between events generated on the UI thread and events generated on other threads. The TME object contains 5 things:
As can bee seen, TME objects and their execusion on the UI thread involves a huge overhead if you only want to send a "character-received-beep" to the UI thread. In many ways, Windows are nothing but an object building, object disposing, byte copying machine with very little payload :-) Therefore, it is highly recommended to collect a full telegram before you send it to the UI thread. From a serial port point of view, you can easily call control.BeginInvoke each time your eventhandler for the DataReceived event runs (this is done in the sample program) because you will just transfer more bytes in case of a heavy load condition, but other Windows programs may suffer from this. In case of BeginInvoke, you may also use a global flag to limit the number of messages on the message queue (Invoke is synchronous and will not let you post a new message before the previous one is processed). Before you call BeginInvoke, you test a flag, which could be Boolean or for example a 32-bit value (there is no way a 32-bit value can be half updated due to the pre-emptive multitasking). If the flag is False or the value is Null, you set the flag or value and call BeginInvoke. If the flag is True or the value different from Null, that is, there is already a message of that type on the message queue, you don't call BeginInvoke, but you may update the value. The method on the UI thread clears the flag (False) or value (Null) when it is used. Control.Invoke and control.BeginInvoke are programmed in such a way that they are thread safe, that is, able to work
on the shared message queue and ThreadCallbackList - even if they are called from more different threads at the same
time. This is obtained by means of some locks (not shown).
|
|||
Al transmissions in and out from the serial port takes place by means of a UART (Universal Asynchronous Receiver Transmitter), which is an integrated circuit in the PC. All data are transmittet asynchronous in the so-called NRZ (Not Return to Zero) format. Each byte is transmittet as a start bit (logical 0) followed by 7 or 8 data bits with the least significant bit first, any parity bit and a stop bit (logical 1) as shown below: ---- ------- ------- ------- ------- ------- ------- ------- ------- ------- ------- ---- | | | | | | | | | | | | | Start | bit 0 | bit 1 | bit 2 | bit 3 | bit 4 | bit 5 | bit 6 | bit 7 | Pari. | Stop | | | | | | | | | | | | | ------- ------- ------- ------- ------- ------- ------- ------- ------- ------- Note that bit 7 and the parity bit are optional. In the early days, 5 or 6 data bits and/or 1.5 or 2 stop bits was also used, but this is newer used anymore and USB-Serial adapters usually do not support less than 7 bits. There are two reasons for transmitting the least significant bit first.
Advanced UART's have a FIFO (First In, First Out) buffer on the transmitter and the receiver, so that the computer can transmit and receive in bursts and do not need to respond for each byte. The standard UART in the PC (16C550) has a 16 byte transmitter FIFO and a 16 x 11-bit receiver FIFO, which not only contains the received byte, but also three flags, which indicate a break condition, that is, a condition where all bits including the stop bit are logical 0, a framing error, which indicates that the received stop bit is not logical 1 (except for break) and a parity error, which indicates an error on the odd or even parity. Advanced UART's are also able to use the parity bit as a 9th bit to distinguish between address and data and very advanced UART's like 16C950 are able to perform the address comparison automatically, which is very useful on multidrop lines. They even have a 9-bit wide transmitter FIFO to be able to transmit the 9th bit automatically. As described previously, the interrupt latency of Windows is approximately 1 mS so with a 16 x 11-bit receiver FIFO it
is able to receive up to 115.2 kbit/s. At this speed, it takes 1.4 mS to fill the FIFO. If higher transmission speeds are
needed, it is necessary to use more advanced UART's. At for example 921.6 kbit/s you need a UART with a 128 x 11-bit receiver
FIFO like 16C850 or 16C950.
|
|||
The PC uses voltage levels according to the RS-232 standard to transmit the data. A logical 0 or "on" is the active state, which corresponds to a positive voltage level between +3V and +15V, and a logical 1 or "off", which is the passive state, corresponds to a negative voltage between -3V and -15V. The default line state is logical 1 (passive) so unfortunately it is not possible to use the Break detection to determine if a unit is connected. It is very important to notice that SerialPort uses "True" as the active state so that true corresponds to logical 0 and not logical 1 as you would expect! The input impedance of a RS-232 receiver is approximately 5 Kohm. The trig level is not 0V, but approximately 1.4V
(with a little hysteresis). This makes an RS-232 input compatible with all logic families (TTL, CMOS etc.) with a
supply voltage of 3.3V or 5V. This is very useful, because it enables you to connect a modem signal input directly to a
logic signal and utilize the event (see next chapter). Note however that on old RS-232 receivers, which has a 5
KOhm serial resistor instead of a 5 KOhm pull down resistor (modern receivers), the switching times - especially the turn
off time - may be increased considerably due to the reduced input current.
|
|||
A standard 9-pin serial connector contains the following signals (pins). The signals may be set or read with the specified properties:
Note that if you set BreakState, DtrEnable and/or RtsEnable true, the output(s) will go to a positive voltage. This may be used for a simple I/O system. Note however that the drive power is very limited. The output pins are usually not able to sink or source more than approximately 35-50 mA and in some cases even lower. At 12 V, a standard RS-232 input will only draw 2.4 mA and this is what the drivers are designed for - not for use as power supply although some applications use the signals that way. It is usually recommended to set DTR and RTS true. If the connected device uses these signals, it will not transmit before the signals are set! WARNING! The methods used to set these signals use multithreading and therefore do not course the outputs to change immediately! This means that if the time between such statements are less than approximately 11 mS on a single processor system or 16 mS on a multiprocessor system, you cannot expect the time delay to be active. If you for example write: COMPort.Breakstate = True Sleep(5) COMPort.Breakstate = False you may experience that in some cases (in the order of 5%), the two COMPort.BreakState statements are executed immediately after each other, so that
the break transmission does not work! Windows is certainly not a Real Time Operating (RTO) system!
|
|||
Before a serial port can be opened, a new instance of the System.IO.Ports.SerialPort class must be made by means of a statement like this: Dim WithEvents NameOfSerialPort As New System.IO.Ports.SerialPort WithEvents means that the new instance is able to raise events like DataReceived when new data are available and PinChanged when any of the moden input signals change state. If you are only going to use SerialPort for a polled system, you may not need any events and may therefore omit WithEvents. Each instance of SerialPort can only handle one COM port at a time. Therefore, it is necessary to close one COM port before opening a new one. Note that it is necessary with a short time delay to allow the background thread to close down before you are allowed to reopen the port. If you need more COM ports to be open at the same time, you must use more instances of SerialPort. The new serial port is opened with NameOfSerialPort.Open(). When this happens, all data you have selected in the UI of your UART driver like hardware flow control, FIFO settings etc. are loaded into the driver, and SerialPort tries to set the size of the transmitter buffer and the receive buffer in the driver to the values of the properties WriteBufferSize and ReadBufferSize by means of the API function SetupComm. The default size for the transmitter buffers is 2048 bytes and the default size of the receive buffer is 4096 bytes and .NET ignores any values smaller than these values. The receive buffer should be able to hold all data until the routine, which emties the buffer, gets up and running. Because this may easily take 100 mS, it is recommanded to use a buffer at least 100 times the size of the receiver FIFO in the UART as described previously. If a 16C950 UART is used, it is therefore necessary to increase the buffer size to for example 16384 bytes. It should be noted that many drivers do not support more than 32 kbytes. After the buffer sizes are set, you start to use these buffer. Therefore the sizes cannot be changed while the port is open. The SerialPort object has yet another buffer on top of the driver buffer. This buffer is only used when using methods dealing with encodings i.e. Chars and Strings, but not bytes. When reading bytes, some bytes may be taken out of the buffer, but not put into it, so if you work only with bytes, the internal SerialPort buffer is newer used. The size of the buffer is 1024 bytes. The property BytesToRead tells the total number of bytes in the two buffers together and can therefore have a value greater than ReadBufferSize (size of buffer in UART driver). If you are calling a byte-related method like ReadByte or Read(byteArray, Offset, Count), the byte is returned directly or the bytes are put directly into your byte array as they arrives. If you are calling an encoding-related method such as ReadChar, ReadLine or ReadTo, then the bytes are first put into the internal buffer and then the char-encoded data are put into your buffer or returned as string, etc, depending on the method. Remember that .NET uses 16-bit Unicode for all strings and char's so each received byte is converted to a 16-bit value! The data from the driver buffer are not transferred to the SerialPort buffer before you call the encoding-related method. If you are receiving 16-bit Unicode characters and only one byte is received, then it is assumed that the next incomming byte will complete the character, so the first part is buffered. No half-characters are returned. ReadExisting is a special case because it creates a new buffer with a size equal to the number of bytes available. All data are read out and returned as a string. By default, strings and char's are transmitted (not coded) as 7-bit ASCII where all characters above 127 are illegal and will be replaced with "?" (code 63) - even though you have defined 8 data bits! Therefore, you cannot use a string or char related method to send or read binary information (0-255) unless you change the encoding to something, which include all values up to 255 like: YourSerialPort.Encoding = System.Text.Encoding.GetEncoding(28591) ' or YourSerialPort.Encoding = System.Text.Encoding.GetEncoding(1252) However, why involve an extra receive buffer and encoding/decoding methods if it is not necessary? If you need to transfer binary values, it is better to use a byte related receive method like ReadByte and use the only byte related send method there is - Write(ByteArray, offset, count). Unfortunately, the width of all buffers is only 8 bits so the synchronization between data, break and error conditions from the receiver FIFO in the UART is lost. It is therefore impossible to separate binary telegrams by means of a break condition or use modern 9th bit communication :-( It is important to notice, that the flow control you select in SerialPort is based on software and not always communicated to the driver. If you
intend to use a UART with hardware flow control, you should always select this in the UI of your driver by means of: Control Panel / System / Hardware /
Device Manager / Ports (expand to see available ports) / COMx (double click on wanted port). If the driver recognizes the setting of SerialPort,
it is then necessary to select "RequestToSend" in SerialPort to avoid that the original driver setting (hardware flow control) is changed. However, in
that case you actually get an undesirable double flow control - one in hardware and one in software. If the driver does not recognize the
setting, it is therefore better to set the flow control of SerialPort to "None" so that you only have the hardware control left.
|
|||
After setting the buffer sizes, SerialPort fires up a background thread, which waits for something interesting to happen on the serial port by means of the API function WaitCommEvent(). What is interesting is set by means of the API function SetCommEvent, which is called with a 9-bit mask. The possible events are:
SerialPort converts EV_RXCHAR to the DataReceived event, EV_CTS, EV_DSR, EV_RLSD and EV_BREAK to the PinChanged event and EV_ERR to the ErrorReceived event. Unfortunately, it does not use the EV_TXEMPTY event probably because it cannot be guaranteed that EV_TXEMPTY is raised when the serial output register in the UART is empty or just when the UART driver buffer before the FIFO is empty. Therefore, it is very difficult to control the modem signals and terminate a telegram with a break condition :-( In case of a DataReceived, ErrorReceived or PinChanged event, the background thread calls the API function QueueUserWorkItem(), which graps a thread from the thread pool for any further processing of that event. If no thread is available in the pool, the event is put in queue for a thread (therefore Queue...). The background thread then immediately calls WaitCommEvent() again to wait for the next interesting event. WaitCommEvents clears the event flags before it returns (of course it doesn't clear the return value), so when WaitCommEvents is called again, the new call will not return before the UART driver has set one or more of the event flags again. The new thread from the thread pool does the following:
If you need to change the COM port, it is necessary with some time delay between the Close() and Open() commands to give the system some time to close down the background thread before a new background thread is made for the new COM port. The necessary amount of time is not specified, but 200 mS is usually enough. Since the UI thread has higher priority than the background thread and thread pool threads it is necessary to stop the UI thread temporary. The easiest way to do this is to call Sleep like for example Sleep(200). To ensure that the UI is updated before the UI thread goes to sleep, you can call Application.DoEvents() before the Sleep statement to empty the message queue. It is important that you close the port before you terminate your application. If this is not done, the garbage collector of .NET may close the port while it is still in use! A very common beginners mistake is to open and close the port for each transmission, but because of the big
overhead and the necessary time delay this is not a good idea. Open the port when you have set the various properties
and don't close it again before you want to change the COM port or close your application!
|
|||
The first thing you should be aware of when designing your SerialPort application is that both control.BeginInvoke and Control.Invoke are heavy stuff and should be limited as much as possible - not just because of the huge overhead with build of TME objects, but also because the processing of these type of messages has higher priority than other messages so that too many of these calls may block the UI. Because most (all) practical applications are based on a communication protocol, which combines a number of bytes into a telegram and makes it possible to separate the various telegrams and detect errors, it is highly recommended to read the entire telegram in the event handler for the DataReceived event before you send it to the UI thread. In this way, you will limit the number of Invoke or BeginInvoke calls and will utilize the multithreading and not block the UI thread while you wait for bytes, perform error detection and reject any erroneous telegrams (telegram reception done on a thread pool thread). If you use ASCII and a very simple protocol where each telegram is just terminated with a given character - usually CR or LF, you can just call ReadLine. However, because of the lock in SerialPort you should be aware that you cannot close the port if the event handler for the DataReceived event doesn't exit (Close will hang). You should be aware of this if you use blocking calls like ReadLine or ReadTo(Value) or call ReadByte in a loop without checking BytesToRead. In these cases, you should always specify a timeout for data reception (ReadTimeout) to ensure that your handler always exits. Note that the reason for the ReadTimeout is to be able to close the port - not to make a receive timeout! This is a common misunderstanding. You cannot use ReadTimeout as a timeout together with the DataReceived event because until the first byte is received, the event handler is not running and therefore no receive method is called so the timeout is not active. The default termination character for ReadLine is LF (Line Feed), but it is easy to change by means of the NewLine property. If you for example wants to change it to CR (Carrige Return) you can use the following code: COMPort.NewLine = Chr(13) ... COMPort.ReadLineYou can also use: COMPort.ReadTo(Chr(13)) Behind the scenes, ReadLine and ReadTo are in fact the same method, but if you change the NewLine property it will also affect WriteLine, which appends the NewLine character to the end of the transmitted string. Note that ReadLine and ReadTo strip off the NewLine character before the string is returned! Note that Hayes modem commands (AT commands) are terminated with CR instead of LF. When a full telegram has been received, you can send it to a method on the UI thread by means of control.BeginInvoke or control.Invoke. The are pro's and contras of both methods:
Previous versions of the sample program (up to February 19th 2010) used control.BeginInvoke for simplicity, but due to the higher efficiency of control.Invoke in case of heavy load conditions, the newer versions use that. WARNING! SerialPort seems to have a bug, which courses it to fill up the receive buffer with 00 bytes. You can trigger this bug by means of
a wrong baud rate, but unfortunately it has not been possible to find an exact trigger sequence.
|
|||
Because the help files does not describe how Read actually works, it is logical to assume that the method: BytesReceived = YourCOMPort.Read(buffer, offset, count) is a blocking method, which does not return before "count" number of bytes are received. It is not!. If there
are bytes available on the serial port, Read returns up to "count" bytes, but will not block (wait) for the remaining
bytes. If there are no bytes available on the serial port, Read will block until at least one byte is available on the
port, up until the ReadTimeout milliseconds have elapsed, at which time a TimeoutException will be thrown.
|
|||
The sample program has been designed for very high-speed operation with maximum efficiency as Max-i goes up to 1.843 Mbit/s and is able to transfer up to 10,000 telegrams per second where several hundreds may be passed to the PC. Because of this, not everything is made quite by-the-book. For example, the program uses global buffers, which are usually not recommended for object oriented programming. For low speed operation, you may instead define a delegate to transfer the received telegram to the UI thread and use BeginInvoke instead of Invoke. The statement "Dim WithEvents COMPort As New SerialPort" makes a new instance of System.IO.Ports.SerialPort. Note that because the namespace System.IO.Ports is imported in the beginning of the program it is only necessary to specify "SerialPort" (instead of System.IO.Ports.SerialPort). The Receiver subroutine is activated when the SerialPort object receives some data. Note that this activation is only possible because "WithEvent" has been specified in the object definition. The purpose of the Receiver subroutine is to collect a data package and then send this package to the Display subroutine for display. The type of data package depends on the application. In this case, a package is just all available bytes converted to a text string. For applications where each data package is terminated with a specified character, the whole Receiver subroutine may be replaced with: Dim Buffer As String Private Sub Receiver(ByVal sender As Object, Byval e As SerialDataReceivedEventArgs)_ Handles COMPort.DataReceived Buffer = COMPort.readLine Me.Invoke(New MethodInvoker(AddressOf Display)) End Sub Or in case you prefer BeginInvoke and a delegate: Public Delegate Sub StringSubPointer(ByVal Buffer As String) Private Sub Receiver(ByVal sender As Object, Byval e As SerialDataReceivedEventArgs)_ Handles COMPort.DataReceived Me.BeginInvoke(New StringSubPointer(AddressOf Display), COMPort.Readline) End Sub Because you will use a blocking call (ReadLine) in this case, remember to specify a read timeout to ensure that you always are able to close the port: COMPort.ReadTimeout = 5000 ' 5 sec. timeout To avoid repetitive string operations, the Receiver subroutine receives all bytes in an array and then converts the data to a string by means of the "Dim RxString As New String(RxArray, 0, I)" statement. This is the reason for not using the automatic hex to string conversion of VB, which only works on strings, which then have to be added together. Besides, the used look-up table method is approximately 43 times faster than the build-in String.Format method! This is worth thinking about! Don't use standard function if you can replace them with simple constructions like this! Because the Receiver subroutine handles COMPort.DataReceived, it uses a thread pool thread, so Receiver is not allowed to display the data itself (write to the Received TextBox) because this would course an illegal cross thread call. Therefore, we use Invoke to marshal the call to the UI thread. The Display subroutine just appends the received text to any content of the Received TextBox. For other applications like SCADA systems it may be desirable to overwrite the data instead by means of "Received.Text = (...)" instead of "Received.AppendText(...)" . Display is event driven, but since it do not ends with a "Handles xxx", it needs an Invoke or BeginInvoke statement to tell when to start. The Transmitter subroutine transmits the content of the Transmitted TextBox when the Send button is pressed (handles SendButton.Click) and it appends "TX" plus a line feed to the Received TextBox. It primary consists of a state-action machine to handle the combined HEX and ASCII input and the quotation marks. If the port is not open, an error message is generated instead of the transmission. The transmitter has a 2 sec. timeout in case of troubles with the flow control (handshake). The subroutine PortSelection is activated in case of a change in the selected COM port. The routine closes any open port, empties the message queue, waits 0.2S and then opens the new port. The Application.DoEvents() call is necessary to update the ComboBox and the modem control "lamps" before the UI thread goes to sleep. The MaxiTesterLoad subroutine is activated when the form is loaded. It detects all available COM ports and loads them into a ComboBox. It then loads the mostly used baud rates, the possible number of data bits and the parity and flow control settings into other ComboBoxes. It also switches the modem control "lamps" off. The MaxiTesterClosing subroutine first shows a dialog box to ensure that the program is not closed by accident. If closing is confermed, it closes any open port before the application terminates. Note that to prevent the deadlock mentioned in chapter "SerialPort Receive", the port is closed on a new thread. The ClearReceivedText subroutine clears the Received TextBox, SaveText saves the content into a file and SendBreak generates a break condition. The subroutines BaudRateSelection, DataBitsSelection, ParitySelection and SoftwareFlowControlSelection are used to select the speed, number of data bits, parity and flow control (handshake). Unfortunately, VB does not have a FromString (corresponding to ToString) method to convert from text to enumeration (integer) therefore it is necessary with a rather complicated type conversion. The subroutine ModemLamps updates the modem control signal "lamps". Note that since we just change a simple
property - the background color, it can be done from a thread pool thread without coursing an illegal cross thread error.
|
|||
Click here to download the source code. Click here to download MaxiCOM.exe alone. Note that the file structure of a VB program is extremely confusing, because the word 'Project' is used for two different things, and two directories by default have the same name. When you generate a new project you get the following default directory structure (only the most important files are shown): \Visual Studio 2005\ Projects\ YourProject\ YourProject.sln YourProject\ Form1.vb ' Source file Form1.Designer.vb ' Designer source file Form1.resx ' .NET Resource file YourProject.vbproj YourProject.vbproj.user bin\ debug\ YourProject.exe ' Exe file for debugger (may also be published) release\ YourProject.exe ' Exe file to be included in the solution However, the directory should actually look like this: \Visual Studio 2005\ Solutions\ ' Note Solutions - not Projects Solution1\ Solution1.sln Project1\ Form1.vb ' Source file Form1.Designer.vb ' Designer source file Form1.resx ' .NET Resource file Project1.vbproj ' Project definition file Project1.vbproj.user bin\ debug\ Project1.exe ' Exe file for debugger (may also be published) release\ Project1.exe ' Exe file to be included in the solution Project2\ ' Any second project included in the solution Form1.vb Form1.Designer.vb Form1.resx Project2.vbproj Project2.vbproj.user bin\ debug\ Project2.exe release\ Project2.exe Solution2\ Solution2.sln Project3\ Form1.vb Form1.Designer.vb Form1.resx Project3.vbproj Project3.vbproj.user bin\ debug\ Project3.exe release\ Project3.exe Project4\ Form1.vb Form1.Designer.vb Form1.resx Project4.vbproj Project4.vbproj.user bin\ debug\ Project4.exe release\ Project4.exe A so-called VB project is really a superior solution, which may contain many projects, so the Projects folder does not contain projects, but solutions. If you have any comments or suggestions, please let us know.
|