REALbasic Cross-Platform Application Development

The fact that REALbasic uses an event-driven paradigm is out of your control. You have no choice in the matter and you need to program within those constraints. You do have a choice about how you program within those constraints, and one of the most basic choices is how you go about organizing your codeboth from the practical perspective of where you save your files and from the conceptual perspective of how you go about creating classes and performing tasks. I'll start first with the conceptual perspective.

The Model-View-Controller architecture (MVC) is the one that I have used with the RSSReader application. In this framework, the program is divided up into three distinct domains or areas of responsibility. The view refers to the user interface (which is why MVC is so commonly used when programming GUI applications). The view presents information to the user and accepts input from user interaction. The model is the data model for your application, the units of information that your program is manipulating. The controller mediates the interaction between the view and the model. The view responds to a user request by referring it to the controller. The controller performs any application logic required to process the request and then refers the appropriate model (an object) to the view to be presented to the user.

When you are writing your application, some code that you write will be involved in managing the user interfacethings such as determining how particular controls respond to particular events. This is sometimes referred to as presentation logic. You will also be writing code that performs the logic of your application, sometimes called domain or business logic. You want to minimize instances where business logic and presentation logic appear in the same method or object, and this can be something of a challenge at times. In the RSSReader application, I keep these unique areas of responsibility distinct, and I also organize my project in such a way as to make it clear which classes belong to which particular layer.

Organizing Your Code

One way to organize your project is to make judicious use of folders. Folders in the IDE work just like folders do on your hard drive. You are able to save classes and modules into folders.

REALbasic doesn't have the concept of packages in the same way that Java does. This means that organizing your classes into folders doesn't affect the way that you call those classes. This lack of namespaces is a limitation of REALbasic that should be corrected in the future. One side effect is that it makes "namespace collisions" easier to happen. In other words, no two classes can have the same name. In languages such as Java, you can avoid this by grouping classes into packages and using that as a way to refer to the class. Not so in REALbasic.

In the RSSReader application, I have the following folders set up:

  • Providers

  • Preferences

  • RSSItems

  • Viewers

  • Writers

The Providers and Writers folders hold classes responsible for the model layer. Viewers holds classes responsible for the view layernamely Window and Control subclasses and supporting classes. RSSItems contains the RSS class, which is a subclass of App and which serves as the controller for this application.

Windows and Controls are responsible for two things: presenting information to the user and responding to user requests.

In this case I am developing an RSS reader, so the model will represent the RSS data. The viewthe way that the data encapsulated in an RSS objectis presented to the user. This is done using Windows and Controls. In addition to presenting information to the user, Windows and Controls also allow the user to interact with the application and edit data, request data, or whatever happens to be appropriate for that action. The third area, the controller, manages the interactions between the model and the view, and this is managed by the RSS class. I'll use this basic approach to develop the rest of the RSS application.

At this stage, the RSSReader application is not complete, and the following represents a simplified view of the final application, primarily because I have not yet covered XML. In what follows, I will present the classes I have developed along with some explanation of the role they play. I will not spend a lot of time reviewing individual lines of code now, but will instead refer to it when writing about programming with Windows and Controls later in the chapter.

The RSSReader Application

There are many small applications where the formalisms of this kind of structure are not required. As you will see in the following pages, REALbasic makes it very easy to mix your application's business logic with that of its presentation logic. Sometimes that's okay. At the same time, my experience has been that your life is much easier if you keep a distinct line between presentation and business logic because of the flexibility it gives you in the future. A screenshot of the RSSReader application can be seen in Figure 5.1.

Figure 5.1. RSSReader application screenshot.

Here is a description of how the RSSReader application will work:

  • The user will be able to subscribe to RSS channels by typing a URL into an EditField and clicking a Subscribe button.

  • A complete list of subscribed-to RSS channels will be maintained and displayed in a ListBox. The list will be originally maintained in a text file. Later in the text, it will be reimplemented using a database.

  • The application will use XSLT to convert the RSS XML files into HTML for viewing.

  • A TabPanel will allow the user to switch between the HTML view and the plain text/XML view of the channel.

  • When a channel that is listed in the ListBox is selected, the channel will be displayed in an HTMLView control on the TabPanel.

  • The plain text will be viewed in an EditField on the TabPanel as well.

