TweetFollow Us on Twitter

In Praise of 4GLs

Volume Number: 20 (2004)
Issue Number: 1
Column Tag: Programming

Rapid Development

by Richard Gaskin

In Praise of 4GLs

Making a custom Internet application in less than a day

Introduction

Fourth-generation languages (4GLs) have grown up. Once limited to narrow tasks like database queries, modern 4Gls, like Revolution and SuperCard, offer rich languages and object models suitable for complete GUI application development.

Using a 4GL can be a smart choice when developer productivity is a project's critical driver. For example, many vertical market applications are too specialized to have a broad enough audience to support development in lower-level languages like C++ or Java, but a 4GL can often get the job done in a fraction of the time.

The tradeoff for this ease of development is sometimes execution speed, as the interpretation of scripts at runtime involve steps done at compile-time in lower-level languages. However, with well-optimized 4Gls, like Revolution, this is not always the case, and for many common tasks Revolution measurably outperforms Java, and other lower-level languages.

To better appreciate the runtime efficiency of a modern 4GL like Revolution, consider what's happening under the hood. In Revolution, when a script is loaded, it's interpreted into bytecode in a form reported to be more efficient than Java's. When executing a script, this bytecode acts as a glue, connecting compiled routines in the engine written in C++. So, while a script is indeed interpreted, a single line of script often triggers the execution of hundreds of lines of compiled, optimized C++.

To illustrate the productivity gain, consider the task of creating an alias to a file. In C it takes about 18 lines, and in a Java example posted to an Apple discussion list it took 247 lines. In Revolution's scripting language, Transcript, it's a one-liner:

create alias MyAliasPath to file MySourcePath

With hundreds of commands, functions, and object properties in Transcript, things like opening windows, handling controls, reading files, and other common tasks are usually one-liners, which means that most of the code being executed is natively compiled C++. In essence, the Transcript engine acts like a precompiled object library you string together with a few lines of script.

Productivity in Action

Let's take a look at how these productivity benefits play out in a real-word example: building a custom FTP client in a few hours.

The need for this arose from a teleconference with a client. We were starting a project in which we'd need to trade a lot of files back and forth. The number of files made email a cumbersome option, and the size of some of them made using email prohibitive.

Once we decided to use FTP we reviewed the various FTP clients available, including my personal favorite, Interarchy. While the applications we looked at were all great general solutions for file transfer, the breadth of features they offered was problematic in our case, given that files would sometimes need to be transferred by people with little or no experience using FTP tools. Also, the client's office had a mix of machines running OS X and Windows, so we wanted something that was not merely easy to use, but also had the same simple interface for both platforms.

I offered to make a custom FTP tool with the goal of having the fewest features possible. It would be hard-wired to work with a single directory on our server, require nothing more than a password to log in, and work identically on both OS X and XP.

When I told him I'd have it ready by the end of the day he was surprised. What he didn't know was that the secret weapon making this possible was Revolution's Internet library, libURL.

libURL: The Heart of our application

Tim Monroe's excellent series of MacTech articles on developing QuickTime applications with Revolution provides a great introduction to the development environment and scripting language, so here we'll focus on working with libURL.

The Revolution engine contains robust support for TCP and UDP sockets, but if you've ever written an FTP client, you know how difficult it can be to handle all the error-checking needed to do it right. So, along with the other scripted libraries included with Revolution is libURL, which provides a simple interface for handling HTTP and FTP transactions.

LibURL uses Transcript's simple put command for the most common tasks. Just as you would display text in a field with put "Hello World" into field 1, you can specify a URL as a container as well.

To download a file from a Web server and display its contents in a field:

put url "http://www.fourthworld.com/data.txt" into field 1

Uploading a file to an FTP server is just as easy:

put tMyData into url \
  "ftp://user:password@ftp.domain.com/tDataFile.dat"

LibUrl also provides more than a dozen other commands to allow asynchronous transfer, logging, status updates, and more.

