In Visual Basic, objects can have one or more interfaces. All objects have a primary, or native, interface, which is composed of any methods, properties, events, or member variables declared using the Public keyword. You can also have objects implement secondary interfaces in addition to their native interface by using the Implements keyword.
The native interface on any class is composed of all the methods, properties, events, and even variables that are declared as anything other than Private. Though this is nothing new, do a quick review of what is included in the native interface to set the stage for discussing secondary interfaces. To include a method as part of your interface, you can simply declare a Public routine:
Public Sub AMethod() End Sub
Notice that there is no code in this routine. Any code would be implementation and is not part of the interface. Only the declaration of the method is important when discussing interfaces. This can seem confusing at first, but it is an important distinction, as the separation of the interface from its implementation is at the very core of object-oriented programming and design.
Because this method is declared as Public, it is available to any code outside the class, including other applications that may make use of the assembly. If the method has a property, then you can declare it as part of the interface by using the Property keyword:
Public Property AProperty() As String End Property
You can also declare events as part of the interface by using the Event keyword:
Public Event AnEvent()
Finally, you can include actual variables, or attributes, as part of the interface:
Public AnInteger As Integer
This is strongly discouraged, because it directly exposes the internal variables for use by code outside the class. Because the variable is directly accessible from other code, you give up any and all control over changing the implementation.
Rather than make any variable Public, you should always make use of a Property method to expose the value. That way, you can implement code to ensure that your internal variable is set only to valid values and that only the appropriate code has access to the value based on your application's logic.
Ultimately, the native (or primary) interface for any class is defined by looking at all the methods, properties, events, and variables that are declared as anything other than Private in scope. This includes any methods, properties, events, or variables that are inherited from a base class.
You are used to interacting with the default interface on most objects, so this should seem pretty straightforward. Consider this simple class:
Public Class TheClass Public Sub DoSomething() End Sub Public Sub DoSomethingElse() End Sub End Class
This defines a class and, by extension, defines the native interface that is exposed by any objects you instantiate based on this class. The native interface defines two methods: DoSomething and DoSomethingElse. To make use of these methods, you simply call them:
Dim myObject As New TheClass() myObject.DoSomething() myObject.DoSomethingElse()
This is the same thing you did in Chapter 3 and so far in this chapter. However, you will now take a look at creating and using secondary interfaces, because they are a bit different.
Sometimes it's helpful for an object to have more than one interface, thereby enabling you to interact with the object in different ways. You may have a group of objects that are not the same thing, but you want to be able to treat them as though they were the same. You want all these objects to act as the same thing, even though they are all different. Inheritance enables you to create subclasses that are specialized cases of the base class. For example, your Employee is a Person.
Next you may have a series of different objects in an application: product, customer, invoice, and so forth. Each of these would be a different class—so there's no natural inheritance relationship implied between these classes. At the same time, you may need to be able to generate a printed document for each type of object, so you would like to make them all act as a printable object.
To accomplish this, you can define an abstract interface that enables generating such a printed document. You can call it IPrintableObject.
Each of your application objects can choose to implement the IPrintableObject interface. Every object that implements this interface must include code to provide actual implementation of the interface, which is unlike inheritance, whereby the code from a base class is automatically reused.
By implementing this common interface, you can write a routine that accepts any object that implements the IPrintableObject interface and then print it—while remaining totally oblivious to the “real” data type of the object or methods its native interface might expose. Before you learn how to use an interface in this manner, you should walk through the process of actually defining an interface.
You define a formal interface using the Interface keyword. This can be done in any code module in your project, but a good place to put this type of definition is in a standard module. An interface defines a set of methods (Sub, Function, or Property) and events that must be exposed by any class that chooses to implement the interface.
Add a module to the project using Project ⇒ Add ⇒ New Item… Within the Add New Item dialog select an Interface and name it IprintableObject.vb. Then, add the following code to the module, outside the Module code block itself:
Public Interface IPrintableObject End Interface
Interfaces must be declared using either Public or Friend scope. Declaring a Private or Protected interface results in a syntax error. Within the Interface block of code, you can define the methods, properties, and events that make up your particular interface. Because the scope of the interface is defined by the Interface declaration itself, you can't specify scopes for individual methods and events; they are all scoped like the interface itself.
For instance, update IPrintableObject to look similar to the following. This won't be your final version of this interface, but this version will allow you to demonstrate another implementation feature of interfaces.
Public Interface IPrintableObject Function Label(ByVal index As Integer) As String Function Value(ByVal index As Integer) As String ReadOnly Property Count() As Integer End Interface
This defines a new data type, somewhat like creating a class or structure, which you can use when declaring variables. For instance, you can now declare a variable of type IPrintableObject:
Private printable As IPrintableObject
You can also have your classes implement this interface, which requires each class to provide implementation code for each of the three methods defined on the interface.
Before you implement the interface in a class, it's a good idea to see how you can use the interface to write a generic routine that can print any object that implements IPrintableObject.
Interfaces define the methods and events (including parameters and data types) that an object is required to implement if you choose to support the interface. This means that, given just the interface definition, you can easily write code that can interact with any object that implements the interface, even though you do not know what the native data types of those objects will be.
To see how you can write such code, you can create a simple routine in your form that can display data to the output window in the IDE from any object that implements IPrintableObject. Bring up the code window for your form and add the following (code file: Form1.vb):
Public Sub PrintObject(obj As IPrintableObject) Dim index As Integer For index = 0 To obj.Count Debug.Write(obj.Label(index) & ": ") Debug.WriteLine(obj.Value(index)) Next End Sub
Notice that you are accepting a parameter of type IPrintableObject. This is how secondary interfaces are used, by treating an object of one type as though it were actually of the interface type. As long as the object passed to this method implements the IPrintableObject interface, your code will work fine.
Within the PrintObject routine, you are assuming that the object will implement three elements—Count, Label, and Value—as part of the IPrintableObject interface. Secondary interfaces can include methods, properties, and events, much like a default interface, but the interface itself is defined and implemented using some special syntax.
Now that you have a generic printing routine, you need a way to call it. Bring up the designer for Form1, add a button, and name it ButtonPrint. Double-click the button and put the following code within it (code file: Form1.vb):
Private Sub ButtonPrint_Click(sender As Object, e As EventArgs) Handles ButtonPrint.Click Dim obj As New Employee() obj.EmployeeNumber = 123 obj.BirthDate = #1/1/1980# obj.HireDate = #1/1/1996# PrintObject(obj) End Sub
This code simply initializes an Employee object and calls the PrintObject routine. Of course, this code produces runtime exceptions, because PrintObject is expecting a parameter that implements IPrintableObject, and Employee implements no such interface. Now you will move on and implement that interface in Employee so that you can see how it works.
Any class (other than an abstract base class) can implement an interface by using the Implements keyword. For instance, you can implement the IPrintableObject interface in Employee by adding the the following line:
Implements IPrintableObject
This causes the interface to be exposed by any object created as an instance of Employee. Adding this line of code and pressing Enter triggers the IDE to add skeleton methods for the interface to your class. All you need to do is provide implementations (write code) for the methods.
Before actually implementing the interface, however, you can create an array to contain the labels for the data fields in order to return them via the IPrintableObject interface. Add the following code to the Employee class:
Private mLabels() As String = {"ID", "Age", "HireDate"}
To implement the interface, you need to create methods and properties with the same parameter and return data types as those defined in the interface. The actual name of each method or property does not matter because you are using the Implements keyword to link your internal method names to the external method names defined by the interface. As long as the method signatures match, you are all set.
This applies to scope as well. Although the interface and its methods and properties are publicly available, you do not have to declare your actual methods and properties as Public. In many cases, you can implement them as Private, so they do not become part of the native interface and are only exposed via the secondary interface.
However, if you do have a Public method with a method signature, you can use it to implement a method from the interface. This has the interesting side effect that this method provides implementation for both a method on the object's native interface and one on the secondary interface.
In this case, you will use a Private method, so it is only providing implementation for the IPrintableObject interface. Implement the Label method by adding the following code to Employee:
Private Function Label(ByVal index As Integer) As String _ Implements IPrintableObject.Label Return mLabels(index) End Function
This is just a regular Private method that returns a String value from the pre-initialized array. The interesting part is the Implements clause on the method declaration:
Implements IPrintableObject.Label
By using the Implements keyword in this fashion, you are indicating that this particular method is the implementation for the Label method on the IPrintableObject interface. The actual name of the private method could be anything. It is the use of the Implements clause that makes this work. The only requirement is that the parameter data types and the return value data type must match those defined by the IPrintableObject interface method.
This is very similar to using the Handles clause to indicate which method should handle an event. In fact, like the Handles clause, the Implements clause allows you to have a comma-separated list of interface methods implemented by this one function.
You can then finish implementing the IPrintableObject interface by adding the following code to Employee:
Private Function Value(ByVal index As Integer) As String _ Implements IPrintableObject.Value Select Case index Case 0 Return CStr(EmployeeNumber) Case 1 Return CStr(Age) Case Else Return Format(HireDate, "Short date") End Select End Function Private ReadOnly Property Count() As Integer _ Implements IPrintableObject.Count Get Return UBound(mLabels) End Get End Property
You can now run this application and click the button. The output window in the IDE will display your results, showing the ID, age, and hire-date values as appropriate.
Any object could create a similar implementation behind the IPrintableObject interface, and the PrintObject routine in your form would continue to work, regardless of the native data type of the object itself.
Secondary interfaces provide a guarantee that all objects implementing a given interface have exactly the same methods and events, including the same parameters.
The Implements clause links your actual implementation to a specific method on an interface. Sometimes, your method might be able to serve as the implementation for more than one method, either on the same interface or on different interfaces.
Add the following interface definition to your project (code file: IValues.vb):
Public Interface IValues Function GetValue(ByVal index As Integer) As String End Interface
This interface defines just one method, GetValue. Notice that it defines a single Integer parameter and a return type of String, the same as the Value method from IPrintableObject. Even though the method name and parameter variable name do not match, what counts here is that the parameter and return value data types do match.
Now bring up the code window for Employee. You will have to implement this new interface in addition to the IPrintableObject interface as follows:
Implements IValues
You already have a method that returns values. Rather than reimplement that method, it would be nice to just link this new GetValues method to your existing method. You can easily do this because the Implements clause allows you to provide a comma-separated list of method names:
Private Function Value(ByVal index As Integer) As String _ Implements IPrintableObject.Value, IValues.GetValue
This is very similar to the use of the Handles keyword, covered in Chapter 3. A single method within the class, regardless of scope or name, can be used to implement any number of methods as defined by other interfaces, as long as the data types of the parameters and return values all match.
You can combine implementation of abstract interfaces with inheritance. When you inherit from a class that implements an interface, your new subclass automatically gains the interface and implementation from the base class. If you specify that your base-class methods are overridable, then the subclass can override those methods. This not only overrides the base-class implementation for your native interface, but also overrides the implementation for the interface.
Combining the implementation of an interface in a base class with overridable methods can provide a very flexible object design.
Now that you've looked academically at using an interface to provide a common way to define a printing interface, let's apply that to some actual printing logic. To do this will provide a somewhat reusable interface. First create another interface called IPrintable. Now add the following code to your new source file (code file: IPrintable.vb):
Public Interface IPrintable Sub Print() Sub PrintPreview() Sub RenderPage(ByVal sender As Object, ByVal ev As System.Drawing.Printing.PrintPageEventArgs) End Interface
This interface ensures that any object implementing IPrintable will have Print and PrintPreview methods so you can invoke the appropriate type of printing. It also ensures that the object has a RenderPage method, which can be implemented by that object to render the object's data on the printed page.
At this point, you could simply implement all the code needed to handle printing directly within the Employee object. This isn't ideal, however, as some of the code will be common across any objects that want to implement IPrintable, and it would be nice to find a way to share that code.
To do this, you can create a new class, ObjectPrinter. This is a framework-style class in that it has nothing to do with any particular application, but can be used across any application in which IPrintable will be used.
Add a new class named ObjectPrinter to project. This class will contain all the code common to printing any object. It makes use of the built-in printing support provided by the .NET Framework class library.
Within the class you'll need two private fields. The first to define is a PrintDocument variable, which will hold the reference to your printer output. You will also declare a variable to hold a reference to the actual object you will be printing. Notice that the following code shows you are using the IPrintable interface data type for the ObjectToPrint variable (code file: ObjectPrinter.vb):
Public Class ObjectPrinter Private WithEvents document As System.Drawing.Printing.PrintDocument Private objectToPrint As IPrintable End Class
Now you can create a routine to kick off the printing process for any object implementing IPrintable. The following code is totally generic; you will write it in the ObjectPrinter class so it can be reused across other classes.
Public Sub Print(ByVal obj As IPrintable) objectToPrint = obj document = New System.Drawing.Printing.PrintDocument() document.Print() End Sub
Likewise, the following snippet shows how you can implement a method to show a print preview of your object. This code is also totally generic, so add it to the ObjectPrinter class for reuse. Note printing is a privileged operation, so if you see an issue when you run this code you may need to look at the permissions you are running under.
Public Sub PrintPreview(ByVal obj As IPrintable) Dim PPdlg As PrintPreviewDialog = New PrintPreviewDialog() objectToPrint = obj document = New PrintDocument() PPdlg.Document = document PPdlg.ShowDialog() End Sub
Finally, you need to catch the PrintPage event that is automatically raised by the .NET printing mechanism. This event is raised by the PrintDocument object whenever the document determines that it needs data rendered onto a page. Typically, it is in this routine that you would put the code to draw text or graphics onto the page surface. However, because this is a generic framework class, you won't do that here; instead, delegate the call back into the actual application object that you want to print (code file: ObjectPrinter.vb):
Private Sub PrintPage(ByVal sender As Object, ByVal ev As System.Drawing.Printing.PrintPageEventArgs) _ Handles document.PrintPage objectToPrint.RenderPage(sender, ev) End Sub
This enables the application object itself to determine how its data should be rendered onto the output page. You do that by implementing the IPrintable interface on the Employee class.
By adding this interface, you require that your Employee class implement the Print, PrintPreview, and RenderPage methods. To avoid wasting paper as you test the code, make both the Print and PrintPreview methods the same. Both methods implement the print preview display, but that is sufficient for testing. Add the following code to the Employee class (code file: ObjectPrinter.vb):
Public Sub Print() Implements IPrintable.Print Dim printer As New ObjectPrinter() printer.PrintPreview(Me) End Sub Public Sub PrintPreview() Implements IPrintable.PrintPreview Dim printer As New ObjectPrinter() printer.PrintPreview(Me) End Sub
Notice that you are using an ObjectPrinter object to handle the common details of doing a print preview. In fact, any class you ever create that implements IPrintable can have this exact same code to implement a print-preview function, relying on your common ObjectPrinter to take care of the details.
You also need to implement the RenderPage method, which is where you actually put your object's data onto the printed page (code file: Employee.vb):
Private Sub RenderPage(sender As Object, ev As Printing.PrintPageEventArgs) _ Implements IPrintable.RenderPage Dim printFont As New Font("Arial", 10) Dim lineHeight As Single = printFont.GetHeight(ev.Graphics) Dim leftMargin As Single = ev.MarginBounds.Left Dim yPos As Single = ev.MarginBounds.Top ev.Graphics.DrawString("ID: " & EmployeeNumber.ToString, printFont, Brushes.Black, leftMargin, yPos, New StringFormat()) yPos += lineHeight ev.Graphics.DrawString("Name: " & Name, printFont, Brushes.Black, leftMargin, yPos, New StringFormat()) ev.HasMorePages = False End Sub
All of this code is unique to your object, which makes sense because you are rendering your specific data to be printed. However, you don't need to worry about the details of whether you are printing to paper or print preview; that is handled by your ObjectPrinter class, which in turn uses the .NET Framework. This enables you to focus on generating the output to the page within your application class.
By generalizing the printing code in ObjectPrinter, you have achieved a level of reuse that you can tap into via the IPrintable interface. Anytime you want to print a Customer object's data, you can have it act as an IPrintableObject and call its Print or PrintPreview method. To see this work, adjust the Print button handler for Form1 with the following code:
Private Sub ButtonPrint_Click(sender As Object, e As EventArgs) _ Handles ButtonPrint.Click Dim obj As New Employee() obj.EmployeeNumber = 123 obj.BirthDate = #1/1/1980# obj.HireDate = #1/1/1996# 'PrintObject(obj) CType(obj, IPrintable).PrintPreview() End Sub
This code creates a new Employee object and sets its Name property. You then use the CType method to access the object via its IPrintableObject interface to invoke the PrintPreview method.
When you run the application and click the button, you will get a print preview display showing the object's data as shown in Figure 4.9.