Preferences will be maintained so that the main application Window returns to the same state as it was the previous time it was opened. Next, I will review the classes that will implement this functionality.

Controller

Class RSS Inherits Application

The App object is supposed to represent the application itself, so I implemented the controller as a subclass of App, called RSS. There are now two related objects in the projectApp and RSSbut you can safely ignore RSS and call App when you need to. REALbasic automatically uses the lowest subclass on the App hierarchy, so all the properties and methods I have established in RSS will be available.

The complete implementation follows. There are some items in it that you may not understand yet, but most of them will be discussed later on in the chapter. I want to present the class completely so that you can see how everything fits together, and I will refer back to it when necessary.

RSS Properties

The RSS object has five properties, three of which are interfaces. The iPreferences interface has been seen before; this is what gives us the capability to easily employ a cross-platform way of handling user preferences. At this stage in the application, I will not be using the iProvider property. Its function will be to read XML files from either the file system or through an HTTPSocket connection. This will be revisited when we examine REALbasic's XML classes. The iWriter interface is used to write data to some source, whether it's a file, a socket, or the like. At this stage, it is used to save updated versions of the RSSFeeds file that stores the user's subscription information. Viewer is currently a ListBox, but in a later version this will be updated to use a ListBox subclass called mwNodeListBox that can be used to display hierarchical data. Finally, the Feeds property represents the class that's responsible for opening and saving the RSS feed file.

Preferences As iPreferences Provider As iProvider Writer As iWriter Viewer As ListBox Feeds As SubscribedFeeds

The RSS class itself implements only two methods at this pointSubscribe and Delete. These methods will be called from different parts of the application, including the MenuBar and as a consequence of user action. The methods themselves make use of properties of the RSS object, which I discuss next.

Listing 5.1. Sub RSS.Subscribe(aUrl as String)

Me.Feeds.addItem(aUrl)

Listing 5.2. Sub RSS.Delete()

Me.Viewer.RemoveRow(Me.Viewer.ListIndex) Me.Writer.Write(Me.Feeds)

The following Events and MenuItems will be discussed throughout the rest of the chapter. One thing to notice at this point is how the RSS object uses the Open event to initialize all its properties.

Listing 5.3. Sub RSS.Open() Handles Event

// Initalize Preferences #if TargetWin32 then Preferences = iPreferences(new WindowsPreferences("info.choate.rssreader")) Dim myTrayItem as CustomTray myTrayItem = New CustomTray Me.AddTrayItem( myTrayItem ) #elseif TargetMacOS then Preferences = iPreferences(new Preferences("info.choate.rssreader")) #endif If Preferences <> Nil Then Preferences.set("Name") = "Mark Choate" Preferences.set("Location") = "Raleigh, NC" #if TargetMacOS Then Preferences(Preferences).save #endif End If // Initialize Viewer Viewer = Window1.ListBox1 // Initialize Feeds Feeds = New SubscribedFeeds() Feeds.registerListBox(Window1.ListBox1) Feeds.open // Initialize Writer Thread Writer = new tWriter // Start running the Thread tWriter(Writer).Run

Listing 5.4. Sub RSS.EnableMenuItems() Handles Event

If Window1.EditField1.Text <> "" Then FilekSubscribe.Enabled = True Else FilekSubscribe.Enabled = False End If If Viewer.ListIndex > -1 Then EditClear.Enabled = True Else EditClear.Enabled = False End If

Listing 5.5. Sub RSS.CancelClose() as Boolean Handles Event

Dim res as Integer res = MsgBox("Are you sure you want to quit?", 17, "Quit RSSReader") If res = 2 Then Return True End if

Listing 5.6. MenuHandler Function RSS.EditClear() As Boolean

// Call RSS.Delete method Delete

Listing 5.7. MenuHandler Function RSS.FilekSubscribe() As Boolean

// Call RSS.Subscribe method Subscribe(Window1.EditField1.Text)

iPreferences Interface