With libURL and Revolution's simple tools for building interfaces, putting the application together was a snap.

Development Chronology

8:30 AM Defined the requirements with the client.

9:00 AM The simple interface needed only a few objects, including a list field to display remote files, a Download button, Upload button, and of course a button to initiate the connection. Figure 1 shows Revolution's Tool palette for creating objects, and its Inspector palette for setting object properties.


Figure 1. Building the Interface

Figure 2 shows a profile of our objects in Revolution's Application Browser after adding a menu bar and a hidden progress bar which we'll show during file transfers.


Figure 2. Layout Overview

Once the client interface was done, I needed to build an administration window for my own use during development, so I could enter the FTP login information. I could have written those settings directly into the code, but taking a moment to make an interface for myself will make it easier to modify the application for other projects in the future (Figure 3).


Figure 3. Admin window

I also added a simple window containing only a field to display a log of the transactions with the server to help with debugging. This exploited one of the conveniences of libURL: it includes a simple command that lets you use any field object as a log. You just pass it any valid field reference and all TCP transaction logging is automatically appended to the field's contents as it goes:

libUrlSetLogField the long ID of fld "log" \
  of stack "4wFtpLog"

The graphics in the main window were just modified versions of images from our Web site, so the interface was completed in about an hour.

10:00 AM With the interface objects in place it was time to start coding. The first thing the program needed to do, of course, was display a list of files from the remote server. As shown above we can download a file by just using the get url command, and we can download a directory listing by just specifying any valid directory on the server with the trailing slash:

  get url "ftp://user:password@ftp.domain.com/directory/"

This returns a raw listing of files in a standard directory format:

drwxr-xr-x   2 user2000 user2000      512 Oct  1 01:46 .
drwxr-xr-x   4 user2000 user2000      512 Sep 30 08:44 ..
-rw-r--r--   1 user2000 user2000   898089 Oct  1 01:48 Tutorial1.pdf
-rw-r--r--   1 user2000 user2000  1118961 Oct  1 01:46 Tutorial2.pdf
-rw-r--r--   1 user2000 user2000   557660 Oct  1 01:44 Tutorial3.pdf
-rw-r--r--   1 user2000 user2000    88486 Oct  1 01:41 workflow2.png

If order to display the file list in a simpler form for the user, the first function I wrote converted the raw data into a tab-delimited list, since Revolution fields can display tab-delimited data in a field object as a multi-column list automatically.

Converting this raw data introduced one of the strengths unique to Revolution and other 4GLs inspired by HyperTalk, affectionately called chunk expressions. These HyperTalk dialects support fast and simple text parsing with references like word (space-delimited), item (comma-delimited), and line (return-delimited). In Transcript all three of these chunk types can use custom delimiters so they can be applied to a wide variety of tasks. Similar routines in other languages usually require walking through blocks of text one character at a time, counting delimiters, and building arrays as you go. In Transcript, however, you can simply write things like:

get word 2 of item 3 of line 4

Here's the code for the reformatting function:

--
-- FormatRemoteFileList
--
-- Extracts the relevant into from each line in the
-- FTP file list passed in pList and returns a tab-
-- delimited list of just file name, size, and date
-- for display in the file list control
--
function FormatRemoteFileList pList
   put empty into tFormattedList
   repeat for each line tFile in pList
      -- Skip folders in this version:
      if char 1 of tFile = "d" then next repeat
      -- Get file name:
      put word 9 to (the number of words of tFile) \
        of tFile into tName
      -- Skip invisible files:
      if char 1 of tName = "." then next repeat
      --
      put Bytes2Size(word 5 of tFile) into tSize
      put word 6 to 8 of tFile into tDate
      put tName &tab& tSize &tab & tDate \
       &cr after tFormattedList
   end repeat
   delete last char of tFormattedList
   return tFormattedList
end FormatRemoteFileList

The Bytes2Size function simply converts bytes into a common abbreviation appropriate for user display, for example "323455" is returned as "3.2Mb":

