TweetFollow Us on Twitter

Nov 01 Adv WebObjects

Volume Number: 17 (2001)
Issue Number: 11
Column Tag: Advanced WebObjects

Part 3 - Cool Programming Tricks

by Emmanuel Proulx

Neat things you can do with WebObjects.

Redirecting

You already know that, in order to jump from a Web Component to another one, the syntax is simply:

WOComponent anAction() {
  return pageWithName("ComponentName");
}

But what if the page you want to jump to is external to the application? One way of statically jumping to an external page is to hardcode the URL in a static hyperlink on the page. But sometimes you need to process the request before you jump to that external page. How can you do that?

WebObjects has a utility class called WORedirect, which produces a "redirection page". This page will have the browser jump to that external URL. You can invoke WORedirect it by using the following syntax:

WOComponent anExternalAction() {
  //Compute this request...
  WORedirect r = (WORedirect)pageWithName("WORedirect");
  r.setURL("/phonypage?param=" 
           + resultOfRequest);
  return r;
}

Controlling Backtracking (Back Button)

Backtracking (clicking on the "back" button of the browser) can cause big problems, in all Web applications. The problem may arise when:

  • the user is in a first page, the system being in a certain state A
  • the user clicks on something to go to the second page, setting the system to a new state B
  • the user clicks on "back" to see the first page, thinking the state of the system is A again - but the system is still in state B! Any number of things can go wrong now.

As an example, the user might choose to see the records for Monday (state A), then click on a hyperlink marked "Tuesday". The page reloads with the new set of records for Tuesday (state B). The user then backtracks to the previous page, and decides to click button "clean" that deletes all records for the current day. The user thinks he just erased the records for Monday, but in fact he/she erased the records for Tuesday.

This can be a big problem. It is well known by the Web developers, that handle the problem in specific ways:

You can fix the problem by forcing the previous pages to reload every time the user accesses them. You do that by calling the function setPageRefreshOnBacktrackEnabled() from the Application object's constructor. This is implemented in WebObjects by setting an "already expired" expiry date of all produced pages. This solution is pretty good, but not sufficient, because WebObjects caches the pages that are produced. When a user uses the back button to an expired page, WebObjects returns the pages as it was generated previously.

You can also turn off page caching, by calling another function from the Application object: setPageCacheSize(). How does this work? By default, WebObjects keeps the 30 last pages in memory, and returns them to the browsers that ask for them. Setting the cache size to 1 prevents the user from receiving cached pages (except the current one).

Here's an example:

public Application() {
  setPageRefreshOnBacktrackEnabled(true);
  setPageCacheSize(1); //keep only the current page
  //...
}

You can also prevent the user from re-accessing your application after leaving it. The user can still browser back and forth in the system's pages, but not after exiting. This usually involves an "Exit" or "logoff" button or hyperlink, linked to an action like:

public WOComponent exitAction() { 
  WORedirect r = (WORedirect)pageWithName("WORedirect");
  r.setURL("http://www.mactech.com");

  session().terminate();

  return r;
}

You still need to call setPageRefreshOnBacktrackEnabled(true) in the Application object's constructor to force page reload, or else the user will be able to see the old defunct pages that have no associated session, instead of an honest error message saying "session timed out".

Web Components Communication

There's a number of ways pages of the same system can communicate among each other. Let's overview the different ways.

Global Variables

The different pages can share global information by setting variables inside the Application or Session objects.

Imagine a "Login" page and a "User Information" page. These would share a piece of data called the "userID". You would make an instance variable called "userID" in the Session object, like this:

public class Session extends WOSession {
  //...
  protected String userID;
  public String userID() { return userID; }
  public void setUserID(String newID) { userID = newID; }
  //...
}

Then you would set the userID in an action in the Login page:

protected String userIDField;

public WOComponent doLogin() {
  //Verify username and password
  Session s = (Session)session();
  s.setUserID(userIDField);

  UserInformation ui = (UserInformation)pageWithName("UserInformation");
  return ui;

}