The iPreferences interface is the first of several interfaces the RSSReader uses. iPreferences specifies a generic way of setting and accessing preference data so that the interface is the same, whether the Windows Registry is being used, or a preferences file is being used on Macintosh or Linux. Preferences are considered part of the controllerit's how the controller maintains state between user interactions. The data that is saved does not represent the actual data model.

Function get(aKey as String) As String End Function Sub set(aKey as String, assigns aValue as String) End Sub Sub setAppIdentifier(aName as String) End Sub Function getAppIdentifier() As String End Function

The Model

The "Model" in Model-View-Controller refers to your application's data. You can further break down the model into two constituent parts. In this example, there is an XML file that stores the RSS data, and that is the first part, sometimes called the data access layer. The second part is the representation of that data in memorythe actual object model. REALbasic provides XML classes, and it is very easy to use those classes to open RSS files, which means that you could use the XMLDocument class as your model. However, it is generally thought to be better to not have your object model so closely tied to any given representation of that model. For instance, even though RSS files are most commonly stored as XML files, there are four different competing formats the XML documents can take, all of which are very similar, but different enough that you could not rely on XMLDocument objects created from them to have consistently named attributes and elements. For example, what one format calls a "Channel" is called a "Feed" in another.

Another important thing to consider is the physical location of the files. The program will get RSS files through the Internet, but it will cache those files locally. It will use the local file until it's time to refresh it with a new version from the Internet. Also, it's conceivable that there will be new versions in the future, or that the data could be stored in some other format altogether, such as in a database. If that's the case, every time a new format or source of information is made available, you will have to modify all those parts of your application that interact with the XMLDocument object you are using to represent it.

The approach that I will be using is one I have developed over time working with XML documents and content management systems.

Data Access Layer

The data access layer is responsible for getting the data required to instantiate the domain object. Presently, for the RSSReader application, this means being able to retrieve XML files from the Internet, or files stored locally on a hard drive, as well as being able to write files to the local drive so that they can be cached for later use, or something similar. The following interfaces and classes compose this area of functionality.

iProvider Interface

The iProvider interface is a simple interface that accepts a URL (just a string, really) and returns a document. The mwNode class is used as a generic representation of any kind of document, and that will be discussed more later in the chapter. The benefit to this interface is that the sections of the application that request documents do not need to know how the object that implements the interface is getting the mwNode object. This means that the implementing object can get the source data from a file, from an HTTP request, a database, a socket, and so on. These four sources of data would never share much code, so subclassing would not provide much benefit. As a result, I define a very simple interface that can be used for any data source I choose:

Sub open(aUrl as String) as mwNode End Sub

iWriter Interface

iWriter works in tandem with the iWritable interface. There is a REALbasic Writable interface, but it didn't provide exactly what I was looking for. In this application a thread subclass implements the iWriter interface, and it has a Write method that accepts an iWritable object in the parameter. This means that I can send any object to this thread to be written as long as it implements the iWritable interface. Also, much like iProvider, using an interface for iWriter means that I have a lot of flexibility in determining what it means to Write something. I could write to a file, a database, a TCPSocket, and so on, and the rest of my program need be none the wiser.

Sub Write(toWrite as iWritable) End Sub

iWritable Interface

The iWritable interface is simple: just a Write subroutine. It is passed to the RSS.Writer property and represents the document to be written. Again, iWriter can write any object that implements this interface.

Sub Write End Sub

Class SubscribedFeeds

The RSS.Feeds property is an instance of the SubscribedFeeds class. It's used to open the file that contains the list of subscribed-to feeds, as well as to write out the file when it changes. It implements the iWritable interface.

Listing 5.8. SubscribedFeeds Properties

List As ListBox RSSFile As FolderItem

Listing 5.9. Sub SubscribedFeeds.registerListBox(aListBox as ListBox)

me.List = aListBox

Listing 5.10. Sub SubscribedFeeds.write()

//I am storing the Feeds file in the ApplicationSupportFolder. //The file is opened during the Open method of the class. //iWritable Dim tos as TextOutputStream Dim x,y as Integer Dim s as String tos = RSSFile.CreateTextFile y = me.List.ListCount - 1 For x = 0 To Y s = me.List.Cell(x,0) If s <> "" Then tos.WriteLine(s) End If Next tos.Close

