Section 8.1. Atlas OOP Features for JavaScript


8.1. Atlas OOP Features for JavaScript

As you have seen in Chapter 2, JavaScript does have some OOP capabilities, but they are no match for those in programming languages like Visual Basic or C#. However, it's relatively easy to add new features to JavaScript using JavaScript itself, something that the Atlas team has exploited.

To facilitate OOP development, Atlas adds to JavaScript some OOP-type features, which are covered in this chapter. These include namespaces, abstract classes, and interfaces. The additional features are designed to help you architect and write more structured client-side code. They apply not only to Ajax applications, but can also be used with any JavaScript code that you write.

The Atlas runtime script library is sufficient for using the JavaScript OOP features. To use this runtime library, include the Atlas ScriptManager element in the page. It will look like this:

 <atlas:ScriptManager  runat="server"   EnableScriptComponents="false" /> 

If the EnableScriptComponents attribute is set to false, you do not have access to Atlas controls, but still can use the client-site JavaScript enhancements implemented in Atlas. For this, a stripped-down version of Atlas will be loaded and run in the browser. Instead of the file Atlas.js the file AtlasRuntime.js will be included, the latter having only about a third of the 143ize of the former file. (The filenames are subject to change, of course.) However with the runtime library, the pageLoad() function is not available, so you have to set window.onload to execute code once the page has been loaded.

8.1.1. Namespaces

A key Atlas JavaScript OOP extension is the addition of namespace functionality. Namespaces enable you to encapsulate functionality into logical groups under a single name. A namespace helps you avoid name collisions with functions that have the same name but fulfill different purposes. The JavaScript language specification does not specify namespaces, so the language itself cannot offer this functionality. However, Atlas uses a simple technique to emulate namespaces. You can create a new class (which serves as the "namespace"); you can then make another (new) class accessible as a property of the namespace class. This allows you to access your class using NamespaceClassName.YourClassName.

One of the base classes in Atlas runtime is the Type class. Two methods of this class come in handy when creating the Atlas namespaces:


Type.registerNamespace( name)

Registers a namespace


Class.registerClass (name, base type, interface type)

Registers a class as a member of the namespace

To demonstrate this technique, let's create an OReilly namespace for a group of classes used in this book. Suppose that one of them is named Software with two properties: name and vendor. First, you must register the OReilly namespace:

 Type.registerNamespace("OReilly"); 

Next you create the Software class as a member of OReilly, as shown in the following code snippet:

 OReilly.Software = function(name, vendor) {   var _name = (name != null) ? name : "unknown";   var _vendor = (vendor != null) ? vendor : "unknown";   this.getName = function() {     return _name;   }   this.setName = function(name) {     _name = name;   }   this.getVendor = function() {     return _vendor;   }   this.setVendor = function(vendor) {     _vendor = vendor;   } } 

The class constructor expects values for the two properties. To perform data hiding, the class member values are saved as separate variables, and the class implements setter and getter methods for the properties. Note that JavaScript does not support private or protected properties. Therefore, all class members are public. The data hiding implemented here does not provide protection from unauthorized access; it is just a helper tool to structure code and make the data access coherent. Of course most technologies that do support private or protected still allow access to those properties using reflection.


Finally, OReilly.Software must be registered as a class so that you can use it in your applications. You do this with the registerClass() method, which can take up to three parameters:


name

The name of the class


base type

The base type of the class, if any, as a reference to the type


interface type

The interface type of the class, if any, as a reference to the type

The OReilly.Software class does not have a base type and does not implement an interface type. The following call to registerClass() registers the class, omitting the second and third parameters:

 Type.registerClass("OReilly.Software"); 

Atlas implements several types, but the one you will use most often is Sys.IDisposable (because you can write a dispose() method that is called automatically when the script ends), although JavaScript has only a simple garbage collector. However, you do not necessarily need to implement an interface. If you do not use an interface (as we do in this example), the call to Type.registerClass() is not necessary either. However for more advanced features (see the next sections), this method call is mandatory.


