Article

This is in continuation to Reflection in .NET - Part 4

Late Binding

In our programs, we usually use early binding where the programmer declares the type of an object beforehand. In early binding, the type of the object used must be known at compile-time. Binding to a particular method is performed at compile-time before the program is run. When you build an assembly, you must give the references to external assemblies you use in your program. In case, if you don't have the references to external assemblies at compile time, the program will not compile. In other words, you will not be able to invoke methods of a type that the compiler is not able to resolve.

In late binding, the type of an object and the address of a method body is determined at runtime. The code to be executed is not determined until runtime. Late binding is also known as dynamic binding.

The methods declared as virtual or override in C# use late binding. However, it is a restricted kind of late binding. It only offers the flexibility of runtime binding to only those objects that belong to a particular inheritance chain i.e. the type of an object that you want to bind to must belong to a inheritance hierarchy.

Reflection offers complete late binding. At runtime, you can retrieve a list of types in a given assembly, then retrieve a list of methods of a particular type, create an instance of that type, and then invoke a particular method. In summary, you can make a call to a method belonging to an assembly that is not known at compile time.

It is important to note that late binding helps you gain flexibility at the cost of efficiency.

You can use the Activator.CreateInstance() method to dynamically create an instance whose type is not known until runtime. It invokes the constructor that matches the specified arguments. In case, you do not specify any arguments, then the default parameterless constructor is invoked.

Example

Consider that your app needs the ability to support multiple printers such as HP Plotter, Epson Laser. And you do not want the app to adhere to a particular printer vendor product at compile time. The app should be able to identify which printers are available at runtime and the app user should be able to select any of the available printers. The app design should be flexible enough so that you should be able to include the new printers and exclude the existing ones whenever required.

If you bind early to a particular printer, then this application requires rebinding if it needs to use a different printer at a later point of time. This may require rewriting the app and recompiling it. We will use late binding to solve this problem. We will create the DLLs such as PrintToEpson.dll, PrintToHp.dll that will encapsulate the printing functionality. We will put these DLLs in the app directory. At runtime, we will load these DLLs, obtain the list all supported printers and ask the user to select a particular printer.

Using Activator.CreateInstance(), we will dynamically create an instance of the chosen Printer and invoke its Print() method. Note that the compiler doesn't know anything about all this at compile time.

Listing 1: Printer.cs

Printer is an abstract class that has an abstract method named Print(). Any printer-specific class will be derived from this abstract Printer class. Each such class will provide the concrete implementation of the Print() method.

We will make use of a custom attribute named NameAttribute that will be used to store the name of the printer. We will place this attribute on a printer-specific class. When the compiler discovers this attribute, it will emit the corresponding metadata. The attribute information stored in the metadata can then be queried at runtime via reflection.

Compile this file with the following command line: csc /t:library Printer.cs

using System;
public abstract class Printer {
    //We will use this search string in our program to find the different DLLs
    //whose names begin with "PrintTo" and end with ".dll"
    public const string searchDLL = "PrintTo*.dll";
    public abstract void Print();
}

//This attribute should be placed only on a class and it should not be placed more than once.
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class NameAttribute : System.Attribute {
    private string text;
    public string Text { get { return text; } }
    public NameAttribute (string text) { this.text = text; }
}

Listing 2: PrintToEpson.cs

This file contains the classes that encapsulate the functionality of various Epson printers. For example, the PrintLaser class represents the Epson Action Laser 1000 printer. Since these classes will be derived from the Printer class, we need to refer to the Printer.dll during compilation using the /r option.

Compile this file with the following command line. csc /t:library PrintToEpson.cs /r:Printer.dll

using System;

[Name ("Epson Action Laser 1000")]
public class PrintLaser : Printer {
    public override void Print() {
        Console.WriteLine ("Printing using the Epson Action Laser 1000");
    }
}

[Name ("Epson AP-5000 Scalable Font")]
public class PrintScalable : Printer {
    public override void Print() {
        Console.WriteLine ("Printing using the Epson AP5000 Scalable Font");
    }
}

Listing 3: PrintToHp.cs

Similarly this file contains the classes that encapsulate the functionality of various HP printers. Compile this file with the following command line: csc /t:library PrintToHp.cs /r:Printer.dll

using System;

[Name ("HP-GL/2 Plotter")]
public class PrintPlotter : Printer {
    public override void Print() {
        Console.WriteLine (“Printing using the HP-GL/2 Plotter”);
    }
}
[Name ("HP Deskjet 320")]
public class PrintDeskJet: Printer {
    public override void Print() {
        Console.WriteLine ("Printing using the HP Deskjet 320");
    }
}

Listing 4: LateBindingDemo.cs

Compile this file with the following command line. csc LateBindingDemo.cs /r:Printer.dll

using System;
using System.Reflection;
using System.IO;
using System.Collections;

class LateBindingDemo {
    public static void Main() {

        //The table Hashtable stores a Type instance as a value and a unique positive 
        //value as its associated key. These Type instances represent the different 
        //Printer classes like PrintPlotter, PrintLaser, etc.
        Hashtable table = new Hashtable();
        int index = 0;

        //Get all the dll files from the current directory whose names begin with 
        //"PrintTo". Store the names in the 'files' array.
        string[] files = Directory.GetFiles (Environment.CurrentDirectory, 
                                        Printer.searchDLL);

        Console.WriteLine ("Available Printing Devices:\n");
        foreach (string file in files) {

            //Load an assembly (a given dll file) and obtain all its types
            Assembly asm = Assembly.LoadFrom(file);
            Type[] types = asm.GetTypes();

            foreach (Type type in types) {

                //Ensure that the obtained type is derived from Printer
                if (type.BaseType.ToString() == "Printer") {

                    //Store the Type object references in the Hashtable
                    table.Add (++index, type);

                    //Query for the NameAttribute information
                    Object[] attributes = type.GetCustomAttributes(false);
                    foreach (Object attrib in attributes) {
                        if (attrib.ToString() == "NameAttribute") {
                            NameAttribute na = (NameAttribute) attrib;

                            //Display the Printer Name to the user
                            Console.WriteLine("{0}  {1}\n", index, na.Text);
                        }
                    }
                }
            }
        }

        Console.Write ("To select a printer, enter the corresponding number:  ");
        string selectedPrinter = Console.ReadLine();

        //Based on the user input, retrieve the associated Type instance (value) 
        //from the Hashtable.
        int key = Convert.ToInt32(selectedPrinter);
        Type t = (Type) table[key];

        //Pass the retrieved Type object to Activator.CreateInstance(). It will create 
        //an instance of a class represented by the Type object 't'.
        object printer = Activator.CreateInstance(t);
        Console.Write("\n");

        //Obtain the MethodInfo object that represents the Print method
        MethodInfo method = t.GetMethod ("Print");

        //Use MethodInfo.Invoke() to invoke a method. Its first parameter  accepts an 
        //object on which the method is to be invoked. Its second parameter accepts an 
        //array of objects that you have to populate with the parameters of the method. 
        //Use null if there are no parameters.
        method.Invoke (printer, null);
    }
}

Output

Available Printing Devices:
1  Epson Action Laser 1000
2  Epson AP-5000 Scalable Font
3  HP-GL/2 Plotter
4  HP Deskjet 320

To select a printer, enter the corresponding number:  2

Printing using the Epson AP-5000 Scalable Font