Finally, you would use this information from somewhere within the User Information page:

public UserInformation () { //constructor
  Session s = (Session)session();
  actUponUser(s.userID());
}

This is simple, but can lead to potential problems. All modules from within the system have access to the global variables, and may change them, yielding possibly bad results. This may or may not be appropriate - it's up to you to decide.

Local Variables

You can send information to another page by setting a local variable instead of a global one. Let's use the same example. This time there's no Session or Application involved; the communication would happen directly. You would send the userID to the User Information page like this:

protected String username;

public WOComponent doLogin() {
  //Verify username and password

  UserInformation ui = (UserInformation)
    pageWithName("UserInformation");
  ui.setUserID(username);
  return ui;
}

You need a variable to receive the userID in the User Information page, in order to use it:

protected String userID;
public String userID() { return userID; }
public void setUserID(String newID) { userID = newID; }

public UserInformation () { //constructor
  actUponUser(userID());
}

Interface Variables

Custom Components are meant to be "black boxes"; you can use them but you don't know how they are built from the inside. In theory, you could buy libraries of components, and you don't have to have the source code in order to use them. But sometimes you need to communicate with them to let them know how they should behave. How can you do that without opening the .java file and looking at the source code? To work around this problem, we use an "interface"; a description of their publicly available members. The interface shows up in the WebObjects Builder while editing the parent component, by selecting the sub-component and opening the Inspector.

NOTE: Go to any project folder and take a look at the available files. Notice these files with the extension ".api". These files hold the interfaces for your Web Components.

The tool used for managing the interface is the API Editor. It is available in the WebObjects Builder, by clicking on the toolbar button .

Open any component and click on this button. This dialog is API Editor window:

Here's the list of things you can do in this window:

You can expose your component's variables. You do that by clicking on the button in the upper-right corner, and choosing "Add binding". You then enter the name of your variable. For each variable, you can then specify restrictions (a failure to obey these restrictions in the parent component will trigger an error message when displaying the component):

  • Required: is it mandatory to link this attribute to a value?
  • Will Set: must the attribute be linked to a variable of the parent component? (As opposed to a constant value.)
  • Value Set: specifies the type of data that can be sent to the variable. The choices are shown here:

The button "Add Keys From Class" lets you share all variables of the current component at once. This is a big time saver. I usually click on this first, and then remove the unwanted variables.

The Validation tab lets you describe a "validation scheme", a more complex restriction strategy than the one in the Bindings tab. If the user of the component fails to link the attributes to values that follow the validation condition, an error will be returned when displaying the component.

The Display tab lets you specify a documentation file (an HTML file that will show up when clicking on the help button in the Inspector), and an icon file that represents your component (shows up in the parent components instead of the default icon, e.g. ).

Interface Variable Synchronization

In WebObjects, you have control over the way values are stored in the attributes of elements (or sub-components).

