Category: Visual Basic 8

.NET controls in Microsoft Access

Creating ActiveX objects in Visual Basic .NET is easy from Visual Studio 2008. There are a few checkboxes to tick, and a class attribute to add. But if you are doing an ActiveX control for use in let’s say an Microsoft Access application, there are some requirements that must be met, otherwise Access will not list your control. Of course, Microsoft gives you a template for this, and then you´re off. Thanks to Håkan Ståhl for pointing out this template to me.

Create a VB6 Interop UserControl project. The project that is shown on the screenshot below uses the standard name InteropUserControlLibrary1. I already hold a control. To rename the control, rename all three files (the vb file, the manifest and the bitmap). When the vb file is renamed, you will be asked if you want to rename the references too. Do that. Finally, fix the broken references in the manifest file (enter the new name in the progid attribute and the name attribute). Now you´re on your way. You can add a test project to the solution and start coding. The control is registered when you compile the project.

How to upload a file

First I must mention that this is my first ever blog post using Windows Live Writer. The topic was inspired by a question that was raised on the MSDN forums. How do I upload a file, and how can I control the remote filename? This is how it could be done:

'The full path to the source file.
Dim Source As String = "C:\MyFiles\SourceFile.txt"

'The full destination path (will be created).
Dim Destination As String = "ftp://www.myserver.com/myfolder/destination.txt"

'Use the static (shared) method Create to create a web request.
'Pass the destination as an argument, and cast it to a FtpWebRequest.
Dim R As System.Net.FtpWebRequest = CType(System.Net.WebRequest.Create(Destination), _
System.Net.FtpWebRequest)

'Tell the request how it will login (using a NetworkCredential object)
R.Credentials = New System.Net.NetworkCredential("myUsername", "P@ssw0rd")

'...and what kind of method it will represent. A file upload.
R.Method = System.Net.WebRequestMethods.Ftp.UploadFile

'Here I use the simplest method I can imagine to get the
'bytes from the file to an byte array.
Dim FileContens() As Byte = System.IO.File.ReadAllBytes(Source)

'Finaly, I put the bytes on the request stream.
Using S As System.IO.Stream = R.GetRequestStream()
   S.Write(FileContens, 0, FileContens.Length)
   S.Close()
End Using

You could increase the level of control by replacing the ReadAllBytes call with some own code to read the bytes. This might be interesting if you’re for example are handling larger files, and want to show progress. To give away all control, you can use the already build function My.Computer.Network.UploadFile.

Get the bytes from a file

The bytes of a file are the file’s data at the lowest level available, and in .NET bytes are usually stored in byte arrays. If you’re not trying to cheat in a role playing game by hacking some save file, why would you want to load the bytes of the file? You might want to pass the file something. Add it as an attachment in a mail or add it as a value in a database. In these cases, you are not bothered by the fact that the file is quite useless when stored in a byte array, in fact, you want the file as it is and you are not concerned by the issue that you can’t use the file’s format to access the actual data of the file.

Once the file is in a byte array, you can pass it to a SqlCommand object, convert it to a string, pass it to a web service, or indeed write it to disk.

To do this, create a FileInfo object to represent the file, and call the OpenRead method of the FileInfo object. OpenRead returns a FileStream object.

Dim Fi As New System.IO.FileInfo(“C:\Windows\notepad.exe”)
Dim S As System.IO.FileStream = Fi.OpenRead()