Now, you can instantiate the Software class using the new keyword and get and set its properties. Example 8-1 does exactly that, creating two instances, one for Microsoft Internet Explorer and one for Mozilla Foundation Firefox.

Example 8-1. Using Atlas namespaces

 ClientNamespaces.aspx <%@ Page Language="C#" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head  runat="server">   <title>Atlas</title>   <script language="Javascript" type="text/javascript">   window.onload = function() {     var s = "";     Type.registerNamespace("OReilly");     OReilly.Software = function(name, vendor) {       var _name = (name != null) ? name : "unknown";       var _vendor = (vendor != null) ? vendor : "unknown";       this.getName = function() {         return _name;       }       this.setName = function(name) {         _name = name;       }       this.getVendor = function() {         return _vendor;       }       this.setVendor = function(vendor) {         _vendor = vendor;       }     }     Type.registerClass("OReilly.Software");     var ie = new OReilly.Software("Internet Explorer", "Microsoft");     s = ie.getName() + " from " + ie.getVendor() + "<br />";     var ff = new OReilly.Software();     ff.setName("Firefox");     ff.setVendor("Mozilla Foundation");     s += ff.getName() + " from " + ff.getVendor();     document.getElementById("output").innerHTML = s;   }   </script> </head> <body>   <form  runat="server">     <atlas:ScriptManager  runat="server" EnableScriptComponents="false">     </atlas:ScriptManager>     <div >     </div>   </form> </body> </html> 

Figure 8-1 shows the result displayed when the page is loaded.

Figure 8-1. Instantiating two objects within the same namespace


Although Atlas namespace classes are not "real" namespaces, they can make it easier for you to structure complex JavaScript code, with very little code overhead.

8.1.2. Class Inheritance

There is limited support for class inheritance in JavaScript, in the form of the prototype property, as detailed in Chapter 2. Atlas provides even more abstraction. The prototype mechanism is supported for namespace classes that were registered using Class name.registerClass(). As a second parameter for registerClass(), you can specify a base class. Here is where you say from which class the current class derives.

8.1.2.1. Derived classes

Let's create a class that inherits from Software. One very specific type of software is a web browser, so let's create a Browser class. In addition to the features of the generic Software class, a browser would benefit from some 147xtra properties. An isJavaScriptSupported property can usefully provide information about whether a particular browser is capable of running JavaScript:

 OReilly.Browser = function(name, vendor, isJavaScriptSupported) {   //... } 

Here's how to register the class. Note how the new class (the string parameter) derives from the old OReilly.Software class (no string!):

 OReilly.Browser.registerClass('OReilly.Browser', OReilly.Software); 

Of course it would be possible to create getter and setter methods for name and vendor once again, and to write the constructor code as well. However, one of the benefits (actually the major benefit) of class inheritance is that you can reuse functionality. So since OReilly.Browser inherits from OReilly.Software, you can use the getter and setter methods that are already there, as well as the _name and _vendor "private" members. You do, however, need to add getter and setter methods and private members for the new isJavaScriptSupported property, as shown here:

 var _isJavaScriptSupported = (isJavaScriptSupported != null) ?   isJavaScriptSupported : false; this.getIsJavaScriptSupported = function() {   return _isJavaScriptSupported; } this.setIsJavaScriptSupported = function(isJavaScriptSupported) {   _isJavaScriptSupported = isJavaScriptSupported; } 

All that remains is for us to write the constructor. But instead of writing it again from scratch, you can reuse the base class constructor. To do so, Atlas provides the initializeBase() method. The first parameter is the instance of which the base class will be initialized; usually, you provide this as the value. The second parameter is an array of arguments to be passed to the base constructor. In our case, this array consists of the browser name and vendor:

 OReilly.Browser.initializeBase(this, new Array(name, vendor)); 

You can save a few characters and use JSON to create the array:

 OReilly.Browser.initializeBase(this, [name, vendor]); 


Example 8-2 shows the code needed to create and use the new, derived Browser class.

Example 8-2. Using Atlas class inheritance

 ClientInheritance.aspx <%@ Page Language="C#" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head  runat="server">   <title>Atlas</title>   <script language="Javascript" type="text/javascript">   window.onload = function() {     var s = "";     Type.registerNamespace("OReilly");     OReilly.Software = function(name, vendor) {       var _name = (name != null) ? name : "unknown";       var _vendor = (vendor != null) ? vendor : "unknown";       this.getName = function() {         return _name;       }       this.setName = function(name) {         _name = name;       }       this.getVendor = function() {         return _vendor;       }       this.setVendor = function(vendor) {         _vendor = vendor;       }     }     Type.registerClass("OReilly.Software");     OReilly.Browser = function(name, vendor, isJavaScriptSupported) {     OReilly.Browser.initializeBase(this, new Array(name, vendor));       var _isJavaScriptSupported = (isJavaScriptSupported != null) ?         isJavaScriptSupported : false;       this.getIsJavaScriptSupported = function() {         return _isJavaScriptSupported;       }       this.setIsJavaScriptSupported = function(isJavaScriptSupported) {         _isJavaScriptSupported = isJavaScriptSupported;       }     }     OReilly.Browser.registerClass('OReilly.Browser', OReilly.Software);     var ie = new OReilly.Browser("Internet Explorer", "Microsoft", true);     s = ie.getName() + " from " + ie.getVendor() +     (ie.getIsJavaScriptSupported() ? " (w/ JS)" : " (w/o JS)") +     "<br />";     var lynx = new OReilly.Browser("Lynx");     lynx.setIsJavaScriptSupported(false);     s += lynx.getName() + " from " + lynx.getVendor() +     (lynx.getIsJavaScriptSupported() ? " (w/ JS)" : " (w/o JS)");     document.getElementById("output").innerHTML = s;   }   </script> </head> <body>   <form  runat="server">     <atlas:ScriptManager  runat="server" EnableScriptComponents="false">     </atlas:ScriptManager>     <div >     </div>   </form> </body> </html> 

Figure 8-2 shows the results displayed when the page is loaded and its JavaScript runs.

Figure 8-2. Instantiating objects derived from the same base class


Just in case you are wondering, the Lynx text browser does have a vendor. The copyright holder is the University of Kansas.


8.1.2.2. Accessing base methods

When we talk about class inheritance, a logical question is whether methods can be overridden in derived classes. The answer is yes, they can. The next question: is there any way to access the equivalent method of the base class, i.e., the overridden method? Even better, the answer is again yes, Atlas allows you to do so. To demonstrate this, let's add a toString() method to OReilly.Software that outputs the product and vendor names stored by the class. The prototype property ensures automated inheritance and also helps demonstrate access to the base method later on:

 OReilly.Software.prototype.toString = function() {   return this.getName() + " from " + this.getVendor(); } 

You could also directly access the properties _name and _vendor as variables. Using the getter methods is just a personal preference; there is no functional difference in doing so.


In the OReilly.Browser class, you could write a similar toString() method:

 OReilly.Browser.prototype.toString = function() {   return this.getName() + " from " + this.getVendor() +          (this.getIsJavaScriptSupported() ? " (w/ JS)" : " (w/o JS)"); } 

However, it is once again advisable to reuse existing code. We are obviously talking about the base class's toString() method. Atlas provides you with callBaseMethod(), a helper method to call a method from the parent class. You can provide up to three parameters:


instance

The instance whose parent's method to call (usually this)


methodName

The name of the method (as a string)


baseArguments

Parameters for the method, if any (as an array)

In this case, the toString() method of OReilly.Browser can be implemented as follows:

 OReilly.Browser.prototype.toString = function() {   return OReilly.Browser.callBaseMethod(this, "toString") +          (this.getIsJavaScriptSupported() ? " (w/ JS)" : " (w/o JS)"); } 

Then, the code to output the browser information can be reduced a bit to these commands:

 var s = ""; var ie = new OReilly.Browser("Internet Explorer", "Microsoft", true); s = ie.toString() + "<br />"; var lynx = new OReilly.Browser("Lynx", null, false); s += lynx.toString(); document.getElementById("output").innerHTML = s; 

Example 8-3 shows the complete listing.

Example 8-3. Accessing a base class method

 ClientBaseMethods.aspx <%@ Page Language="C#" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head  runat="server">   <title>Atlas</title>   <script language="Javascript" type="text/javascript">   window.onload = function() {     var s = "";     Type.registerNamespace("OReilly");     OReilly.Software = function(name, vendor) {       var _name = (name != null) ? name : "unknown";       var _vendor = (vendor != null) ? vendor : "unknown";       this.getName = function() {         return _name;       }       this.setName = function(name) {         _name = name;       }       this.getVendor = function() {         return _vendor;       }       this.setVendor = function(vendor) {         _vendor = vendor;       }     }     Type.registerClass("OReilly.Software");     OReilly.Browser = function(name, vendor, isJavaScriptSupported) {       OReilly.Browser.initializeBase(this, new Array(name, vendor));       var _isJavaScriptSupported = (isJavaScriptSupported != null) ? isJavaScriptSupported : false;       this.getIsJavaScriptSupported = function() {         return _isJavaScriptSupported;       }       this.setIsJavaScriptSupported = function(isJavaScriptSupported) {         _isJavaScriptSupported = isJavaScriptSupported;       }     }     OReilly.Browser.registerClass("OReilly.Browser", OReilly.Software);     OReilly.Software.prototype.toString = function() {       return this.getName() + " from " + this.getVendor();     }     OReilly.Browser.prototype.toString = function() {     return OReilly.Browser.callBaseMethod(this, "toString") +     (this.getIsJavaScriptSupported() ? " (w/ JS)" : " (w/o JS)");     }     var ie = new OReilly.Browser("Internet Explorer", "Microsoft", true);     s = ie.toString() + "<br />";     var lynx = new OReilly.Browser("Lynx", null, false);     s += lynx.toString();     document.getElementById("output").innerHTML = s;   }   </script> </head> <body>   <form  runat="server">     <atlas:ScriptManager  runat="server" EnableScriptComponents="false">     </atlas:ScriptManager>     <div >     </div>   </form> </body> </html> 

As you see when you run this page, the output of this code is identical to that shown in Figure 8-2.

8.1.3. Abstract Classes

An abstract class is a special kind of base class. It usually contains little implementation code, but does contain signatures for methods that must be implemented by all derived classes. Abstract classes cannot be instantiated directly; they must be subclassed.

Abstract classes can be registered using Atlas's Type.registerAbstractClass() method. You provide up to two parameters:


typeName

The name of the abstract class (as a string)


baseType

A reference to the base class (optional)

Not all methods defined in the abstract class need to be implemented. Atlas currently does not complain if they are implemented anyway. To facilitate the implementation, Atlas provides the special type Function.abstractMethod. Set all abstract methods to this type, and Atlas will prevent subclasses from implementing these methods by preventing code from calling into these abstract methods.

Here is a small example for a generic OReilly.Product class:

 Type.registerNamespace("OReilly"); OReilly.Product = function(name, vendor) {   this.toString = Function.abstractMethod; } Type.registerAbstractClass("OReilly.Product"); 

Another new class, OReilly.InvalidProduct, derives from this class, however does not implement the required toString() method:

 OReilly.InvalidProduct = function(name, vendor) {   OReilly.InvalidProduct.initializeBase(this, new Array(name, vendor)); } Type.registerClass("OReilly.InvalidProduct", OReilly.Product); 

Now what happens if we instantiate the InvalidProduct class and try to access the base toString() method? Atlas throws an exception. So we are using a try...catch block to retrieve the error message and to avoid a JavaScript error:

 var ip = new OReilly.InvalidProduct("Invalid", "Product"); try {   document.getElementById("output").innerHTML = ip.toString(); } catch (e) {   document.getElementById("output").innerHTML = "<b>Error:</b> " + e; } 

Example 8-4 shows the complete code for this example.

Example 8-4. Declaring but not implementing Atlas abstract methods

 ClientAbstractError.aspx <%@ Page Language="C#" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head  runat="server">   <title>Atlas</title>   <script language="Javascript" type="text/javascript">   window.onload = function() {     Type.registerNamespace("OReilly");     OReilly.Product = function(name, vendor) {       this.toString = Function.abstractMethod;     }     Type.registerAbstractClass("OReilly.Product");     OReilly.InvalidProduct = function(name, vendor) {       OReilly.InvalidProduct.initializeBase(this, new Array(name, vendor));     }     OReilly.InvalidProduct.registerClass("OReilly.InvalidProduct", OReilly.Product);     var ip = new OReilly.InvalidProduct("Invalid", "Product");     try {     document.getElementById("output").innerHTML = ip.toString();     } catch (e) {     document.getElementById("output").innerHTML = "<b>Error:</b> " + e;     }   }   </script> </head> <body>   <form  runat="server">     <atlas:ScriptManager  runat="server" EnableScriptComponents="false">     </atlas:ScriptManager>     <div >     </div>   </form> </body> </html> 

Figure 8-3 shows the result of loading the page defined in Example 8-4: abstract methods must be implemented.

Figure 8-3. Attempting to call an abstract method throws an exception


Now that you have seen how abstract methods work, let's make use of this new knowledge. We will again define an abstract base class named OReilly.Product, but this time we'll put some code in it: we'll implement the properties name and vendor (see Example 8-5). The toString() method, however, remains an abstract method. We then have OReilly.Software implement OReilly.Product, and have OReilly.Browser inherit from OReilly.Software. By doing this, we refactor the implementation and change the architecture, but the output remains the same.

Example 8-5. Using abstract methods

 ClientAbstract.aspx <%@ Page Language="C#" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head  runat="server">   <title>Atlas</title>   <script language="Javascript" type="text/javascript">   window.onload = function() {     var s = "";     Type.registerNamespace("OReilly");     OReilly.Product = function(name, vendor) {       var _name = (name != null) ? name : "unknown";       var _vendor = (vendor != null) ? vendor : "unknown";       this.getName = function() {         return _name;       }       this.setName = function(name) {         _name = name;       }       this.getVendor = function() {         return _vendor;       }       this.setVendor = function(vendor) {         _vendor = vendor;       }     }     Type.registerAbstractClass("OReilly.Product");     OReilly.Product.prototype.toString = Function.abstractMethod;     OReilly.Software = function(name, vendor) {       OReilly.Software.initializeBase(this, new Array(name, vendor));     }     OReilly.Software.registerClass("OReilly.Software", OReilly.Product);     OReilly.Software.prototype.toString = function() {     return this.getName() + " from " + this.getVendor();     }     OReilly.Browser = function(name, vendor, isJavaScriptSupported) {       OReilly.Browser.initializeBase(this, new Array(name, vendor));       var _isJavaScriptSupported = (isJavaScriptSupported != null) ? isJavaScriptSupported : false;       this.getIsJavaScriptSupported = function() {         return _isJavaScriptSupported;       }       this.setIsJavaScriptSupported = function(isJavaScriptSupported) {         _isJavaScriptSupported = isJavaScriptSupported;       }     }     OReilly.Browser.registerClass("OReilly.Browser", OReilly.Software);     OReilly.Browser.prototype.toString = function() {     return OReilly.Browser.callBaseMethod(this, "toString") +     (this.getIsJavaScriptSupported() ? " (w/ JS)" : " (w/o JS)");     }     var ie = new OReilly.Browser("Internet Explorer", "Microsoft", true);     s = ie.toString() + "<br />";     var lynx = new OReilly.Browser("Lynx", null, false);     s += lynx.toString();     document.getElementById("output").innerHTML = s;   }   </script> </head> <body>   <form  runat="server">     <atlas:ScriptManager  runat="server" EnableScriptComponents="false">     </atlas:ScriptManager>     <div >     </div>   </form> </body> </html> 

You do have to use the prototype approach to declare the toString() method; otherwise, the subclasses cannot access it. Alternatively you could call registerBaseMethod() once again.


8.1.4. Interfaces

The final OOP-like feature made available to JavaScript by Atlas is interfaces. An interface does not contain any implementation at all but instead specifies the members that subclasses must implement. So even if you inherit from an interface, there is no implementation you can use. This is a good way for developers to keep class structure and implementation details separated in their code.

As you probably have already guessed, the method for creating an interface is Type.registerInterface(). The interface name is the third (optional) parameter of registerClass() or registerAbstractClass(); you provide the interface you have just created there. So let's start with the interface itself:

 OReilly.IProduct = function() {   this.toString = Function.abstractMethod; } Type.registerInterface("OReilly.IProduct"); 

Here, OReilly.Product is the same abstract class as before, which introduces and implements the properties name and vendor. The change comes in the implementation of OReilly.Software. Since we do not want to instantiate this class directly (we have subclasses like OReilly.Browser for that), this class can now also be turned into an abstract one. It derivesas beforefrom OReilly.Product (to get name and vendor), but it also implements OReilly.IProduct (for the toString() method). So, after declaring the class, we register it with the following call to Type.registerClass():

 OReilly.Software.registerClass("OReilly.Software", OReilly.Product, OReilly.IProduct); 

The rest of the code remains unchanged. At the end, you have the following code. It is quite long, so you might consider putting it into an external .js file for legibility of the .aspx file. Example 8-6 shows the complete listing.

Example 8-6. Using interfaces to structure code

 ClientInterface.aspx <%@ Page Language="C#" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head  runat="server">   <title>Atlas</title>   <script language="Javascript" type="text/javascript">   window.onload = function() {     var s = "";     Type.registerNamespace("OReilly");     OReilly.IProduct = function() {       this.toString = Function.abstractMethod;     }     Type.registerInterface("OReilly.IProduct");     OReilly.Product = function(name, vendor) {       var _name = (name != null) ? name : "unknown";       var _vendor = (vendor != null) ? vendor : "unknown";       this.getName = function() {         return _name;       }       this.setName = function(name) {         _name = name;       }       this.getVendor = function() {         return _vendor;       }       this.setVendor = function(vendor) {         _vendor = vendor;       }     }     Type.registerAbstractClass("OReilly.Product");     OReilly.Software = function(name, vendor) {       var _name = (name != null) ? name : "unknown";       var _vendor = (vendor != null) ? vendor : "unknown";       this.getName = function() {         return _name;       }       this.setName = function(name) {         _name = name;       }       this.getVendor = function() {         return _vendor;       }       this.setVendor = function(vendor) {         _vendor = vendor;       }     }     OReilly.Software.registerClass("OReilly.Software", OReilly.Product, OReilly.IProduct);     OReilly.Software.prototype.toString = function() {     return this.getName() + " from " + this.getVendor();     }     OReilly.Browser = function(name, vendor, isJavaScriptSupported) {       OReilly.Browser.initializeBase(this, new Array(name, vendor));       var _isJavaScriptSupported = (isJavaScriptSupported != null) ? vendor : false;       this.getIsJavaScriptSupported = function() {         return _isJavaScriptSupported;       }       this.setIsJavaScriptSupported = function(isJavaScriptSupported) {         _isJavaScriptSupported = isJavaScriptSupported;       }     }     OReilly.Browser.registerClass("OReilly.Browser", OReilly.Software);     OReilly.Browser.prototype.toString = function() {       return OReilly.Browser.callBaseMethod(this, "toString") +              (this.getIsJavaScriptSupported() ? " (w/ JS)" : " (w/o JS)");     }     var ie = new OReilly.Browser("Internet Explorer", "Microsoft", true);     s = ie.toString() + "<br />";     var lynx = new OReilly.Browser("Lynx", null, false);     s += lynx.toString();     document.getElementById("output").innerHTML = s;   }   </script> </head> <body>   <form  runat="server">     <atlas:ScriptManager  runat="server" EnableScriptComponents="false">     </atlas:ScriptManager>     <div >     </div>   </form> </body> </html> 




Programming Atlas
Programming Atlas
ISBN: 0596526725
EAN: 2147483647
Year: 2006
Pages: 146

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net