Listing 5.11. Sub SubscribedFeeds.open() Handles Event

Dim tis as TextInputStream Dim s as String RSSFile = ApplicationSupportFolder.Child("info.choate.rssreader") If RSSFile.Exists Then tis = RSSFile.OpenAsTextFile If (tis <> Nil) And (List <> Nil) Then While Not tis.EOF s = tis.ReadLine() If s <> "" Then List.AddRow(s) End If Wend End If End If

The addItem method adds a URL to the ListBox that was assigned in the RegisterListBox method, and then saves the updated data to the file.

Listing 5.12. Sub SubscribedFeeds.addItem(aURL as String)

me.List.addRow(aURL) me.write

Listing 5.13. Function SubscribedFeeds.writeError() as Boolean

If RSSFile.LastErrorCode <> 0 Then Return True Else Return False End If

Object Model

The object model will be the classes that are used to represent the data found in RSS files, in their various formats. I have developed a hierarchy of classes that I use to provide a generic representation of documents and document collections in other applications I have developed, and I will use a similar approach here.

You may already be familiar with the DOM, or Document Object Model that is used in XML and HTML. It treats a document like a tree; in HTML, the root is the HTML document, and that document has two branches: the head and the body. The head can have any number of branches, one of which is the title of the document. Likewise, the body can have any number of headings, paragraphs, and so on.

An RSS document can be viewed the same way. At the root is a channel and the channel contains a sequence of items, each of which has a title, a URL, and a description. (In recent alternative formats, the channel is called an entry.) If you think about it, all documents and collections of documents can be modeled this way. A book is a sequence of chapters, a chapter is a sequence of paragraphs, a paragraph is a sequence of words, and words are a sequence of letters. In all cases, the order in which these sequences of things matters.

All the differing formats of RSS will be mapped onto this basic object model, and that is the object model that will be used in the application. The process of reading the XML from a file, or accessing it through an HTTP connection, will be performed by objects that implement the iProvider interface. At this stage, I have not covered XML or databases, so the initial development I describe in the rest of this chapter will be a simplified version, and the rest of the functionality will be added as we go in later chapters.