function Bytes2Size n, pPadFlag
   if pPadFlag is empty then set the numberformat to "0.#"
   else set the numberformat to "0.0"
   --
   if n < 1024 then put n &" bytes" into n
   else
      put n / 1024 into n
      if n < 1024 then put n &"k" into n
      else
         put n / 1024 &"Mb" into n
      end if
   end if
   return n
end Bytes2Size

Since most of the controls would affect the user interface, I wrote one handler to handle all interface updates, called from most of the other UI-related code. I wanted to keep things simple, so I had it pass just one parameter to indicate if the user was connected to the server so it would refresh the file list. If called with no arguments, it cleared the list field:

--
-- UpdateConnectionUI
--
-- One-stop shopping for updating the user interface
-- whenever the user connects or disconnects
--
on UpdateConnectionUI pConnectedFlag
   libUrlSetStatusCallback
   put (pConnectedFlag = "connected") into tIsConnected
   set the enabled of grp "server" to tIsConnected
   hide scrollbar "progress"
   if tIsConnected is true then
      set the label of btn "Connect" to "Disconnect"
      --
      put ServerDirectoryUrl() into tUrl
      put url tUrl into tFileList
      if (the result is not empty) and \
       ("not completed" is not in the result) then
         answer the result
         UpdateConnectionUI
         exit to top
      end if
      put FormatRemoteFileList(tFileList) into fld "files"
      PutStatus "Connected"
      --
   else
      set the label of btn "Connect" to empty
      put empty into fld "files"
      PutStatus "Not connected"
   end if
   disable btn "Download..."
end UpdateConnectionUI

That handler called a couple of other simple handlers written as a convenience:

--
-- ServerDirectoryUrl
--
-- Returns the full URL to the server directory
-- by concatenating info stored in the hidden
-- Admin window
--
function  ServerDirectoryUrl
   return "ftp://"& \
        fld "login"     of stack "4wFtpAdmin" &":"& \
        fld "password"  of stack "4wFtpAdmin" &"@"& \
        fld "server"    of stack "4wFtpAdmin" &"/"& \
        fld "directory" of stack "4wFtpAdmin" &"/"
end ServerDirectoryUrl

--
-- PutStatus
--
-- Displays the string in s in the "Status" field
--
on PutStatus s
   put s into field "status" of stack "4wFtp"
end PutStatus

With the basics in place it was time to put them to work by adding code to handle the Connect button. To keep the interface as simple as possible, I used one button object for both connecting and disconnecting, merely changing the button's label property to reflect the change to the state. This is from the Connect button's script:

on mouseUp
   if the label of me = "Disconnect" then 
     DisconnectFromServer
   else ConnectToServer
end mouseUp

ConnectToServer and DisconnectFromServer are defined in the script of the main window, which Revolution refers to as a "stack":

--
-- ConnectToServer
--
-- Called from the Connect/Disconnect button
-- to log on to the FTP server and obtain a
-- list of files
--
on ConnectToServer
   put fld "Login" into tLogin
   if tLogin <> fld "login" of stack "4wFtpAdmin" then
      answer "Incorrect login"
      exit to top
   end if
   --
   set cursor to watch
   put fld "login" into tLogin
   ask password clear "Enter your password:" as sheet
   if it is empty then exit to top
   put it into tPassword
   if tPassword <> fld "password" of stack \
    "4wFtpAdmin" then
      answer "Wrong password for your site."
      exit to top
   end if
   --
   if "4wFtpLog" is in the windows then
      libUrlSetLogField the long ID of \
       fld "log" of stack "4wFtpLog"
   end if
   --
   UpdateConnectionUI "connected"
end ConnectToServer

--
-- DisconnectFromServer
--
-- Called from the Connect/Disconnect button to
-- clear the file list and update the interface
--
on DisconnectFromServer
   UpdateConnectionUI
end DisconnectFromServer

Most of the code is fairly self-explanatory with the background provided earlier, but it's worth calling your attention to the ask and answer commands. These Transcript commands provide one-line convenience for displaying a simple alert dialog with answer, or allowing user input with ask, returning values in a predefined local variable named it. Extensions to these commands also provide a script interface to the OS's GetFile and PutFile dialogs on each of the supported platforms, which will be used later when we script the Download and Upload buttons.

I tested the work done thus far, clicking the Connect button to see the remote file list displayed and the button relabeled to Disconnect. I clicked it again to clear the interface. So far, so good. Time for lunch.

12 Noon With things going so well I packed a picnic and took it to the deck on the roof of my office building to enjoy a long lunch in the California sunshine.

1:30 PM Returning to the office, the next step was to get the Upload and Download buttons working. Both of these make extensive use of a great feature of libURL, the libUrlSetStatusCallback command.

As we covered earlier, Transcript's get and put commands can be used to conveniently use URLs as containers. While these are very convenient, they are synchronous, suspending other script execution such as one might need for updating a progress bar, for example.

Fortunately, libURL provides commands for asynchronous data transfer, as well. For FTP, these commands include libUrlDownloadToFile and libUrlFtpUploadFile.

But, to get the most out of asynchronous execution, you'll want to be notified of the status of the transaction so you can take appropriate steps, such as updating a progress bar, and notifying the user when the transfer is completed. You set this up with libURL using the libUrlSetStatusCallback command, specifying the message name you want sent and the object it should be sent to:

libUrlSetStatusCallback message, object

While a transfer is in progress, libURL will then send that message to the object along with an argument containing the URL the callback is for, and another containing a string which describes the current status. The status argument will consist of one of the following:

queued: on hold until a previous request to the same site is completed
contacted: the site has been contacted but no data has been sent or received yet
requested: the URL has been requested
loading bytesTotal,bytesReceived: the URL data is being received
uploading bytesTotal,bytesReceived: the file is being uploaded to the URL

downloaded: the application has finished downloading the URL
uploaded: the application has finished uploading the file to the URL
error: an error occurred and the URL was not transferred
timeout: the application timed out when attempting to transfer the URL
(empty): the URL was not loaded, or has been unloaded

Since so much of the user interface that needs to be updated during a transfer is common to both uploading and downloading, I wrote one handler to handle status callbacks for both. Using a switch block to handle each type of status message appropriately, this handler was effectively the core of the application:

--
-- UpdateStatus
--
-- Callback from libURL providing status info
-- and manages each stage of the transaction
-- based on the current status
--
on UpdateStatus pUrl, pStatus
   set the itemdel to "/"
   put last item of pUrl into tFileName
   set the itemdel to comma
   put empty into tStatusDisplayString
   --
   switch item 1 of pStatus
      -- Handle progress:
   case "uploading"
      put "Uploading" into tStatusDisplayString
   case "loading"
      -- Update progress bar:
      put item 2 of pStatus into tBytesReceived
      put item 3 of pStatus into tTotalBytes
      set the endvalue of scrollbar "progress" \
        to tTotalBytes
      set the thumbpos of scrollbar "progress" \
        to tBytesReceived
      show scrollbar "progress"
      --
      if tStatusDisplayString is empty then 
        put "Downloading" into tStatusDisplayString
      end if
      put cr& tFileName &cr& \
         Bytes2Size(tBytesReceived, "pad") & \
         " of "& Bytes2Size(tTotalBytes, "pad") \
         after tStatusDisplayString
      PutStatus tStatusDisplayString, "right"
      break
      --
      -- Errors:
   case "error"
   case "timeout"
      answer pStatus
      exit to top
      break
      --
      -- Completed successfully:
   case "uploaded"
      UpdateConnectionUI "connected"
   case "downloaded"
      hide scrollbar "progress"
      PutStatus "Done"
      enable btn "Download..."
      enable btn "Upload..."
      unload pUrl
      break
   end switch
end UpdateStatus

I added scripts to the Upload and Download buttons that simply call these handlers to initiate each respective transfer:

