Find the serial port settings in Delphi - delphi

Hi I have the need to find the Baud rate and other settings for a serial port, Looking about on the web, it looks like I should be using GetCommConfig, This returns a TCommConfig record with what I assume is the data I need. The problem is the function I wote returns the wrong values.
The code below looks like it is working, but the baud rate is always 1200, which looking in windows device manager (and altering port settings), is wrong.
I have tried calling it like so:
ComPort('com1');
ComPort('COM1');
ComPort('COM1:');
ComPort('COM4');
ComPort('COM9');
the first 4 are valid but return 1200 and the 5th is invalid and returns 0
function ComPort(l_port:String):TCommConfig;
{Gets the comm port settings}
var
ComFile: THandle;
PortName: array[0..80] of Char;
size: cardinal;
CommConfig:TCommConfig;
begin
FillChar(Result, SizeOf(TCommConfig), 0);//blank return value
try
StrPCopy(PortName,l_port);
ComFile := CreateFile(PortName,GENERIC_READ or GENERIC_WRITE,0,nil,OPEN_EXISTING,0{ FILE_ATTRIBUTE_NORMAL},0);
try
if (ComFile <> INVALID_HANDLE_VALUE) then
begin
FillChar(CommConfig, SizeOf(TCommConfig), 0);//blank record
CommConfig.dwSize := sizeof(TCommConfig);//set size
//CommConfig.dcb.DCBlength := SizeOf(_dcb);
size := sizeof(TCommConfig);
if (GetCommConfig(ComFile,CommConfig,size)) then
begin
Result := CommConfig;
end;
end;
finally
CloseHandle(ComFile);
end;
except
Showmessage('Unable to open port ' + l_port);
end;
end;
Stepping through the code, the first 4 always hit the line Result := CommConfig;, so the GetCommConfig is retuning a valid code, so I must be missing something.
I have tryed verious other things, such as setting the length of the dcb record, but all have the same result, as baud of 1200.
Does anyone know where I am going wrong?

The baud rate and other settings for a serial port, are set when the serial port is opened.
I think you are reading default values.