We will be using two basic classes: Group and Item. Group and Item both share the same parent class, Meme, and they inherit several common methods and properties (I use the term for my own personal amusement. If you would like to know why, just read the last chapter in Richard Dawkins' The Selfish Gene). A Group corresponds to a channel, or entry. An Item corresponds to an Item.

The Meme class is a subclass of the mwNode class. The mwNode class provides the methods and properties that allow it to act like a tree. When diagramming a tree, there are two basic nodes: a branch node and a leaf node. A branch node has one or more children, which are either branch nodes or leaf nodes. A leaf node has no children. Branch nodes correspond to Group objects and leaf nodes correspond to Item objects.

In this chapter, I will introduce the mwNode class. Groups and Items will be introduced in the next chapter, where I will demonstrate parsing XML documents and converting them into the objects model.

Class: mwNode

The is the root class for any document types that I use throughout the rest of the application.

Listing 5.14. mwNode Properties

Protected TypeID As integer Protected Position as Integer Protected Sequence as iSequence Protected Parent as mwNode Protected Container as mwNode Protected Expanded as boolean Protected Name As String Protected Depth As Integer

Listing 5.15. Sub mwNode.Constructor(aType as Integer, aName as String)

// A type with a positive number is a Leaf // A type with a negative number is a Branch Me.TypeID = aType Me.Name = aName Me.Expanded = False Me.Sequence = new NodeSequence(me) Me.Depth = 1

Listing 5.16. Sub mwNode.Release()

The mwNode class contains instances where the parent node has a reference to the Sequence object, and the Sequence object has a reference to the parent node. This kind of self-reference can cause problems with REALbasic's reference counting, leaving objects in memory long after they are no longer being used. This method clears those cross-references.

Parent = nil Container = nil Sequence = nil

Listing 5.17. Function mwNode.isLeaf() as Boolean

if TypeID > 0 then return true else return false end if

Listing 5.18. Function mwNode.isBranch() as Boolean

If TypeID < 0 Then Return true Else return false End If

Listing 5.19. Function mwNode.getTypeID() as Integer

Return me.TypeID

Listing 5.20. Sub mwNode.setTypeID(aTypeConstant as Integer)

Me.TypeID = aTypeConstant

Listing 5.21. Sub mwNode.setParent(aParent as mwNode)

If not(aParent is Nil) Then Me.Parent = aParent End If

Listing 5.22. Function mwNode.getParent() as mwNode

Return me.Parent

Listing 5.23. Sub mwNode.setPosition(pos as Integer)

Me.Position = pos

Listing 5.24. Function mwNode.getPosition() as Integer

Return me.Position

Listing 5.25. Sub mwNode.setContainer(aContainer as mwNode)

Me.Container = aContainer

Listing 5.26. Function mwNode.getContainer() as mwNode

Return Me.Container

Listing 5.27. Function mwNode.getPath() as String

// Path is a recursive function that gets // the node path to this particular node // The format returned will be 1.2.4, which // is the first node of the root node, the // first node's second child and that // nodes 4th child Dim cont_path as String If not(me.Container is nil) Then cont_path = me.Container.getPath If cont_path = "" Then cont_path = str(me.Position) Else cont_path = cont_path + "." + str(me.Position) End If Return cont_path Else Return "" End If

Listing 5.28. Function mwNode.getName() as String

Return me.Name

Listing 5.29. Sub mwNode.setName(aName as String)

Me.Name = aName

Listing 5.30. Function mwNode.isExpanded() as Boolean

Return me.Expanded

Listing 5.31. Sub mwNode.Expand()

Me.Expanded = True

Listing 5.32. Sub mwNode.Collapse()

Me.Expanded = False

Listing 5.33. Function mwNode.getVersion() as String

Return "mwNode-v1"

Interface: iSequence

The iSequence interface is a class used to manage the parent node's children. The mwNode class has a property Sequence that implements iSequence.

Sub Constructor(aNode as mwNode) End Sub Function getChildCount() As Integer End Function Function getChild(pos as Integer) As mwNode End Function Sub appendChild(aNode as mwNode) End Sub Sub insertChild(pos as Integer, aNode as mwNode) End Sub Sub removeChild(pos as Integer) End Sub Sub removeChild(aNode as mwNode) End Sub

Class NodeSequence (Implements iSequence)

Here is the actual implementation of iSequence that I will use in the examples. Note the use of a class BadNodeErrorthis will be discussed later in the chapter.

Property Node As mwNode Property Child(-1) As mwNode

Listing 5.34. Sub NodeSequence.Constructor(aNode as mwNode)

Dim bne as BadNodeError If Not aNode Is Nil Then me.Node = aNode Else bne = new BadNodeError bne.Message = "Error constructing node" Raise bne End If

Listing 5.35. Sub NodeSequence.Release()

Redim Child(-1) Node = Nil

Listing 5.36. Sub NodeSequence.appendChild(aNode as mwNode)

Child.Append(aNode) aNode.setPosition(ubound(Child) + 1) aNode.setParent(Node) aNode.Depth = Node.Depth + 1

Listing 5.37. Function NodeSequence.getChild(pos as Integer) as mwNode

If (pos > -1) and (pos < getChildCount) Then Return Child(pos) End If

Listing 5.38. Sub NodeSequence.insertChild(pos as Integer, aNode as mwNode)

Child.Insert(pos, aNode) aNode.setPosition(pos + 1) aNode.setParent(Node) aNode.Depth = Node.Depth + 1

Listing 5.39. Function NodeSequence.getChildCount() as Integer

Return ubound(Child) + 1

Listing 5.40. Sub NodeSequence.removeChild(pos as Integer)

Dim x,y,z as Integer y = getChildCount - 1 For x = pos + 1 to y z = Child(x).getPosition Child(x).setPosition(z - 1) Next Child.Remove(pos)

Listing 5.41. Sub NodeSequence.removeChild(aNode as mwNode)

Dim res as Integer res = Child.IndexOf(aNode) If res > -1 Then Child.Remove(res) End If

Views

Views are implemented with Windows and Controls. Because that is a primary topic of this chapter, the bulk of the rest of the chapter will be devoted to elucidating the views used in the RSSReader application.

Категории