This article is part 1 of 1 from the series: How to program AVR for .NET with classes and OO techniques
  1. Introduction to using classes with AVR for .NET

We've noticed in tech support that customer interest seems to move in waves. One week it seems that everyone wants to know about data grids, then the next week everyone wants to know about open query files. It's almost like our customers have a secret meeting on Friday afternoon and then spring the new topic on us all at once the following Monday.

Lately, I've noticed the Friday meeting must have been about using AVR for .NET's OO capabilities to add power and sophistication to their programs. I think one of the reasons so many customers are riding this wave is the number of AVR Classic customers moving to AVR for .NET. The advent of Windows 10, and the realization that COM won't last forever, has probably provided some motivation for this.

We have a several-years-old AVR class workbook that has two important chapters on using classes with AVR. Chapters 2 and 5 are required reading for anyone moving to AVR for .NET from any procedural language. That's the good news. The bad news is that while the material isn't really technically dated, it needs to be on the Web and it needs a bit of a refresher (it has some typos and its original format doesn't lend itself well to conversion to HTML). So, that's what we're starting here. This is the first in a new series of articles about using classes and some OO concepts with AVR. Some of these articles will be featured in the ASNA newsletter, but they'll also be posted as regular content in our new knowledge base. Keep an eye on that section of ASNA.com for more articles in this series.

The video version

For better control and resolution view the video directly at YouTube

The text version

Unlike AVR Classic or green-screen RPG, AVR for .NET is built on an object oriented foundation. In AVR for .NET (for the rest of this article "AVR" refers to AVR for .NET), classes define a "program boundary". That is, variable scope (which includes disk files and their record formats) is contained within a class--unless you specifically declare a variable public.

In .NET, some classes present themselves implicitly. For example, in Win or Web forms, each form gets its own class (often called the "code-behind" or "code-beside" class). Think of these classes as structural. You can create a pretty good AVR app using nothing but these implicit structural classes and procedural code. However, for non-trivial applications, putting all of your code into these structural classes makes a challenging, hard-to-test and hard-to-maintain application. You end up with a boar's nest of subroutines, event handlers, and virtually no distinction between business logic, file IO, and UI management.

One of the things many AVR coders could do to improve their applications is to use more classes to separate, or partition, distinct parts of the app from each other. We'll dig more into the motivations for partitioning in a moment, but first, let's have a class refresher course. In academic parlance, a class is a template (or blueprint) for an instance of an object. The class provides data and, usually, actions to perform on that data. It "encapsulates" that data and those actions from the rest of the application. This separation of concerns improves maintenance, reuse, and testing by imposing a strict boundary between the class and the rest of the app (its other classes). The only data and/or actions that a class makes available to other classes are those members that you explicitly expose. Let's look at a small example.

A simple class

Figure 1a below shows a very simple class with two public properties (Sales and Returns) and one private field (Rank). The Sales and Returns values are available to the outside world; the Rank value is only visible inside an instance of the class. The Access keyword controls member visibility. *Public members are available outside the class instance, *Private members are not. You'll often see using private members referred to as data hiding. While that might sound like a bad thing, it's a good thing. Private members do work inside the class and their values don't need, and shouldn't be, visible or changeable outside the class.

BegClass SalesData Access(*Public) 
    DclProp Sales   Type(*Packed) Len(12,2) Access(*Public)  
    DclProp Returns Type(*Packed) Len(12,2) Access(*Public) 

    DclFld  Rank    Type(*Integer4) Access(*Private)   
EndClass

Figure 1a. A small class with data members only.

In AVR Classic or green-screen RPG, you would have probably used a multi-occurrence data structure or a data structure array to store values. As you'll see, using a class, rather than these old-school storage techniques, adds considerable possibilities.

The code below in Figure 1b shows how to use an instance of the SalesData class. Note the word "instance." That's important. Your code doesn't work with the SalesData class, rather it works with an instance of it. The SalesData class is an ethereal definition of data (and, optionally actions) and it doesn't contain data. An instance of it does.

DclFld sd Type(SalesData) New() 

sd.Sales = 45
sd.Returns = -17  

Figure 1b. A small class with data members only.

In the code in Figure 1b, the instance of SalesData is named sd. The New after its declaration causes an instance of SalesData to be created in memory. That instance is assigned to sd. Access to a class instance's public members is available through a fully qualified reference; ie, sd.Sales allows access to this instance's Sales member. For all intents and purposes, what we've created here is a special case data structure. As written it doesn't seem to offer much of a benefit over a traditional data structure.

Passing class instances

A benefit of a class instance over a traditional RPG data structure is that class instances can be passed as arguments to subroutines and functions. This allows you to dispatch the global nature of a traditional data structure. Before we dig into passing a class instance as an argument, let's review passing scalar numeric types.

By default AVR for .NET passes arguments to functions and subroutines by value. This ensures that when you pass an AVR intrinsic scalar value type (*Integer4, *Zoned, *Packed, etc) changes made to passed arguments are not seen by the caller. For example:

DclFld y Type(*Integer4)
...
y = 55
MySubr(y)
// y is still 55 after the call.
...
BegSr MySubr 
    DclSrParm x Type(*Integer4) 

    x = 5
EndSr     

Value types (which, with one exception are all numeric) directly contain a value. Instances of classes you create with AVR are reference types, where the instance variable contains a reference to the class instance. For example, in Figure 1c below, the sd variable contains a reference to an instance of SalesData. Surprisingly, when you pass the sd instance by value, changes made in the called routine are reflected in the caller. Notice how sd.Sales is 450 after the call to TestByVal. What you're passing in this case is a reference to the class, not a direct value or values.

DclFld sd Type(SalesData) New() 

sd.Sales = 45
sd.Returns = -17
TestByVal(sd)
// sd.sales is now 450. 

BegSr TestByVal
    DclSrParm sd Type(SalesData) 

    sd.Sales = 450
EndSr

Further, if you pass sd by reference, changes made in the called routine are also reflected in the caller. For example, in the code below, sd.Sales is 450 after the call to TestByRef.

DclFld sd Type(SalesData) New() 

sd.Sales = 45
sd.Returns = -17
TestByVal(*ByRef sd)
// sd.sales is now 450. 

BegSr TestByRef
    DclSrParm sd Type(SalesData) By(*Reference) 

    sd.Sales = 450
EndSr

The full reason for this behavior is beyond the scope of this article, but is related to how value types and reference types are stored in the stack and the heap. Read this article for more info. But don't agonize over it very much, just understand the implications.

The notion that members in a class are exposed as you pass a class instance around is both a good thing and a bad thing. It's a good thing because you can pass around a class instance with lots of data in it effectively because you are simply passing a reference to the class instance around. However, as your programming antennae probably tells you, it's also a potentially bad thing because any routine to which you pass the class instance has the right to change public values in that class instance.

The upshot? Code your class member visibility carefully, making sure to only make public those members that truly need to be public. Remember, too, that AVR arrays are reference types, so this warning applies to them, too.

Note also that while strings in .NET are reference types, they are exempt from the exposure just discussed. AVR's *Char is implemented as a System.String. System.String is a reference type, but it is a special-case reference type. Most reference types have to be explicitly instanced and most reference types can't be passed by value; *Char and its kissing cousin System.String are reference types that obey value type semantics. That is, they don't need to be explicitly instanced and they can be passed by value.

Methods give classes actions

A modified SalesData class is shown in Figure 1c with an added member, GetNetSales(). This is a function that returns the sum of Sales and Returns.

BegClass SalesData Access(*Public) 
    DclProp Sales   Type(*Packed) Len(12,2) Access(*Public)  
    DclProp Returns Type(*Packed) Len(12,2) Access(*Public) 

    DclFld  Rank    Type(*Integer4) Access(*Private)

    BegFunc GetNetSales Type(*Packed) Len(12,2) Access(*Public)
        LeaveSr *This.Sales + *This.Returns 
    EndFunc 
EndClass

Figure 1d below shows GetNetSales() being called to return the net sales. While this calculation is very simple, in the real world it represents a complex calculation. With this computation power added to the SalesData class, that class is now much more powerful than a traditional data structure. In this case, it is a data structure smart enough to total itself. Not a ton of smarts, for sure, but 100% more than any old-school data structure has!

DclFld sd Type(SalesData) New()
DclFld NetSales Type(*Packed) Len(12,2)  

sd.Sales = 45
sd.Returns = -17

NetSales = sd.GetNetSales()

Enough for now

That's plenty to absorb right now. As we move forward, the article in this series will focus on delivering something tangible with AVR and classes. File IO is one place where a great payoff is available by the effective partitioning of your code into classes. Check back soon for part 2.

This article is part 1 of 1 from the series: How to program AVR for .NET with classes and OO techniques
  1. Introduction to using classes with AVR for .NET