--
-- UploadFile
--
-- Called from the Upload button to select a local
-- file and start uploading it to the server
--
on UploadFile
   answer file "Select a file to upload:"
   if it is empty then exit to top
   put it into tSource
   --
   set the itemdel to "/"
   put ServerDirectoryUrl()&last item of tSource into tDest
   --
   libUrlSetStatusCallback "UpdateStatus", long name of me
   libUrlFtpUploadFile tSource, tDest, "UpdateStatus"
   disable btn "Download..."
   disable btn "Upload..."
end UploadFile

--
-- DownloadFile
--
-- Called from the Download button to start downloading
-- the selected file
--
on DownloadFile pFile
   ask file "Save this file to:" with pFile
   if it is empty then exit to top
   put it into tDest
   --
   put ServerDirectoryUrl()&pFile into tSource
   --
   libUrlSetStatusCallback "UpdateStatus", long name of me
   libUrlDownloadToFile tSource, tDest, "UpdateStatus"
   disable btn "Download..."
   disable btn "Upload..."
end DownloadFile

I had a little extra time so I added a useful flourish: I placed an animated GIF version of the flag over the image at the top of the window, and modified the UpdateConnectionUI handler to show the GIF when connected and hide it when disconnected. It was a small touch, but the animated element helped visually reinforce when the connection was "live".

After a little debugging to address a few typos, it seemed to be working well, so now it was time to build and test the standalone application.

3:30PM One of Revolution's greatest strengths is its broad support for deploying on multiple operating systems. With engines available for Mac Classic, OS X, all Win32 systems, and most flavors of UNIX and Linux it covers nearly every modern desktop system.

Standalones are built with Revolution's Distribution Builder (Figure 4), a utility accessed from the File menu, that lets you assign the application's file name and version info, assign icons, etc.. Once you set it up, you can save the settings for future use.


Figure 4. Distribution Builder

You just click the Build Distribution button and it does the rest, embedding the appropriate engine into a copy of your stack file to create the standalone. For OS X it constructs the bundle and writes the info.plist file for you as well.

Figure 5 shows the completed application in action.


Figure 5. Final application

4:00 PM I compressed the application with SuffIt and emailed it to the client. He ran a few tests and was delighted with its simplicity. I left work early.

Conclusion

4GLs may not be the best choice for every job, especially computationally intensive tasks like writing device drivers or rendering 3D. But, when time-to-market is a concern a good 4GL like Revolution can be hard to beat. How many other systems let you build a custom FTP tool in under 200 lines of code?

If you want to check out the source for yourself you can download it from:

http://www.fourthworld.com/mactech/

Since the application requires your server info you'll need Revolution to set up the Admin window and build the application. You can download the free trial version of Revolution from Runtime Revolution Ltd.'s site:

http://www.runrev.com/