The attribute's connection (also called binding) is a two-way street; if you set an element attribute in the parent component, the destination variable is automatically updated with the new value. If you set the binded variable in the element or sub-component, the parent's source variable is also changed (assuming it's not a constant). The "synchronization" between the parent and child variable happens at six specific times during the Request/Response loop, in the active WOComponent instance:

Before the generation, right before and right after calling takeValuesFromRequest()

During the action processing, right before and right after calling invokeActionFromRequest()

After the generation, right before and right after calling appendToResponse()

This process ensures that both the parent component and the element/sub-component have harmonized values at all times. It happens automatically, and relies on the existence of standard getter and setter functions in the classes of the variables. Casting is also done automatically as needed.

In the case of Custom Components, sometimes you don't want the variables to be in harmony. Sometimes, you don't even have a destination variable in the sub-component. You can turn off the synchronization of the variables and handle it by hand. You do that by overloading the Custom Component's synchronizesVariablesWithBindings() function. Then, you need to implement the variable synchronization yourself. Two functions help you do that: valueForBinding() to get the parent component's variable, and setValueForBinding() to set it to a new value. In the following example, we read in a variable from the parent, process it, and return the result in another variable:

import java.lang.Math;

public class MortgageCalculator extends WOComponent {
  //...

  public boolean synchronizesVariablesWithBindings() { 
    //Don't synchronize the parent's variables -
    //we'll do that ourselves
    return false;
    }

  public void awake() {
    //the parent thinks these variables exist:
    Double p = (Double)valueForBinding("principal");
    Double y = (Double)valueForBinding("years");
    Double i = (Double)valueForBinding("interest"); 
    if( (p==null) || (y==null) || (i==null) ) return;
    double r = i.doubleValue() / 1200;
    double cr = p.doubleValue() * r;
    double a = Math.pow(1.0+r, 12.0 * y.doubleValue());
    double b = a - 1.0;
    
    //put result back in another binding
    setValueForBinding(new Double(cr * a/b) , "payments"); 
  }

  //...
}

Getting and Sending Data to External Pages

You can't send values to external pages with global or local variables. We have to use the "standard" HTTP ways...

URL parameters

You can send data to a page by passing it URL parameters. The syntax is:
www.mactech.com/path/phonypage?param1=value1¶m2=value2...

The values have to be properly encoded (spaces replaced by plus signs (+), etc.).

TRICK: you can translate each value to comply to the URL encoding by using the function

String encodedValue = java.net.URLEncoder.encode("value to encode");
provided by Java. The result of the above line of code would be "value+to+encode". Don't encode the whole URL, just the values.

Note that URL parameters are automatically decoded and transformed into fields on reception...

Processing fields

You can receive form fields (including URL parameters) from an external page by using these WORequest functions, in the overloaded takeValuesFromRequest() of your component:

Methods Description
NSDictionary formValues() Returns key/value
pairs for all form
values.
NSArray formValueKeys() Returns the list of all
form field names
(NSArray of String).
These are used as
input for the next
methods.
String formValueForKey(String keyName) Returns the value for
the specified field
name
NSArray formValuesForKey(String keyName) Ditto. Use when
passed a list of
values.
(NSArray of String)

In the following example, we'll receive the username and password, and pre-populate the corresponding fields:

String password = ""; //connected to a "password" form field
String username = ""; //connected to a "username" form field

public void takeValuesFromRequest(WORequest r, WOContext c) {
  //Method #1
  String u = r.formValueForKey("username");
  if( (u != null) && !u.equals("") ) //not empty, assign it
  username = u; //pre-populate the username field on this login page

  //Method #2
  NSDictionary d = r.formValues(); //get all fields
  String p = d.objectForKey("password"); //extract field password
  if( (p != null) && !p.equals("") ) //not empty, assign it
    password = p; //pre-populate password field on this login page
}

Posting fields

The other way around, you can post form data to an external page, if you know how. Usually, the user navigates to an external page by clicking on a button, an image button or a hyperlink. Posting data is necessarily done by not pointing back to an action on the server, but by going directly to that external location. Note that this solution is equivalent to sending data as URL parameters. Here are some points to consider while implementing this solution:'

When using a hyperlink, submit button or image button, use a static one. You can turn a dynamic element into a static one by opening the Inspector and clicking on the button "Make Static". If you're planning to use a hyperlink, make it execute the submission of the form, like this:
<A HREF=”” onClick=”document.formName.submit(); return true;”>

Click here to submit

You should also use a static form (use "Make Static" button), and in the HTML , add some code to go to the external site. <FORM method=post ACTION=”www.othersite.com/cgi-bin/page.pl” NAME=formName>

The fields you use can be dynamic, but they don't have to be visible: you can use "hidden" fields to hide the submission of the form from the eyes of the user. This is commonly used to make it harder for the user to see data that he/she is not allowed to view. To make a hidden field, insert a custom component in your form , and in the Inspector set its class to WOHiddenField. Don't forget to set the "value" attribute.

Post-processing field posting

You might be interested in executing an action on the server before posting the form data to an external page. I have yet to see a simple way to do that. The most effective way I have found is to use a component with a form that posts itself automatically:

Use a normal button or hyperlink, connected to an action. The action would contain something like:

public WOComponent doSomethingAction() {
  //calculate value1
  //calculate value2
  //other processing...
  RedirectionPage rp = new RedirectionPage();
  rp.setField1(value1);
  rp.setField2(value2);
  return rp;
}

Create the new RedirectionPage component. It is an empty page, that consists of words "Please Wait", and the static Form with two hidden fields: field1 and field2. In the static Form's declaration, add the following action (in bold): <FORM method=post ACTION=”www.othersite.com/cgi-bin/page.pl” NAME=formName>

In the page's tag, add this JavaScript to post the form (in bold): <BODY onLoad=”document.formName.submit(); return true;”>

That's all. You may instead write a generic "Please Wait" page for your application, containing a dictionary of fields/values, and a variable target for the form. That way you would get maximum reuse.

Direct Actions

Let's do an experiment. Run any application and get in it through the front door by going to the URL:

http://localhost/cgi-bin/WebObjects.exe/AppName

Then click on a link. You should get a weird-looking URL syntax, with lots of digits that don't seem to mean anything. Here's an example URL:

http://localhost/cgi-bin/WebObjects.exe/AppName.woa/wo/nJ600003400sh6002/0.6

Let's dissect this URL. Of course, the first part is the protocol and server (http://...) followed by the WebObjects Adaptor (/cgi-bin/WebObjects.exe). Then there's the application name (AppName.woa - the extension is optional and means "WebObjects Application"). Until now, everything seems exactly like the previous URL. But what is the rest for? The wo/ tells us we are looking at a Web component. The following nJ600003400sh6002 is a randomly generated code that represents the current session, stored in the Session object as the member sessionID(). Lastly, the code /0.6 represents the page itself.

Take a look at the session identifier again. It is randomly generated. That means, when you return to the front door of the application, a new sessionID is created. This also means, after a session has expired, there's no way to point directly to the current page, if it is not the component Main.wo. This can be problematic, because sometimes you want to be able to bookmark or jump directly to a page.

This is a job for the Direct Actions. A Direct Action is a "backdoor" to your site. It is represented by a special URL syntax, like this:

http://localhost/cgi-bin/WebObjects.exe/AppName.woa/wa/actionName

Here the .woa extension is mandatory. The following /wa indicates a Direct Action, as opposed to a Component. And then there's the Direct Action's name.

Here there's no session identifier involved. The page is bookmarkable, and can be hardcoded in external Web pages and applications.

By default, Direct Actions point to a special class of your application, called DirectAction (extends WODirectAction), which is created automatically by the New Project assistant. When the user specifies a Direct Action, WebObjects calls automatically to the right function in this class, by using the Reflection API - no need to configure anything!

As an example, consider an administration page Admin.wo. It cannot be accessed directly or else any user would have total control. Creating a backdoor access to this site is very simple. The URL to use is:

http://localhost/cgi-bin/WebObjects.exe/AppName.woa/wa/Admin

and the corresponding Direct Action method is:

public class DirectAction extends WODirectAction {
  //...
  public WOComponent AdminAction() {
    Admin a = (Admin)pageWithName("Admin");
    return a;
  }
  //...
}

The important point here is to use a different Direct Action name for every "backdoor" entry point of the application, and to create a function of the same name, but with the word "Action" appended. Example: Since the Direct Action name is "AdminPage", so the function name is AdminPageAction().

TRICK: Say you want to get in through a backdoor from an external application, but keep the current session's context at the same time. You can do that by sending the session().sessionID() String out to the external application, then by going back to the desired Direct Action and passing back the session identifier in the URL parameter "wosid". This acronym means "WebObjects Session IDentifier", and it's a reserved URL/form parameter name. Here's an example URL:

http://localhost/cgi-bin/WebObjects/AppName.woa/wa/action?wosid=nJ600003400sh6002

Many more things can be done with Direct Actions. For example, in buttons and hyperlinks, Direct Actions can be used in lieu of regular actions. Also, special URL syntax can be used to access Direct Actions located in classes other than DirectAction.

Customizing Error Pages

Whenever there's an exception in your application, whether it happens in your code or in the frameworks, you get the usual, dreaded, ugly WebObjects exception page. Take a look at this sample:


Figure 1. Default ugly error page.

You don't want your users to see that - believe me. In an ideal world, you would never get an exception. But this is far from an ideal world, so let's get to work.

What you do want your users to see is a custom-made company-standards-compliant pretty page. How do you achieve that? Well, you can for sure catch exceptions as they occur (try block). Then you can redirect right away to your own nice exception page.

The following piece of code does just that, in the form of an action method:

WOComponent anAction() {
  try {
    doSomething();
    return resultPage;
  } catch (Throwable e) {
    NiceExceptionPage nep = new NiceExceptionPage();
    nep.setExceptionMessage(e.getMessage());
    nep.setTrace(NSLog.throwableAsString(e));
    return nep;
  }
}

But that's not complete. What about the other exceptions, the ones that happen outside your control? You're in luck. Whenever an uncaught exception occurs, WebObjects calls Application.handleException(). Let's see an example in which we overload this function to fire up our NiceExceptionPage:

public WOResponse handleException (Exception e, WOContext c) {
  NiceExceptionPage nep = new NiceExceptionPage(c);
  nep.setExceptionMessage(e.getMessage());
  nep.setTrace(NSLog.throwableAsString(e));

  WOResponse response = nep.generateResponse();

  return response;
}

You can even make a different error page for each of these events, by overloading the proper function:

Error handler in Application Is called ...
handleException() ... always. Default
implementation
brings up one of
the next error
pages, or if none of
them are
appropriate, brings
up the page
WOExceptionPage.
handleSessionCreationErrorInContext() ... when there was
an exception during
session
initialization. By
default, brings up
the page
WOSession
CreationError.
handleSessionRestorationErrorInContext() ... when the session
timed out and
the user is trying to
go back to an
earlier page. By
default, brings up
the page
WOSession
RestorationError.
handlePageRestorationErrorInContext() ...when the user
backtracks to an
earlier page (back
button) but the
context of that page
is lost. By default,
brings up the page
WOPage
RestorationError.

There's a second way to customize the error pages. You can fix all exception pages once and for all, for every single application on your computer or your server. It involves modifying the template files for the actual (ugly) exception pages from WebObjects' framework folders. Here's the location of these components:

Location Component Name
/System/Library/Frameworks/ WOExceptionPage.wo
JavaWOExtensions.framework/ WOPageRestorationError.wo
Resources/ WOSessionCreationError.wo
WOSessionRestorationError.wo

NOTE: when you modify these components, don't forget to

  • make a copy of the original page before starting
  • propagate the modified pages to all your Server(s) along with your application - they are not tied to your project

This will save you a lot of trouble.


Emmanuel Proulx is a Course Writer, Author and Web Developer, working in the domain of Java Application Servers. He can be reached at emmanuelproulx@yahoo.com.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Audio Hijack 3.2.0 - Record and enhance...
Audio Hijack (was Audio Hijack Pro) drastically changes the way you use audio on your computer, giving you the freedom to listen to audio when you want and how you want. Record and enhance any audio... Read more
FontExplorer X Pro 5.0.1 - Font manageme...
FontExplorer X Pro is optimized for professional use; it's the solution that gives you the power you need to manage all your fonts. Now you can more easily manage, activate and organize your... Read more
Calcbot 1.0.2 - Intelligent calculator a...
Calcbot is an intelligent calculator and unit converter for the rest of us. Featuring an easy-to-read history tape, expression view, intuitive conversion, and much more! Features History Tape -... Read more
MTR 5.0.0.1 - The Mac's oldest and...
MTR (was MacTheRipper)--the Mac's oldest and smartest DVD-backup app--is now updated to version 5.001 MTR -- the complete toolbox, not a one-trick, point-and-click extractor. MTR is intended for... Read more
LibreOffice 4.4.5.2 - Free, open-source...
LibreOffice is an office suite (word processor, spreadsheet, presentations, drawing tool) compatible with other major office suites. The Document Foundation is coordinating development and... Read more
Adobe Lightroom 6.1.1 - Import, develop,...
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
File Juicer 4.41 - Extract images, video...
File Juicer is a drag-and-drop can opener and data archaeologist. Its specialty is to find and extract images, video, audio, or text from files which are hard to open in other ways. It finds and... Read more
A Better Finder Rename 9.52 - File, phot...
A Better Finder Rename is the most complete renaming solution available on the market today. That's why, since 1996, tens of thousands of hobbyists, professionals and businesses depend on A Better... Read more
OmniFocus 2.2.3 - GTD task manager with...
OmniFocus helps you manage your tasks the way that you want, freeing you to focus your attention on the things that matter to you most. Capturing tasks and ideas is always a keyboard shortcut away in... Read more
TinkerTool 5.4 - Expanded preference set...
TinkerTool is an application that gives you access to additional preference settings Apple has built into Mac OS X. This allows to activate hidden features in the operating system and in some of the... Read more

Cosmonautica (Games)
Cosmonautica 1.1 Device: iOS Universal Category: Games Price: $6.99, Version: 1.1 (iTunes) Description: Cast off! Are you ready for some hilarious adventures in outer space? | Read more »
Rescue humanity from a Demon horde in An...
Angel Stone is Fincon's follow up to the massively successful Hello Hero and is out now on iOS and Android. You play as a member of The Resistance, a group of mighty human warriors who have risen up in defiance of the Demon horde threatening to... | Read more »
Gallery Doctor (Photography)
Gallery Doctor 1.0 Device: iOS iPhone Category: Photography Price: $2.99, Version: 1.0 (iTunes) Description: Free up valuable iCloud and iPhone storage with Gallery Doctor, the only iPhone cleaner that automatically identifies the... | Read more »
You Against Me (Games)
You Against Me 1.0 Device: iOS Universal Category: Games Price: $.99, Version: 1.0 (iTunes) Description: A simple game… You. Me. Claim, steal, lock, score, win! | Read more »
Yep, it's True - Angry Birds 2 is O...
The not exactly rumors were true and the birds are back. Angry Birds 2 has come to the App Store and the world will... well I suppose it'll still be the same, but now we have more bird-flinging options! [Read more] | Read more »
You Could Design Your Own Card for Chain...
If you've ever wanted to create your own item, weapon, trap, or even monster for Chainsaw Warrior: Lords of the Night, this is your chance. Auroch Digital is currently holding a contest so that fans can fight to the death (not really) to see which... | Read more »
Bitcoin Billionaire is Going Back in Tim...
If you thought you managed to buy everything there is to buy in Bitcoin Billionaire and make all the money, well you though wrong. Those of you who made it far enough might remember investing in time travel - and it looks like that investment is... | Read more »
Domino Drop (Games)
Domino Drop 1.0 Device: iOS Universal Category: Games Price: $1.99, Version: 1.0 (iTunes) Description: Domino Drop is a delightful new puzzle game with dominos and gravity!Learn how to play it in a minute, master it day by day.Your... | Read more »
OPERATION DRACULA (Games)
OPERATION DRACULA 1.0.1 Device: iOS Universal Category: Games Price: $5.99, Version: 1.0.1 (iTunes) Description: 25% off launch sale!!! 'Could prove to be one of the most accurate representations of the Japanese bullet hell shmup... | Read more »
Race The Sun (Games)
Race The Sun 1.01 Device: iOS iPhone Category: Games Price: $4.99, Version: 1.01 (iTunes) Description: You are a solar craft. The sun is your death timer. Hurtle towards the sunset at breakneck speed in a futile race against time.... | Read more »

Price Scanner via MacPrices.net

Sale! 13-inch MacBook Pros on sale for $100 o...
B&H Photo has 13″ MacBook Pros on sale for $100 off MSRP. Shipping is free, and B&H charges NY sales tax only: - 13″ 2.5GHz/500GB MacBook Pro: $999.99 save $100 - 13″ 2.7GHz/128GB Retina... Read more
Sale! Save $100 on 13-inch MacBook Airs this...
B&H Photo has the 13″ 1.6GHz/128GB MacBook Air on sale for $899.99 including free shipping plus NY tax only. Their price is $100 off MSRP, and it’s the lowest price available for this model.... Read more
Worldwide Tablet Market Decline Continues, Ap...
The worldwide tablet market declined -7.0% year-over-year in the second quarter of 2015 (2Q15) with shipments totaling 44.7 million units according to preliminary data from the International Data... Read more
TP-LINK TL-PA8030P KIT Powerline Featuring Ho...
Consumer and business networking products provider TP-LINK is now shipping its TL-PA8030P KIT AV1200 3-Port Gigabit Passthrough Powerline Starter Kit that expands your home’s network over its... Read more
Apple refurbished iPad Air 2s available for u...
The Apple Store has Apple Certified Refurbished iPad Air 2s available for up to $140 off the price of new models. Apple’s one-year warranty is included with each model, and shipping is free: - 128GB... Read more
Updated Apple iPad Price Trackers
We’ve updated our iPad Air Price Tracker and our iPad mini Price Tracker with the latest information on prices and availability from Apple and other resellers. Read more
Apple refurbished 2014 13-inch 128GB MacBook...
The Apple Store has Apple Certified Refurbished 2014 13″ MacBook Airs available starting at $759. An Apple one-year warranty is included with each MacBook, and shipping is free: - 13″ 1.4GHz/128GB... Read more
Apple’s Education discount saves up to $300 o...
Purchase a new Mac or iPad at The Apple Store for Education and take up to $300 off MSRP. All teachers, students, and staff of any educational institution qualify for the discount. Shipping is free,... Read more
Save up to $600 with Apple refurbished Mac Pr...
The Apple Store has Apple Certified Refurbished Mac Pros available for up to $600 off the cost of new models. An Apple one-year warranty is included with each Mac Pro, and shipping is free. The... Read more
Mac Pros on sale for up to $260 off MSRP
B&H Photo has Mac Pros on sale for up to $260 off MSRP. Shipping is free, and B&H charges sales tax in NY only: - 3.7GHz 4-core Mac Pro: $2799, $200 off MSRP - 3.5GHz 6-core Mac Pro: $3719.99... Read more

Jobs Board

*Apple* Retail - Multiple Positions (US) - A...
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* Online Store UAT Lead - Apple (Unite...
**Job Summary** The Apple Online Store is a fast paced and ever evolving business environment. The User Acceptance Testing (UAT) lead in this organization is able to Read more
*Apple* MAC Support Services Subject Matter...
Title: Apple MAC Support Services Subject Matter Expert Location: Pleasanton, CA Type of position: Temporary Contract for approximately 6 weeks Tasks The tasks for the Read more
Lead Infrastructure Engineer - *Apple* /Mac P...
…of a team * Requires proven problem solving skills Preferred Additional: * Apple Certified System Administrator (ACSA) * Apple Certified Technical Coordinator (ACTC) Read more
*Apple* Retail - Multiple Positions (US) - A...
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.