You can read the length of the file (in bytes) from the FileInfo object or the FileStream object. Either way, it is returned as a Long (64 bit integer), so it has to be converted to an Integer (32 bit). The largest amount of memory you can allocate is two gigabytes (Integer.MaxValue).  Also, in Visual Basic (unlike C#) you need to know the last index, not the size of, when you declare an array. Therefore, if you want an array with 3 elements, use Dim MyArray(2) As MyType.

Dim X(CType(S.Length – 1, Integer)) As Byte

Then, call the Read method of the FileStream. Pass in the buffer (in this case X), the offset and the number of bytes you want to read.

S.Read(X, 0, CType(S.Length, Integer))

Close and dispose the stream, and you are done!

S.Close()
S.Dispose()

Now, do what you want with the bytes of the file. You might want to remember the file ending of the original file. That can be good to know if you want to write the file back to disk.

Optional arguments in Visual Basic

Some things that can be accomplished using optional arguments can also be accomplished using overloading. I usually write the function with the most arguments first, and then I do overloaded versions of the function that calls the first version and provide the arguments like this:

Public Sub DoSome(ByVal X As Integer)
	Console.WriteLine(X)
End Sub

Public Sub DoSome()
	DoSome(4)
End Sub

This can also be accomplished using optional parameters. They are declared using the keyword optional. You also need to provide a default value to the optional argument like this:

Public Sub DoSome(Optional ByVal X As Integer = 4)
	Console.WriteLine(X)
End Sub

This last example will effectively be the same for the one that calls the function, who still can call DoSome with one or no parameters. One syntax limitation is that all parameters declared after an optional parameter must also be optional.

As long as the procedure doesn’t contain any parameter arrays, it can be called using named arguments. In this example, the procedure HitMe can be called without named arguments like this: HitMe(1, 2) or using named arguments like this: HitMe(X:=1, Y:=2).

Public Sub HitMe(ByVal X As Integer, ByVal Y As Integer)

The code for calling a function that takes lots of optional arguments without using named arguments is really ugly. It might look something like this:

MyFunction(, , , , , , , , , , , , , , , , , , , , , , 6)

It is not very readable. By specifying the name of the argument you want to pass, the code is much more readable.

MyFunction(OneParameter:=6)

Microsoft Reporting and parameters

If you use Microsoft Reporting, anyone that is used to work with ASP.NET 2.0 or later will be very familiar. In this example, I use the Northwind database (Microsoft Access - not SQL Server). Add the report file, create and configure the data source using the built in wizard, drag fields from the Data Sources window onto the report, and finally, if required, modify the typed dataset that the wizard supplied to you. To view a Microsoft Reporting report, use the Report Viewer control in the Data section of the toolbox.

In detail, open the typed dataset. Right click on the data table and click Configure. You should see a query looking something like this:

SELECT OrderID, CustomerID, EmployeeID, OrderDate, RequiredDate, ShippedDate, ShipVia, Freight, ShipName, ShipAddress, ShipCity, ShipRegion, ShipPostalCode, ShipCountry FROM Orders

To add a parameter, append the following code to the query:

WHERE EmployeeID=?

Now, when you have stepped through the wizard, the Fill method of the corresponding data adapter has a new parameter: An employee ID is required! The parameter you send to the Fill method, will be used in the query. Imagine if everything was this easy!

Crystal Reports and dynamic data sources

For this example, I have downloaded the Access 2000 Northwind database from http://www.microsoft.com/. If you google on the filename (nwind.mdb), make sure that you download it from Microsoft’s web site.

The problem I have with Crystal Reports is that when I distribute my application, the data source in my report gets invalid, since the connection data refers to a file on my computer, the filename gets invalid when the application is installed on the end users machine.

To solve this problem, the connection data must be dynamically assigned when an end user opens a report.

An application with a Crystal Report usually has a report (rpt-file) added to the project, and a Crystal Report Viewer (a Windows Form with a CrystalReportViewer control). The report file contains the connection string that points to the mdb-file, and the Crystal Report Viewer has a reference to the rpt-file.

All I have to do to simulate the problem, is to rename the mdb-file from nwind.mdb to nwind1.mdb (I close Visual Studio before I do this, to release any locks). Now, Crystal Reports will prompt me for a login ID and a password, since he can’t connect to the file. There is no reason to give a login ID, since the problem is that the file isn’t found (stupid reaction to that error). No matter what I do, the report will not load.

I couldn’t find any information on this, so it took me a while to track how the rpt-file works. This is the solution:

In the form that contains the report viewer, type some code in the control’s Load event, here:

Private Sub CrystalReportViewer1_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles CrystalReportViewer1.Load

Declare a variable that can point at the report document:

Dim Doc As CrystalDecisions.CrystalReports.Engine.ReportDocument

Assign the current document to the variable:

Doc = CType(Me.CrystalReportViewer1.ReportSource, _
CrystalDecisions.CrystalReports.Engine.ReportDocument)

Finally, call the SetConnection in the connection object you want to modify. Use the filename as the server and database argument.

Doc.DataSourceConnections(0).SetConnection( _
“C:\Temp\Nwind1.mdb”, “C:\Temp\Nwind1.mdb”, False)

Now, you have a code that allows you to dynamically modify the location of the underlying database of crystal report!

Loading and saving bitmap images

Instances of the Bitmap class (System.Drawing.Bitmap) represent a bitmapped image. To load an image, just pass the filename to the Bitmap constructor. This example loads a jpeg image from the “my pictures” folder in the current user’s local storage.

Dim MyPics As String = Environment.GetFolderPath(Environment.SpecialFolder.MyPictures)
Dim B As New System.Drawing.Bitmap(MyPics & “\duck.jpg”)

The supported formats are BMP, GIF, JPEG, PNG and TIFF.

To save the image, just call the function Bitmap.Save of the Bitmap instance, and specify the desired format by passing one of the read only properties in the ImageFormat class as the second argument. This code is saves the loaded image in the Bitmap instance in five different formats.

B.Save(MyPics & “\testsave.jpg”, Imaging.ImageFormat.Jpeg)
B.Save(MyPics & “\testsave.bmp”, Imaging.ImageFormat.Bmp)
B.Save(MyPics & “\testsave.gif”, Imaging.ImageFormat.Gif)
B.Save(MyPics & “\testsave.tif”, Imaging.ImageFormat.Tiff)
B.Save(MyPics & “\testsave.png”, Imaging.ImageFormat.Png)

In some cases, you want to pass arguments to control compression and quality, for example when you save a JPEG image. The Save function has an overloaded version that can take codec parameters as an argument. To use that overload, I need a JPEG codec that can be found in the vector that is returned from the ImageCodecInfo.GetImageEncoders function. This is one piece of code that really would look better if Linq is used, but I do this in Visual Basic 8, so Linq is not available to me.

Dim Codecs() As Imaging.ImageCodecInfo = Imaging.ImageCodecInfo.GetImageEncoders()
For Each Codec As Imaging.ImageCodecInfo In Codecs
If Codec.MimeType = “image/jpeg” Then
‘More code here
Exit For
End If
Next

At the remark “More code here”, I will now add code to actually save the image. This is the complete code for this example:

Dim Codecs() As Imaging.ImageCodecInfo = Imaging.ImageCodecInfo.GetImageEncoders()
For Each Codec As Imaging.ImageCodecInfo In Codecs
If Codec.MimeType = “image/jpeg” Then
‘Create the desiered parameters in a list
Dim HighCompression As New Imaging.EncoderParameter(Imaging.Encoder.Quality, 0)
Dim AllMyParameters As New Imaging.EncoderParameters(1)
AllMyParameters.Param(0) = HighCompression
‘Save the image using the codec and the parameter(s)
B.Save(MyPics & “\high_compression.jpg”, Codec, AllMyParameters)
Exit For
End If
Next

If you run this, you should end up with a picture of very poor quality. The compression (0) is the highest possible, and the file is as small as it can be.

The quality parameter can be a value from 0 to 100, so to get the best possible quality (lowest compression), change the quality parameter to 100, like this:

Dim HighCompression As New Imaging.EncoderParameter(Imaging.Encoder.Quality, 100)

This is the basics in loading and saving bitmap images in .NET 2.0.

Multithreaded search and the BackgroundWorker

The background worker component in .NET Framework is designed to make it easy to execute code in a separate thread. One purpose of writing multithreaded applications might be to allow an application to do heavy work without deteriorating the user experience. An example of this might be searching.

Imagine a form with a single threaded search routine. It might have a textbox for entering a search expression, a search button and a list view that displays the search result. The user is expected type the expression in, click the button and wait for the result.

A multithreaded solution might not require a search button, just a text box and a list view. It will also give the impression of being much faster than the single threaded version. This is how you do it, instead of just doing the search in the click event of the search button:

 

In the form, declare a Boolean variable that will keep track of the need for doing a search. You will also need a BackgroundWorker control.

In the TextChanged event of the text box, set the Boolean to true. Check if the background worker is already doing something (read the IsBusy property), and if it isn’t, restore the Boolean to false and start the worker. To start the worker call theRunWorkerAsync method of the background worker, and pass the content of the text box as an argument.

In the DoWork event of the background worker, do the search and present the result in the list view. There are a couple of things to keep in mind here.

Finally, in the RunWorkerCompleted event of the background worker, just restart the worker in the same way as you do on the TextChanged event of the text box, if required. (Check the flag, and restore it if you actually start the background worker again.)

This is what you must keep in mind when you present the result:

Have the search result stored in a member variable, so that it can be reached in both the procedure that populates it and the procedure that present it. Remember to call theBeginUpdate and EndUpdate methods of the list view control, to make the update fast. Also, remember that the actual presenting of the result must be done in the GUI thread (that is the main thread), not the thread started by the background worker. So write the code that uses the list view in a separate procedure, and call it using the Invoke method of the form. Pass in a delegate that point to the procedure. The code might look like this:

In the form, add the delegate:

Private Delegate Sub MyThreadSwitcher()

In the code that does the search, call the method that displays the result (in this example it is called RefreshResult) like this:

Dim X As New MyThreadSwitcher(AddressOf Me.RefreshResult)
Me.Invoke(X)