Richard Gaskin is president of Fourth World Media Corporation, a Los Angeles-based consultancy specializing in multi-platform software development. With 15 years' experience, Richard has delivered dozens of applications for small businesses and Fortune 500 companies on Mac OS, Windows, UNIX, and the World Wide Web. http://www.fourthworld.com

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Bartender 3.0.32 - Organize your menu-ba...
Bartender lets you organize your menu-bar apps by hiding them, rearranging them, or moving them to Bartender's Bar. You can display the full menu bar, set options to have menu-bar items show in the... Read more
Adobe Animate CC 2018 18.0.1.115 - Anima...
Animate CC 2018 is available as part of Adobe Creative Cloud for as little as $19.99/month (or $9.99/month if you're a previous Flash Professional customer). Animate CC 2018 (was Flash CC) lets you... Read more
Adobe Lightroom Classic CC 7.1 - Import,...
Adobe Lightroom is available as part of Adobe Creative Cloud for as little as $9.99/month bundled with Photoshop CC as part of the photography package. Lightroom 6 is also available for purchase as a... Read more
ExpanDrive 6.1.8 - Access cloud storage...
ExpanDrive builds cloud storage in every application, acts just like a USB drive plugged into your Mac. With ExpanDrive, you can securely access any remote file server directly from the Finder or... Read more
ExpanDrive 6.1.8 - Access cloud storage...
ExpanDrive builds cloud storage in every application, acts just like a USB drive plugged into your Mac. With ExpanDrive, you can securely access any remote file server directly from the Finder or... Read more
DiskCatalogMaker 7.2.7 - Catalog your di...
DiskCatalogMaker is a simple disk management tool which catalogs disks. Simple, light-weight, and fast Finder-like intuitive look and feel Super-fast search algorithm Can compress catalog data for... Read more
DiskCatalogMaker 7.2.7 - Catalog your di...
DiskCatalogMaker is a simple disk management tool which catalogs disks. Simple, light-weight, and fast Finder-like intuitive look and feel Super-fast search algorithm Can compress catalog data for... Read more
Iridient Developer 3.2.1 - Powerful imag...
Iridient Developer (was RAW Developer) is a powerful image-conversion application designed specifically for OS X. Iridient Developer gives advanced photographers total control over every aspect of... Read more
BusyContacts 1.2.7 - Fast, efficient con...
BusyContacts is a contact manager for OS X that makes creating, finding, and managing contacts faster and more efficient. It brings to contact management the same power, flexibility, and sharing... Read more
MegaSeg 6.0.5 - Professional DJ and radi...
MegaSeg is a complete solution for pro audio/video DJ mixing, radio automation, and music scheduling with rock-solid performance and an easy-to-use design. Mix with visual waveforms and Magic... Read more

Latest Forum Discussions

See All

The best deals on the App Store this wee...
A new week means new discounts on the App Store. This week's deals run the gamut of action-adventure titles, puzzle games, and one of the best narrative adventure series out there. If you're looking to fill out your mobile gaming library on a... | Read more »
What you need to know about Animal Cross...
We hope you've been hard at work on collecting all of those holiday items in Animal Crossing: Pocket Camp, because you're about to get a whole new list of fun things to do as the game receives its first big update sometime soon. There are a lot of... | Read more »
Reigns: Her Majesty guide - how to use e...
Ruling a kingdom isn't easy--doubly so for a queen whose every decision is questioned by the other factions seeking a slice of power. Reigns: Her Majesty builds on the original game's swipey tactics, adding items that you can use to move the story... | Read more »
The best new games we played this week -...
Friday has crept up on us once again, so it's time to honor the best new games we've played over the past few days. This past week was a pretty exciting one, with the debut of lots of beautiful new indies and some familiar faces returning to the... | Read more »
Portal Knights guide- beginner tips and...
Portal Knights is finally making the jump to iOS and Android, and it's already climbing the ranks to become the next big MMO experience on mobile. This sprawling sandbox game will let you pursue any adventure you wish, whether you want to sling... | Read more »
Reigns: Her Majesty guide - how to swipe...
Reigns: Her Majesty is storming the App Store this week, bringing more tinder-esque kingdom building to eager players everywhere. If you've played the original Reigns, you'll know that leading a kingdom is never easy. It's a careful balancing act... | Read more »
Getting Over It (Games)
Getting Over It 1.0 Device: iOS Universal Category: Games Price: $4.99, Version: 1.0 (iTunes) Description: A game I madeFor a certain kind of person To hurt them. • Climb up an enormous mountain with nothing but a hammer and a pot.•... | Read more »
Reigns: Her Majesty (Games)
Reigns: Her Majesty 1.0 Device: iOS Universal Category: Games Price: $2.99, Version: 1.0 (iTunes) Description: | Read more »
Pocket Legends Adventures guide - how to...
Pocket Legends Adventures is a fun action adventure RPG that takes control when you want it to, but also opens itself for player input, too, if you're looking to tkae a more active role in combat. Regardless of play style, the game can be quite... | Read more »
Portal Knights (Games)
Portal Knights 1.2.4 Device: iOS Universal Category: Games Price: $4.99, Version: 1.2.4 (iTunes) Description: Craft your adventure. Forge your hero. Become the ultimate Portal Knight! | Read more »

Price Scanner via MacPrices.net

Apple Watch Series 2, Certified Refurbished,...
Apple has Certified Refurbished Apple Watch Nike+ Series 2s, 42mm Space Gray Aluminum Case with Anthracite/Black Nike Sport Bands, available for $249 (38mm) or $279 (42mm). The 38mm model was out of... Read more
Apple offers Certified Refurbished 2016 12″ R...
Apple has Certified Refurbished 2016 12″ Retina MacBooks available starting at $949. Apple will include a standard one-year warranty with each MacBook, and shipping is free. The following... Read more
B&H drops price on 13″ 256GB MacBook Air...
B&H has the 13″ 1.8GHz/256GB Apple MacBook Air (MQD42LL/A) now on sale for $1079 including free shipping plus NY & NJ sales tax only. Their price is $120 off MSRP, and it’s the lowest price... Read more
Holiday sale: 9″ iPads starting at $299, take...
MacMall has 9″ WiFi iPads on sale for $30 off including free shipping: – 9″ 32GB WiFi iPad: $299 – 9″ 128GB WiFi iPad: $399 Read more
Green Monday deal: 15″ 2.8GHz MacBook Pro on...
B&H Photo has the 15″ 2.8GHz Space Gray MacBook Pro on sale for $250 off MSRP for today only as part of their Green Monday/Holiday sale. Shipping is free, and B&H charges sales tax for NY... Read more
Green Monday sale: B&H offers 12″ Apple i...
B&H Photo has 12″ iPad Pros on sale for up to $150 off MSRP as part of their Green Monday/Holiday sale. Shipping is free, and B&H charges sales tax in NY & NJ only: – 12″ 64GB WiFi iPad... Read more
Holiday deal: 21″ and 27″ Apple iMacs on sale...
MacMall has 2017 21″ and 27″ Apple iMacs on sale for up to $200 off MSRP. Shipping is free: – 21″ 2.3GHz iMac: $999 $100 off MSRP – 21″ 3.0GHz iMac: $1199 $100 off MSRP – 21″ 3.4GHz iMac: $1379 $120... Read more
Holiday deal: Apple Mac minis for up to $150...
MacMall has Mac minis on sale for up to $100 off MSRP, each including free shipping: – 1.4GHz Mac mini: $399 $100 off MSRP – 2.6GHz Mac mini: $599 $100 off MSRP – 2.8GHz Mac mini: $949 $50 off MSRP... Read more
Beats by Dr. Dre – BeatsX Earphones on sale f...
Best Buy has BeatsX Earphones on sale for $109, $40 off, on their online store. Sale price for online orders only. Choose free store pickup, if available, or choose free shipping. Read more
10″ 64GB WiFi Apple iPad Pros on sale for $59...
MacMall has 10.5″ 64GB Apple iPad Pros on sale for $599 including free shipping. That’s $50 off MSRP and among the lowest prices available for these iPads from any Apple reseller. Read more

Jobs Board

*Apple* Retail - Multiple Positions - Apple,...
Job Description:SalesSpecialist - Retail Customer Service and SalesTransform Apple Store visitors into loyal Apple customers. When customers enter the store, Read more
*Apple* Retail - Multiple Positions - Apple,...
Job Description: Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, Read more
*Apple* Retail - Multiple Positions - Apple,...
Job Description:SalesSpecialist - Retail Customer Service and SalesTransform Apple Store visitors into loyal Apple customers. When customers enter the store, Read more
*Apple* Information Security - Security Data...
# Apple Information Security - Security Data Analyst Job Number: 113119545 Austin, Texas, United States Posted: 10-Nov-2017 Weekly Hours: 40.00 **Job Summary** This Read more
*Apple* Retail - Multiple Positions - Apple,...
Job Description: Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.