Multiple Interfaces

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.

Object Interfaces

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.

Abstract Interfaces

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.


Note
By convention, this type of interface is typically prefixed with a capital “I” to indicate that it is a formal interface.

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.

Defining the 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.

Using the Interface

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.

Implementing the Interface

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.


Note
You can also use the AssemblyLoad method, which scans the directory containing your application's .exe file (and the global assembly cache) for any EXE or DLL containing the Objects assembly. When it finds the assembly, it loads it into memory, making it available for your use.

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.

Reusing a Common Implementation

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.

Implementing IPrintable

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.

Figure 4.9 Print preview display from running sample project

4.9
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset