Object Orientation and PowerBuilder

Object Orientation and PowerBuilder Powerbuilder is a fully Object Oriented development environment. This means that Powerbuilder supports all of the ...
Author: Cynthia Ryan
8 downloads 0 Views 337KB Size
Object Orientation and PowerBuilder Powerbuilder is a fully Object Oriented development environment. This means that Powerbuilder supports all of the accepted requirements for Object Orientation. The definition of Object Orientation is sometimes debatable. We will walk through object orientation as it is used by PowerBuilder.

What is an Object? I guess that question is at the heart of object orientation. An object is a discreet collection of data and functionality. All of that data and functionality is related to everything else in the object. Think of a structure with members that also supports functions. … now put it on steroids. An object can be visual. A window is an object as is a command button and a list box. A Datawindow is an object. You can create your own custom visual objects. An object can be non-visual. A datastore is an example as is a transaction object like SQLCA. So basically an object is the most basic form of object orientation. Usually the functions and data that associated with the object are accessed using dot notation.

Object Variables There are two main types of variables for a class. There are local variables that can be found in events and functions and there are instance variables.

Declaration Local variables are declared inside the event or function. They have the scope of, or 'live' within that event or function only. Once the event or function ends the variable goes out of scope and any memory set aside for that variable is returned to the General Store. In other words, it is gone permanently. It is important to note that the variable scope is that single event or function. That is to say, if you have a variable declared as: string ls_first_name declared in wf_set_values and you have another variable of the same name declared in wf_do_this then those variables will be completely different and one will not affect the other. Allow me to give an example: Variable Scope Example public function string wf_format_name (string as_name); // This function will capitalize the first letter of the // name and return it string ls_return_value ls_return_value = upper(left(as_name, 1)) + lower(right(as_name, len(as_name) - 1)) return ls_return_value end function

public function string wf_no_null(string as_value); string ls_return_value ls_return_value = as_value if isNull(ls_return_value) then ls_return_value = “” return ls_return_value Please take a moment to look at the two functions above. Each of the functions contains an instance of a variable named ls_return_value. The two, even though they have the same name, are entirely different and the value of one will never affect the value of the other. Instance variables are different. The are not declared in an event or in a function. They are declared at the 'class level'. See Illustration 1: Instance Variables. While you are in a painter you will normally find a tab page for declaring instance variables alongside the Event List, Function List and others. If it isn't there you can go to 'View' in the menu and select 'Variables'.

Illustration 1: Instance Variables Once you are inside that painter you can create your instance variables for the class. You can see in our case that I've declared an instance variable that is a private string and named id_user_name. So what does this do? If you will permit me some license here it creates what works as a global version but ONLY for a particular instance of a class. That is to say, in our case the variable is_user_name will be available in every function and every event of the class. If you set is_user_name to “Fred” in one function then in another event it will still be “Fred”. As long as the class remains in memory the variable will be available to any function or event.

Public Variables A variable, when declared is by default Public. If you declare an instance variable to be public then it can be seen from anywhere in the class AND any of the descendants of the class. It can also be see from outside the class. A Public variable is not protected in any way. Not only can other objects see the variable, they can change it.

Private Variables An instance variable can only be private by explicit declaration. It is really simple. You simply precede the instance variable declaration with the word 'private'. Private string is_status = 'new' That variable can only be accessed by the one particular instance of the object. Descendants can not access the variable. It's like that variable doesn't even exist. Private variables exist for that class only. Further, and most importantly maybe, a private variable can not be seen outside the class. That is to say, if I have a class called uo_my_object that has a private variable called is_my_name then the following code is illegal: uo_my_object lo_object lo_object = create uo_my_object string ls_string ls_string = lo_object.is_my_name // Will not compile, is_my_name is not accessible here. You might ask why anyone would want to declare a variable as private. Suppose that you have a variable whose value you would like to control. Just for an example let's choose a variable named ii_age. You may want to control ii_age to make sure that the age is never set to more than 130 or less than 0. If you leave the variable as public then any routine, even one written by another programmer on your team can change it to any number. The solution is to make the variable private and write set and get functions for it. Here is an example for an object called nvo_person. This object is in the example application that comes with the book. In this object I have several private variables and get/set functions for each. I will list just the ii_age variable and its functions. For a more complete explanation of get and set functions see Polymorphism later in this chapter. Private Variables and Get/Set Functions type variables private int ii_age = 0 private string is_first_name = "" private string is_last_name = "" private string is_middle_name = "" end variables public function integer age (); return ii_age // The Get Function end function public function integer age (integer ai_age); // The Set function. First get the current value of li_retVal // then make sure the argument is within acceptable range // finally return the value before it was set int li_retVal li_retVal = age() if ai_age > -1 and ai_age < 130 then ii_age = ai_age return li_retVal

end function If you examine those functions you will see that I create two for each of the private variables. This other objects have access to the variable but restricts the values to which they may be set. Further, the setting functions return the value of the variable before the setting. That way the calling function can easily restore the value should it choose to do so. Now I can control the value of the variables and make certain that the variables are always within an acceptable range.

Protected Variables Protected Variables are similar to Private variable in that they can not be seen outside the class. They can be seen in descendants though. So if you would like to protect variables from being changed but you still want descendants to be able access them then this is the way that you would like to declare them.

Object Functions Objects may have, and usually do have, functions. This is true of all objects, whether they are window objects, or custom objects or even standard objects. Each object, even a custom object, has functions. Those functions are inherited by any descendant. Objects have access just as the variables have. You can declare functions to be public, protected, or private.

Public Functions Object functions are, by default, public. That means that unless you actually change access of the function it will be public and visible... and callable, from any other object.

Private Functions If you set the Access of a function to be private then that function is accessible only from within that particular class. Descendants can not call it and neither can any other object. It is precisely like a private variable. There are several reasons to create a private function. You may want to create a function that is specific to a class. The function might not be applicable to any other class. You might want this function to be a utility function for the class. In that case you would make it private.

Class, Declaration, and Instantiation A lot of people get confused when they speak of an object. There is the definition of an object. In PowerBuilder that is what you create in a painter. This is a class. Then there is the instantiation of the class. This is what happens as the application is running. First you declare an object, then you create it. This gives your program an object with which it can interact. So, if you inherit one window from a window named w_grandpa, and you save it as w_pa then you have created a class named w_pa. If you write code in that class (and why else would you inherit it

other than to write code in it?) then you are manipulating the class. Then later, if you are in a script for some other object, you may both declare and instantiate your object. Each time you create an object you are creating an instance of it. In the code below you will see the two instantiations of one object Instantiation Example nvo_person uo_grandpa //declaration nvo_person uo_pa uo_grandpa = create nvo_person // Instantiation uo_person = create nvo_person You may ask yourself what is the big deal? Why should I take the time to explain this? Because objects can upscale. Upscaling is when you use an assign an ancestor object to a descendant to perform some common operation.

Illustration 2: Upscaling Take a look at Illustration 2: Upscaling. In this illustration we have two objects, one is NVO_PERSON and the second is NVO_CUSTOMER. NVO_CUSTOMER inherits from NVO_PERSON. That's why you see the same functions in NVO_CUSTOMER that you find in NVO_PERSON. Of course NVO_CUSTOMER has a few functions and bits of data that NVO_PERSON doesn't have. That's the bottom half of his class in the illustration. In our example we assign a variable of type NVO_PERSON to a variable of type NVO_CUSTOMER. That's what that left-most arrow is. Can we do that? I mean, is it legal? Yes, it is. I may assign it because NVO_PERSON is smaller than NVO_CUSTOMER. So if I make this assignation the compiler just ignores the part that isn't available in the object of type NVO_PERSON. Now even with a picture that doesn't make a lot of sense. Let's try an example. Suppose several objects inherit from NVO_PERSON. nvo_person nvo_customer

In this example we have four different kinds of people that inherit from nvo_person. Each of those types of people have the same functionality

nvo_vendor that nvo_person has. In each case the descendant has other functionality nvo_employee not shared with nvo_person nor with any of the other objects in the nvo_carrier hierarchy. Suppose that we had a list of these people, vendors, employees, carriers, and customers that we retrieved from the database. This might be a selection of people to whom we are going to send a newsletter. All that we need is their name and address. Further suppose that the name and address are defined in the NVO_PERSON. So we can loop through this list and upscale a local copy of NVO_PERSON over the top of each item in our list. As long as we only use functions defined in our list we will be fine. Here is some sample code that may tie it all together for you. Upscaling Example nvo_person uo_list[] uo_list = f_get_people() int li_count, li_max li_max = upperBound(uo_list) for li_count = 1 to li_max f_print(uo_list.formatted_name_address()) next We have a couple of assumptions here. First we can assume that the function f_get_people will return an array of objects, some are employees, others are vendors, etc. Then we loop through that array and call a function named formatted_name_address for each item in the array. It doesn't matter if that array element is really a customer or vendor. The function is declared in the ancestor to them all so the code will work.

Window Objects Version 1 of PowerBuilder had no object orientation. In version 2 we got the ability to inherit windows. It wasn't until much later that object orientation was fully supported in PowerBuilder. However, inheriting windows was so important that it was released before the full object orientation was ready. I think inheriting windows is still the most common form of inheritance in PowerBuilder. Inheriting a window from another allows you to create functionality that is propagated in a whole tree of windows. It creates, in effect a way to quickly and easily change functionality on many windows at one time. By inheriting windows you can make mass changes very easily. Maybe one of the biggest reasons, or at least one of the most common is to establish uniformity. With inheritance you can establish conventions that are not easily violated. Many development departments, if not most, window up with a hierarchy of windows such as the one shown to the left. The one to the left is 3 levels deep. That is to say that the greatest level of inheritance is represented by the w_db_error and the

w_ancestor w_ancestor_message w_timed_message w_error w_db_error w_system_error w_mdi_frame

w_system_error windows. They inherit from w_error and w_error inherits from w_ancestor_error and that from w_ancestor.

w_sheet w_sdi_window

When PowerBuilder first provided inheritance I think that a lot of us went crazy with the idea. We were so enraptured that soon we had windows inherited 10 layers deep. It became impossible to remember why one window was broken off into a branch in the first place. At one point I was spending a significant part of my day manipulating these windows, moving them around in the hierarchy and trying to make things efficient. Of course the programmers were just confused and they never used hardly any of my carefully designed windows. I learned my lesson. Now I keep the layers of inheritance to a minimum. In fact, I keep the inheritance to a minimum. There is no need to try to hide the programmers from the complexity of their work. For this reason I keep my hierarchy pretty close to the example above.

Non Visual Objects A Non-Visual Object is the original name for a Custom Class. A custom class is basically an object that had no visual properties. It is a place for you to have data and functions that are related to one another. Experience has shown me that more often than not I use a class as a substitute for a structure. I'll often pass a class as an argument or return a class from a window or function. Of course there are other uses for custom classes. Some programmers form complex hierarchies of classes. Personally I have found that it is rare that I use the Custom Class for anything beyond arguments.

Standard Objects A Standard Object is an object that is non-visual and provided by PowerBuilder. Creating a new Standard Object is the same as inheriting from it. There are several objects that can come in handy but the one that I most often use is the Transaction object. I'll most often create a new transaction, modify it, maybe add a function or two, and then tell PowerBuilder that it should use my transaction in place of sqlca. Another commonly used Standard Object is the DataStore, exception, and error. There are many others as well.

Custom Visual Objects Custom Visual Objects are a way that you can combine objects to create a whole new visual object. When you create a new Custom Visual Object you find yourself in what appears to be a window painter. You can then add regular objects such as command buttons, static texts, just like a window painter. You can code events and functions, you can create custom events. In fact you can do just about anything that you could do inside a window painter. You can even add embed other Custom Visual Objects inside your new Custom Visual Object. In this case I'd like to present a custom visual object that I've used for years. It's a perfect example of this powerful object.

The DataWindow VCR Object The DataWindow VCR object will consist of First-Prev-Next-Last buttons all bundled into one easy to use object. When it is completed you will be able to just drop the object on a window, close to the datawindow, write one line of code 'registering' it with the datawindow control and it will then automatically provide functionality for scrolling the datawindow. The first thing is to create an ancestor datawindow. If already have one then you can skip to the next step. If not then create a new Standard Object as shown in Illustration 3: I named mine uo_ancestor_datawindow. I just put a comment in it and save it because I need a reference to it for my ancestor picturebutton.

Illustration 3: The next step is to create an ancestor picturebutton for the datawindow. We could just make four uo_ancestor_pb_dw different picturebuttons, and add the same function to each of them but uo_pb_first that would not be following object oriented principles. If you have several uo_pb_prev objects that all use an identical function or event then that function or uo_pb_next event should be in the ancestor for those objects. uo_pb_last In our case the ancestor datawindow needs to carry an instance variable to the datawindow upon which it operates and it needs to have a function that allows us to set that datawindow instance variable. The descendant picturebuttons will each use that instance variable. For example, the picturebutton that called uo_pb_dw_first would scroll to the first row of the instance variable. uo_ancestor_pb_dw This is the top level picturebutton and the first that you need to create. Create a new standard class and

select Picturebutton from the list. Then you simply add the following instance variable: uo_ancestor_pb_datawindow Instance Variables protected uo_ancestor_datawindow idw Note that we need to use protected rather than private because the descendants of this object need to reference idw. Finally all that you have to do is create the function. uo_ancestor_pb_datawindow.Register(uo_ancestor_datawindow adw) Idw = adw uo_pb_dw_first Now we have an ancestor picturebutton and an ancestor datawindow. Let's start with the buttons that actually do the work. First inherit from uo_ancestor_pb_datawindow. You can call it uo_pb_dw_first. Set the bitmap for the button to be vcr_pb_first and then in the clicked event add the following code: uo_pb_dw_first.clicked() if isNull(idw) then return if idw.rowcount( ) = 0 then return idw.scrollToRow(1) idw.setFocus() At this point I would like to encourage you to test your object. Just create a tabular datawindow and throw it on a window. Then add code in the rowFocusChanged event that selects the current row. Finally put a uo_pb_dw_first button on the window and in the open event retrieve your datawindow and register the datawindow to the pb. Then scroll down and select any row that is not the first. Click on your button and if you did everything right the datawindow should scroll back to the first row. Later, after we've developed all four picturebuttons I will show you that code. uo_pb_dw_prev Now we do practically the same thing that we did with the first button. Just inherit the button from the same ancestor, set the bitmap and add the following code to the clicked event. uo_pb_dw_prev.clicked() if isNull(idw) then return if idw.rowCount() < 2 then return long ll_row ll_row = idw.getRow() if ll_row < 2 then return idw.scrollToRow(ll_row - 1) idw.setFocus() uo_pb_dw_next You can probably see a pattern here. Inherit from the ancestor, set the bitmap, then add the appropriate code to the clicked event. For the next button the code is as follows:

uo_pb_dw_next.clicked() if isNull(idw) then return if idw.rowCount() < 3 then return long ll_row ll_row = idw.getRow() if ll_row >= idw.rowCount() then return idw.scrollToRow(ll_row + 1) idw.setFocus() uo_pb_dw_last Once again, this is another easy button. Inherit from the ancestor, change the picture, and add the following code to the clicked event: uo_pb_dw_last.clicked if isNull(idw) then return if idw.rowCount() < 3 then return long ll_row ll_row = idw.getRow() if ll_row >= idw.rowCount() then return idw.scrollToRow(idw.rowCount()) idw.setFocus() Sanity Check We could be done here. We could just let the programmers put four buttons on the window, close to the datawindow then call the register function for each of them. That would work. In fact, I strongly suggest that at this point you do precisely that. Put your datawindow on a window and add this code to the open event. w_test.open dw_1.setTransObject(sqlca) dw_1.retrieve() pb_first.register( dw_1) pb_prev.register( dw_1) pb_next.register( dw_1) pb_last.register( dw_1) Here is an idea. Take a look at the clicked event of each of the four picturebuttons. You may notice that the first line in all four is exactly the same (Actually I did that on purpose). When you find that this has happened you need to move that line to the ancestor object and delete it from all the descendants. This is a common thing for object hierarchies. uo_dw_toolbar Now we have the four buttons. We can take advantage of the real power of objects.

Illustration 4: In Illustration 4: we see what we are trying to accomplish. The four buttons will be encapsulated inside a toolbar. The toolbar will provide a function (in object oriented parlance it's called exposing a function) that will take a datawindow. That datawindow will then be passed on to the four buttons. So the window is only responsible for calling the one object. The four buttons can be positioned correctly and the programmer doesn't have to worry about lining them up every time. It really is the easiest way. So the next step is to create a Custom Visual object.

Illustration 5: Once you've done this you get what looks like a window painter. Put drag your four objects from the system tree over to the surface of the painter and line them up,

Illustration 6: Finally all we have to do is write one function to tie this all together. It's a special register function for the object. uo_dw_toolbar.register(uo_ancestor_datawindow adw) pb_first.register(adw) pb_prev.register(adw) pb_next.register(adw) pb_last.register(adw) Now to make this work just replace your four individual buttons on your test window with this toolbar and call register once as shown in the code below. w_test.open dw_1.setTransObject(sqlca) dw_1.retrieve() uo_toolbar.register(dw_1) Yet another enhancement Did you think we were done? Well, Microsoft Access has an object that I've always liked. It's like the vcr toolbar that we just did but it has a single line edit in the middle that allows you to type in the row that you like and go straight to it. To do this we simply create a single line edit that we can register with a datawindow, write the code, being careful to restrict the user to valid rows, then inherit from our vcr toolbar and add it. Of course

we have to write a register function for our new access toolbar. That function needs to call the ancestor toolbar register function. The syntax is super::register(adw). That registers the four buttons, then just register your single line edit. Let's start with the single line edit. uo_sle_dw Just like the picture buttons the task is to inherit from the singleline. It's a Standard Visual object just like before. It needs the same instance variable. uo_sle_dw instance variables uo_ancestor_datawindow idw After that the register function, which is again, just like the others. uo_sle_dw register(uo_ancestor_dw) idw = adw We finish up this object with code in the modified event. uo_sle_dw.modified if isNull(idw) then return if idw.rowCount() = 0 then messagebox("Error", "No data found") return end if long ll_row ll_row = long(this.text) if ll_row < 1 then messagebox("Error", string(ll_row) + " is invalid") return end if if ll_row > idw.rowCount() then messagebox("Error", "There are only " + string(idw.rowCount()) + " records") return end if idw.setrow(ll_row) idw.setfocus() This will work. Put the new single line edit on the window and register it. Now type a number in there and watch the datawindow change rows. Is this it? Are we done? I don't think so. Play with your window for a little bit. Change the number, let's say that you put in the number 10. Watch your datawindow highlight the 10th row. Now use the vcr toolbar and click the Next button. Your row in the datawindow changes to the 11th. But the single line edit still shows 10.

That's not good. To fix this we have to register the single line edit with the datawindow. The datawindow needs to change the single line edit when it changes rows. This isn't difficult, it's similar to what we did before. We have to go back to the ancestor datawindow and add an instance variable, a register function, and some code in the rowFocusChanged event. uo_ancestor_datawindow instance variables uo_sle_dw isle Now the register, again, it's very simple. uo_ancestor_datawindow.register(uo_sle_dw asle) isle = asle To finish this part we put code in the rowFocusChanged event. uo_ancestor_datawindow.rowFocusChanged if isValid(isle) then if not isNull(isle) then isle.text = string(currentrow) end if end if We have to use the isValid function call here because the single line edit is never null. It is autoinstantiated so it may not be valid or usable but it would still not be null. So why the second test? Why do I check if it is null when it can never be null? It's because I'm compulsive and don't fully trust that it can't be null. Something in my programmer brain tells me that anything can be null. Now the single line edit will work perfectly. What's next? Is there more? Of course there is. Currently the programmer has to put both the vcr toolbar and the single line edit on the window and register both. Isn't there some way to fix that? Isn't there some way to combine them so that we can have just one line of code? uo_access_toolbar Inherit from uo_dw_toolbar. Move the next and last buttons over to the right to make room for your new single line edit. Then drag your ancestor single line edit between the buttons. Size them appropriately and make them look like Illustration 7: As you can see we have a vcr toolbar but with a single line edit in the middle. This is a very handy object. I use it for just about any kind of presentation style. It works well for tabular, grids, and even free-form datawindows. Most importantly we are going to write this so that it will automatically work with just one line of code. Illustration 7:

To start we have to write the register function for our new

object. uo_access_toolbar.register(uo_ancestor_datawindow adw) sle_1.register(adw) super::register(adw) Finally we just save this. We just extended this object with two lines of code. Now let's look at the open event of the test window. w_tester.open() dw_1.setTransObject(sqlca) uo_toolbar.register(dw_1) dw_1.retrieve()

Constructors & Destructors Two events are common with every object in PowerBuilder. The constructor is an event that fires when the object is instantiated. Remember that instantiation is not the same as declaration. Instantiation happens when the create function is called.

nvo_person lo_person lo_person = CREATE nvo_person // Constructor fires here. Of course there are objects that don't need the CREATE statement. The object might be autoinstantiated. You can find a checkbox labeled autoinstantiate on the general tabpage of the properties. If you check that then you no longer need to use the CREATE statement. In that case the constructor is fired at the declaration. You can use the Constructor for initializing values. I used to do this a lot back in the days before you could initialize in the declaration. There was a time when the following line was illegal: protected string is_name = “Unknown” So in the constructor of the object I would add that line (without the protected prefix of course). Now there is less use for the constructor event. I haven't used it in years. The Destructor is the opposite of the constructor. It happens just before the memory for the object is returned to the general store. So you can use the destructor to do any cleanup that you need. You can also use the destructor event to prompt the user to save changed data. I avoid using the destructor. It is very dangerous. Suppose you add code to the destructor of a datawindow and decide that you want to cancel the destruction of the object. That can easily be done by returning a 0, but if the datawindow is destroyed as a result of the window being closed then returning a 0 will have no effect. The datawindow will go away anyway because the window is gone. For data cleanup I much prefer to put my code in the close event of the window.

Polymorphism I can't leave an explanation of Object Orientation without addressing Polymorphism. As a Senior Programmer I use polymorphism a lot. There used to be a time when programming involved having a notebook with hundreds of functions in it. I had functions named print_address, print_name_address, print_name_address_phone and on and on and on. I was constantly looking up the names of functions. I'd forget if that was print_name_address or was it print_name_and_address? Polymorphism solves this problem. When you have two functions with the same name the compiler will look at the arguments in the function name. PowerBuilder can tell the difference between print_name(“Rik”, “Brooks”) and print_name(uo_name_holder). This means that I can write several functions, all with the same name as long as the argument list is different. In other words the programmer doesn't have to worry about remembering all those different 'print' functions. He only remembers 'print'. It's the argument list that controls which is called. Sure, I still have to write all the different functions so personally I don't save much effort but polymorphism certainly does save the other programmers a lot of effort. Another area where polymorphism is handy is in the Set/Get functionality. We've already seen this in use. If I have a private variable that I want to allow others to set I will write a function for it. In fact, I'll write two functions for it. Both functions will be named similar to the instance variable. The section earlier in this chapter entitled “Private Variables and Get/Set Functions” describes this nicely

Conclusion This chapter doesn't cover all that can be said about Object Orientation and PowerBuilder. That would require a book by itself. In fact that particular book is on my list to write. This chapter should give you enough of an introduction to the subject that you can start exploring this powerful metaphor on your own. More importantly, it will give you enough information that will allow you to understand the rest of this book.