It turns out I was using the wrong function, I should have been using GetDefaultCommConfig and not the GetCommConfig that I was using.
By the look if it, and please correct me if I am wrong, GetDefaultCommConfig returns the settings from windows and GetCommConfig returns the settings of the open connection to the port, writefile opens the port up as it see fit (ignoring the default settings), which is where the 1200 baud rate was coming from.
If this helps anyone in the future, here is the function I came up with.
function ComPort(l_port:String):TCommConfig;
{Gets the comm port settings (use '\\.\' for com 10..99) }
var
size: cardinal;
CommConfig:TCommConfig;
begin
FillChar(Result, SizeOf(TCommConfig), 0);
//strip trailing : as it does not work with it
if (RightStr(l_port,1) = ':') then l_port := LeftStr(l_port,Length(l_port)-1);
try
FillChar(CommConfig, SizeOf(TCommConfig), 0);
CommConfig.dwSize := sizeof(TCommConfig);
size := sizeof(TCommConfig);
if (GetDefaultCommConfig(PChar(l_port),CommConfig,size)) then
begin
Result := CommConfig;
end
//if port is not found add unc path and check again
else if (GetDefaultCommConfig(PChar('\\.\' + l_port),CommConfig,size)) then
begin
Result := CommConfig;
end
except
Showmessage('Unable to open port ' + l_port);
end;
end;

Related

IdHTTP Post returns connection reset by peer for Streams over 32KB

I have a problem posting to a web server using HTTPS. I am not sure if the problem is with me or with the server. So it appeared that if I try to post a Stream greater than 32KB, Delphi crashes with Socket Error 10054 - Connection reset by peer.
I am using Delphi XE5 with the internal version of Indy and latest to date open ssl dlls.
I also try this on XE with latest to date Indy and ssl dlls.
Here is part of my code
function TForm1.SendItemsList(aDataList: TStringList): Boolean;
var
aHTTP: TIdHTTP;
aRes: String;
aURL: String;
aErrMsg: String;
aStrm: TMemoryStream;
aResStrm: TMemoryStream;
aXML: TNativeXML;
aTmpNode: TXmlNode;
aErrNode: TXmlNode;
aList: TList;
i: Integer;
begin
Result := False;
aStrm := TMemoryStream.Create;
aResStrm := TMemoryStream.Create;
aXML := TNativeXML.Create(nil);
aHTTP := CreateHTTP('application/x-www-form-urlencoded');
try
aDataList.SaveToStream(aStrm);
aStrm.Position := 0;
aURL := Format(cIRPURL, ['1']);
try
aHTTP.Post(aURL, aStrm, aResStrm);
aResStrm.Position := 0;
aXML.LoadFromStream(aResStrm);
aTmpNode := aXML.Root.FindNode('ResponseCode');
if aTmpNode <> nil then
begin
if aTmpNode.Value <> '0' then
begin
aErrNode := aXML.Root.FindNode('ResponseText');
aErrMsg := '';
if aErrNode <> nil then
aErrMsg := aErrNode.Value;
aList := TList.Create;
try
aXML.Root.FindNodes('Detail', aList);
for i := 0 to aList.Count-1 do
begin
aErrMsg := aErrMsg+#13#10+TXmlNode(aList[i]).Value;
end;
finally
aList.Free;
end;
end;
end;
except
on E:Exception do
begin
if E is EIdHTTPProtocolException then
aErrMsg := E.Message + #13#10 + (E as EIdHTTPProtocolException).ErrorMessage
else
aErrMsg := E.Message;
Exit;
end;
end;
finally
aXML.Free;
aStrm.Free;
aHTTP.Free;
aResStrm.Free;
end;
Result := True;
end;
Where CreateHTTP looks like
function TForm1.CreateHTTP(aContentType: String): TIdHTTP;
begin
Result := TIdHTTP.Create(nil);
Result.ConnectTimeout:=60000;
Result.ReadTimeout:=90000;
Result.ProtocolVersion:=pv1_1;
Result.HTTPOptions := [hoForceEncodeParams];
Result.HandleRedirects:=True;
Result.IOHandler := SSLHandler;
SSLHandler.ReadTimeout := 30000;
Result.Request.Accept:='*/*';
Result.Request.AcceptLanguage:='en-US';
Result.Request.ContentType:=aContentType;
Result.Request.CharSet:='utf-8';
Result.Request.UserAgent := 'Mozilla/5.0';
end;
All these timeouts exist just because I was testing why I get that error. Then I realized that the problem is when the stream to be sent is larger than 32KB.
I can't really say that there is something wrong with the code at all, because in the same way I send data to several other services like Amazon and Walmart for example where I send sometimes megabytes and I don't receive any errors.
The server is IIS but I don't know what version, the support doesn't seem to believe me that I am doing everything OK.
What I notice is that the SSL handler has some default buffer sizes - SendBufferSize and RecvBufferSize which default to 32KB. Well I tried setting that to 1MB but still I get the same error.
If I send something which is less than 32KB then everything is OK. The error is returned immediately after line with POST is executed - there is no delay, just immediate error. Otherwise sending small streams results in having a delay of a second or two before it gets processed and then the debugger goes to the next line. I started believing it is a setting of the IIS and there is really such a setting, but the guys there say everything on their side is ok and they have 4MB of limit for the requests.
The service provider is IRPCommerce but unfortunately I can't give links for testing because of IP filtering which takes place at the moment there.
I spent several days discussing this with them, searching the web for problems any limitations etc.
So is there something I am missing here, any limitations in Indy which may cause this problem, I doubt but just to be sure I am asking? Anything else I can do to make it clearer where the problem might be?
EDIT:
Here is excerpt of the aDataList:
Stock_ExternalStockID|Brands_Active|Brands_Brand|Models_Active|Models_Model|Models_Description|Models_AdditionalInformation1|Models_AdditionalInformation2|Stock_DisplayOrder|Stock_Option|Stock_Price|Stock_RRP|Stock_SupplierCost|Categories_Active|Categories_Name|Stock_PostageWeight|Stock_PartCode|Stock_ISBNNumber|Stock_UPCAPartCode|Stock_EAN13PartCode|Models_ImageURLs|OptionSelector|OptionSelectorAttributes|OptionSelectorCount|Stock_OutOfStockStatus
17664-00001|TRUE|Polypads|TRUE|Polypads Plus One Outsider Pet Bed|<ul><li>The perfect pet bed for any animal around the house or for covering car seats or boots for travelling. </li><li>Convenient to use. </li><li>6cm Plus One thickness. </li><li>Fully machine washable and quick drying. </li><li>As there is such an extensive range of colours available for the Polypad collection many colour combinations will have to be ordered in specifically; this service could take up to two weeks. </li><li>If you do not have any specific colours in mind please select, Colour Not Important, from the drop down menu.</li></ul>| | |10|Royal Blue-Navy|43.95|48.99|21.69|TRUE|Dog Beds|1000|160||||https://saddlery.biz/media/catalog/product/o/u/outp1.jpg|1|21,44|2|10
17664-00002|TRUE|Polypads|TRUE|Polypads Plus One Outsider Pet Bed|<ul><li>The perfect pet bed for any animal around the house or for covering car seats or boots for travelling. </li><li>Convenient to use. </li><li>6cm Plus One thickness. </li><li>Fully machine washable and quick drying. </li><li>As there is such an extensive range of colours available for the Polypad collection many colour combinations will have to be ordered in specifically; this service could take up to two weeks. </li><li>If you do not have any specific colours in mind please select, Colour Not Important, from the drop down menu.</li></ul>| | |20|Soft Blue-Royal Blue|43.95|48.99|21.69|TRUE|Dog Beds|1000|160||||https://saddlery.biz/media/catalog/product/o/u/outp1.jpg|1|21,44|2|10
17664-00003|TRUE|Polypads|TRUE|Polypads Plus One Outsider Pet Bed|<ul><li>The perfect pet bed for any animal around the house or for covering car seats or boots for travelling. </li><li>Convenient to use. </li><li>6cm Plus One thickness. </li><li>Fully machine washable and quick drying. </li><li>As there is such an extensive range of colours available for the Polypad collection many colour combinations will have to be ordered in specifically; this service could take up to two weeks. </li><li>If you do not have any specific colours in mind please select, Colour Not Important, from the drop down menu.</li></ul>| | |30|Black-Purple|43.95|48.99|21.69|TRUE|Dog Beds|1000|160||||https://saddlery.biz/media/catalog/product/o/u/outp1.jpg|1|21,44|2|10
Here I have 165 rows. If I send about 40 of them they go, just because 40 are just about 32KB. I have confirmed that data is not the problem, because I have tried sending one by one each of the lines.
I tried multipart/form-data with no luck. Actually they haven't told me what to use, no matter how much times I have asked about that, so I used the same thing I am using with Walmart.
I think the server is IIS 8.5.
It seems that the "solution" is to change the Content-type to text/xml. None of the other mentioned content-types work with streams larger than 32KB. At the same time I got a confirmation from the developers of the site that the content-type is not considered at all on the server side.
So I am really confused what is going on here and why only 'text/xml' works fine.

Delphi7, Save User's Changes or other User's Information / Notes

In my program, the user completes a form and then presses Submit. Then, a textfile or a random extension file is created, in which all the user's information is written. So, whenever the user runs the application form, it will check if the file, which has all the information, exists, then it copies the information and pastes it to the form. However, it is not working for some reason (no syntax errors):
procedure TForm1.FormCreate(Sender: TObject);
var
filedest: string;
f: TextFile;
info: array[1..12] of string;
begin
filedest := ExtractFilePath(ParamStr(0)) + 'User\Identity\IdentityofMyself.txt';
if FileExists(filedest) then
begin
AssignFile(f,filedest);
Reset(f);
ReadLn(info[1], info[2], info[3], info[4], info[5], info[6], info[7],
info[8], info[9], info[10], info[11], info[12]);
Edit1.Text := info[1];
Edit2.Text := info[2];
ComboBox1.Text := info[3];
ComboBox5.Text := info[4];
ComboBox8.Text := info[4];
ComboBox6.Text := info[5];
ComboBox7.Text := info[6];
Edit3.Text := info[7];
Edit4.Text := info[8];
Edit5.Text := info[11];
Edit6.Text := info[12];
ComboBox9.Text := info[9];
ComboBox10.Text := info[10];
CloseFile(f);
end
else
begin
ShowMessage('File not found');
end;
end;
The file exists, but it shows the message File not found. I don't understand.
I took the liberty of formatting the code for you. Do you see the difference (before, after)? Also, if I were you, I would name the controls better. Instead of Edit1, Edit2, Edit3 etc. you could use eFirstName, eLastName, eEmailAddr, etc. Otherwise it will become a PITA to maintain the code, and you will be likely to confuse e.g. ComboBox7 with ComboBox4.
One concrete problem with your code is this line:
readln(info[1], info[2], info[3], info[4], info[5], info[6], info[7],
info[8], info[9], info[10], info[11], info[12]);
You forgot to specify the file f!
Also, before I formatted your code, the final end of the procedure was missing. Maybe your blocks are incorrect in your actual code, so that ShowMessage will be displayed even if the file exists? (Yet another reason to format your code properly...)
If I encountered this problem and wanted to do some quick debugging, I'd insert
ShowMessage(BoolToStr(FileExists(filedest), true));
Exit;
just after the line
filedest := ...
just to see what the returned value of FileExists(filedest) is. (Of course, you could also set a breakpoint and use the debugger.)
If you get false, you probably wonder what in the world filedest actually contains: Well, replace the 'debugging code' above with this one:
ShowMessage(filedest);
Exit;
Then use Windows Explorer (or better yet: the command prompt) to see if the file really is there or not.
I'd like to mention an another possibility to output a debug message (assuming we do not know how to operate real debugger yet):
{ ... }
filedest := ExtractFilePath(ParamStr(0)) + 'User\Identity\IdentityofMyself.txt';
AllocConsole; // create console window (uses Windows module) - required(!)
WriteLn('"' + filedest + '"'); // and output the value to verify
if FileExists(filedest) then
{ ... }

Identifying the server from which a Delphi 7 program is run

Separate versions of a Delphi 7 program have been deployed on various servers.
In order to help troubleshoot reported errors, I'm trying to write a function to identify what server the program is running from.
The following code gets me the local computer name.
sbAll.Panels.Items[1].Text := 'Server: ' + GetEnvironmentVariable('COMPUTERNAME');
Assuming that the absolute path of the program is:
\\Swingline\Programs\Folder\Program.exe
How do I get it to return Server: Swingline regardless of what computer it is run from?
You can probably use Application.ExeName, split it by the slashes and get the second element...
This is the code I ended up using based on #Zdravko's suggestion.
List := TStringList.Create;
try
ExtractStrings(['\'], [], PChar(Application.ExeName), List);
if (List.Text[2] = ':') then // On local computer, Ex. J:\Programs\Foo.exe
sbAll.Panels.Items[1].Text := 'Server: ' + ntComputer.ComputerName
else // In the case of \\Swingline\Programs\Folder\Program.exe
sbAll.Panels.Items[1].Text := 'Server: ' + UpperCase(List[0]);
finally
List.Free;
end;
You can do this without using a string list...
function ExeLocation: String;
var
S: String;
begin
S:= ParamStr(0);
if Copy(S, 2, 2) = ':\' then begin
Result:= GetEnvironmentVariable('COMPUTERNAME');
end else
if Copy(S, 1, 2) = '\\' then begin
Delete(S, 1, 2);
Result:= Copy(S, 1, Pos('\', S)-1);
end;
end;
Keep in mind that if you are referencing the file by the machine's IP address, this will only return the IP address. For example \\192.168.1.123\SomeFolder\SomeFile.exe would just return 192.168.1.123. I looked for other ways but I'm not knowledgeable enough in that department to dig deep enough for the true machine name. It might be possible, but I'm just not seeing it possible.

Delphi Indy Ping Error 10040

I have a small piece of code that checks if a computer is alive by pinging it. We use to have a room with 40 computer and I wanna check remotely through my program which on is alive.
Therefore I wrote a little ping function using indy
function TMainForm.Ping(const AHost : string) : Boolean;
var
MyIdIcmpClient : TIdIcmpClient;
begin
Result := True;
MyIdIcmpClient := TIdIcmpClient.Create(nil);
MyIdIcmpClient.ReceiveTimeout := 200;
MyIdIcmpClient.Host := AHost;
try
MyIdIcmpClient.Ping;
Application.ProcessMessages;
except
Result := False;
MyIdIcmpClient.Free;
Exit;
end;
if MyIdIcmpClient.ReplyStatus.ReplyStatusType <> rsEcho Then result := False;
MyIdIcmpClient.Free;
end;
So I've developped that at home on my wifi network and everthing just work fine.
When I get back to work I tested and I get an error saying
Socket Errod # 10040 Message too long
At work we have fixed IPs and all the computer and I are in the same subnet.
I tried to disconnect from the fixed IP and connect to the wifi which of course is DHCP and not in the same subnet, and it is just working fine.
I have tried searching the internet for this error and how to solve it but didn't find much info.
Of course I have tried to change the default buffer size to a larger value but it didn't change anything I still get the error on the fixed IP within same subnet.
Moreover, I don't know if this can help finding a solution, but my code treats exceptions, but in that case it takes about 3-4 seconds to raise the error whereas the Timeout is set to 200 milliseconds. And I cannot wait that long over each ping.
By the way I use delphi 2010 and I think it is indy 10. I also have tested on XE2 but same error.
Any idea
----- EDIT -----
This question is answered, now I try to have this running in multithread and I have asked another question for that
Delphi (XE2) Indy (10) Multithread Ping
Set the PacketSize property to 24:
function TMainForm.Ping(const AHost : string) : Boolean;
var
MyIdIcmpClient : TIdIcmpClient;
begin
Result := True;
MyIdIcmpClient := TIdIcmpClient.Create(self);
MyIdIcmpClient.ReceiveTimeout := 200;
MyIdIcmpClient.Host := AHost;
MyIdIcmpClient.PacketSize := 24;
MyIdIcmpClient.Protocol := 1;
MyIdIcmpClient.IPVersion := Id_IPv4;
try
MyIdIcmpClient.Ping;
// Application.ProcessMessages; // There's no need to call this!
except
Result := False;
Exit;
end;
if MyIdIcmpClient.ReplyStatus.ReplyStatusType <> rsEcho Then result := False;
MyIdIcmpClient.Free;
end;
For XE5 and Indy10 this is still a problem, even with different Packet Size.
To answer the more cryptical fix:
ABuffer := MyIdIcmpClient1.Host + StringOfChar(' ', 255);
This is a "magic" fix to get around the fact that there is a bug in the Indy10 component (if I have understood Remy Lebeau right).
My speculation is that this has some connection with the size of the receive buffer. To test my theory I can use any character and don't need to include the host address at all. Only use as many character you need for the receive buffer. I use this small code (C++ Builder XE5) to do a Ping with great success (all other values at their defaults):
AnsiString Proxy = StringOfChar('X',IcmpClient->PacketSize);
IcmpClient->Host = Host_Edit->Text;
IcmpClient->Ping(Proxy);
As you can see I create a string of the same length as the PacketSize property. What you fill it with is insignificant.
Maybe this can be of help to #RemyLebeau when he work on the fix.
use this code
ABuffer := MyIdIcmpClient1.Host + StringOfChar(' ', 255);
MyIdIcmpClient.Ping(ABuffer);

Delphi: How to send MIDI to a hosted VST plugin?

I want to use VST plugins in my Delphi program which acts as a VST host. I have tried the tobybear examples, used the delphiasiovst stuf, got some of it even working, but... I don't know how to send MIDI messages to the plugin (I am aware that most plugins will not handle MIDI, but I have an example plugin that does).
To be more specific: I expect that when I send a MIDI message, I have to either use one or other method in the VST plugin or reroute the MIDI output. I just don't know how.
Can anyone point me to documentation or code on how to do this? Thanks in advance.
Arnold
I use two test plugins: the one compiled from the DelphiAsioVst package and PolyIblit. Both work in Finale and LMMS. Loaded into my test program both show their VST editor.
I did insert the TvstEvent record and initialized it, I inserted the MIDIData and the AddMIDIData procedures and a timer to provide test data and to execute the ProcessEvents routine of the plugin. ProcessEvents gets the correct test data, but no sound is heard. I hear something when I send it directly to the midi output port.
In the code below the PropcessEvents should be sufficient imho, the additional code is a test whether the MIDI information is correctly sent. VstHost [0] is the first plugin, being either the PolyIblit or the VSTPlugin, depending on the test.
procedure TMain_VST_Demo.TimerTimer (Sender: TObject);
var i: Int32;
begin
// MIDIOutput.PutShort ($90, 60, 127);
MIDIData (0, $90, 60, 127);
if FMDataCnt > 0 then
begin
FMyEvents.numEvents := FMDataCnt;
VSTHost[0].ProcessEvents(#FMyEvents);
// if (FCurrentMIDIOut > 0) and MIMidiThru.Checked then
// begin
for i := 0 to FMDataCnt - 1 do
MIDIOutput.PutShort (PVstMidiEvent (FMyEvents.events[i])^.midiData[0],
PVstMidiEvent (FMyEvents.events[i])^.midiData[1],
PVstMidiEvent (FMyEvents.events[i])^.midiData[2]);
// FMidiOutput.Send(//FCurrentMIDIOut - 1,
// PVstMidiEvent(FMyEvents.events[i])^.midiData[0],
// PVstMidiEvent(FMyEvents.events[i])^.midiData[1],
// PVstMidiEvent(FMyEvents.events[i])^.midiData[2]);
// end;
FMDataCnt := 0;
end;
end; // TimerTimer //
So I don't get the events in the plugin. Any idea what do I wrong?
You should really look at the minihost core example (Delphi ASIO project, v1.4).
There is a use of midi events. Basically
you have a TVstEvents variable ( let's say MyMidiEvents: TvstEvents).
for the whole runtime you allocate the memory for this variable ( in the app constructor for exmaple)
When you have an event in your MIDI callback, you copy it on the TVstEvents stack.
Before calling process in the TVstHost, you call MyVstHost.ProcessEvents( #MyMidiEvents ).
this is how it's done in the example (minihost core), for each previously steps:
1/ at line 215, declaration
FMyEvents: TVstEvents;
2/ at line 376, allocation:
for i := 0 to 2047 do
begin
GetMem(FMyEvents.Events[i], SizeOf(TVSTMidiEvent));
FillChar(FMyEvents.Events[i]^, SizeOf(TVSTMidiEvent), 0);
with PVstMidiEvent(FMyEvents.Events[i])^ do
begin
EventType := etMidi;
ByteSize := 24;
end;
end;
3/ at line 986 then at line 1782, the midi event is copied from the callback:
the callback
procedure TFmMiniHost.MidiData(const aDeviceIndex: Integer; const aStatus, aData1, aData2: Byte);
begin
if aStatus = $FE then exit; // ignore active sensing
if (not Player.CbOnlyChannel1.Checked) or ((aStatus and $0F) = 0) then
begin
if (aStatus and $F0) = $90
then NoteOn(aStatus, aData1, aData2) //ok
else
if (aStatus and $F0) = $80
then NoteOff(aStatus, aData1)
else AddMidiData(aStatus, aData1, aData2);
end;
end;
event copy
procedure TFmMiniHost.AddMIDIData(d1, d2, d3: byte; pos: Integer = 0);
begin
FDataSection.Acquire;
try
if FMDataCnt > 2046
then exit;
inc(FMDataCnt);
with PVstMidiEvent(FMyEvents.events[FMDataCnt - 1])^ do
begin
EventType := etMidi;
deltaFrames := pos;
midiData[0] := d1;
midiData[1] := d2;
midiData[2] := d3;
end;
finally
FDataSection.Release;
end;
end;
4/ at line 2322, in TAsioHost.Bufferswitch, the TVstHost.ProcessEvents is called
FDataSection.Acquire;
try
if FMDataCnt > 0 then
begin
FMyEvents.numEvents := FMDataCnt;
VSTHost[0].ProcessEvents(FMyEvents);
if (FCurrentMIDIOut > 0) and MIMidiThru.Checked then
begin
for i := 0 to FMDataCnt - 1 do
FMidiOutput.Send(FCurrentMIDIOut - 1,
PVstMidiEvent(FMyEvents.events[i])^.midiData[0],
PVstMidiEvent(FMyEvents.events[i])^.midiData[1],
PVstMidiEvent(FMyEvents.events[i])^.midiData[2]);
end;
FMDataCnt := 0;
end;
finally
FDataSection.Release;
end;
this should help you a lot if you were not able to analyse the method used.
If you are hosting VST 2.x plugins, you can send MIDI events to the plugin using AudioEffectX.ProcessEvents().
From the VST docs.
Events are always related to the current audio block.
For each process cycle, processEvents() is called once before a processReplacing() call (if new events are available).
I don't know of any code examples. There might be something in DelphiAsioVST.
If you're up for a change of programming language you could try VST.NET that allows you to write plugins and hosts in C# and VB.NET.
Hope it helps.

Resources