Home > Articles

This chapter is from the book

9.4 The MVC Pattern at the Application Level

So far, we have looked at the basic MODEL-VIEW-CONTROLLER pattern and its implementation in the JFace framework. The examples have been rather small and perhaps a little contrived, to enable us to focus on the mechanisms and crucial design constraints. Now it is time to scale the gained insights to the application level. The question we will pursue is how model-view separation influences the architecture of the overall product. Furthermore, we will look at details that need to be considered for this scaling, such as incremental repainting of the screen.

The running example will be a minimal spreadsheet application Mini-Xcel (Fig. 9.12). In this application, the user can select a cell in a special widget displaying the spreadsheet, and can enter a formula into that cell, possibly referring to other cells. The application is responsible for updating all dependent cells automatically, as would be expected.

Figure 9.12

Figure 9.12 The MiniXcel Application

The application offers enough complexity to explore the points mentioned previously. First, the model contains dependencies between cells in the form of formulas, and the parsing of and computation with formulas constitutes a nontrivial functionality in itself. At the interface level, we need a custom-painted widget for the spreadsheet, which must also offer view-level visual feedback and a selection mechanism to link the spreadsheet to the input line on top.

9.4.1 Setting up the Application

The overall structure of the application is shown in Fig. 9.13. The Spread Sheet encapsulates the functional core. It manages cells, which can be addressedarrow.jpg 9.1 from the outside by usual coordinates such as A2 or B3, as well as their interdependencies given by the stored formulas. A formula is a tree-structured COMPOSITE that performs the actual computations. A simplearrow.jpg 2.3.1 (shift-reduce) parser transforms the input strings given by the userbook.jpg 2 into structured formulas. The core point of model-view separation is implementedarrow.jpg 9.1 by making all functionality that is not directly connected to the user interface completely independent of considerations about the display.

Figure 9.13

Figure 9.13 Structure of the MiniXcel Application

The main window (Fig. 9.12) consists of two parts: the SpreadSheet View at the bottom and the CellEditor at the top. These two are coupleddouble_arrow.jpg 12.1 loosely: The SpreadSheetView does not assume that there is a single Cell Editor. Instead, it publishes a generic IStructuredSelection containing the currently selected Cell model element. When the user presses “enter,”arrow.jpg 9.3.2 the cell editor can simply call setFormula on that Cell. This has two effects. First, the dependent cells within the spreadsheet are updated by reevaluating their formulas. Second, all updated cells will notify the view, through their surrounding SpreadSheet model.arrow.jpg 2.2.4

9.4.2 Defining the Model

We can give here only a very brief overview of the model code and highlight those aspects that shape the collaboration between user interface and model. The central element of the model is the SpreadSheet class. It keeps a sparse mapping from coordinates to Cells (line 2) and creates cells on demand as they are requested from the outside (lines 5–12). The model implements the OBSERVER pattern as usual to enable the view to remain up-to-date (lines 4, 14–16, 18–20). The class Coordinates merely stores a row and column of a cell.

minixcel.model.spreadsheet.SpreadSheet

 1 public class SpreadSheet {
 2     private final HashMap<Coordinates, Cell> cells =
 3                       new HashMap<Coordinates, Cell>();
 4     private final ListenerList listeners = new ListenerList();
 5     public Cell getCell(Coordinates coord) {
 6         Cell res = cells.get(coord);
 7         if (res == null) {
 8             res = new Cell(this, coord);
 9             cells.put(coord, res);
10         }
11         return res;
12     }
13
14     public void addSpreadSheetListener(SpreadSheetListener l) {
15         listeners.add(l);
16     }
17      ...
18     void fireCellChanged(Cell cell) {
19          ...
20     }
21      ...
22 }

Application models usually have internal dependencies.

Each Cell in the spreadsheet must store the user’s input (line 4 in the next code snippet) and must be prepared to evaluate that formula quickly (line 5). Since the view will query the current value rather frequently and other cells will require it for evaluating their own formulas, it is sensible to cache that value rather than repeatedly recomputing it (line 6). As furtherarrow.jpg 2.2.1 basic data, the cell keeps its owner and the position in that owner (lines 2–3).

The example of spreadsheets also shows that an application model isarrow.jpg 1.3.3 rarely as simple as, for instance, a list of Java beans. Usually, the objects within the model require complex interdependencies and collaborations to implement the desired functionality. In Cells, we store the (few) cross references introduced by the formula in two lists: dependsOn lists those cells whose values are required in the formula; dependentOnThis is the inverse relationship, which is required for propagating updates through the spreadsheet.

minixcel.model.spreadsheet.Cell

 1 public class Cell {
 2     final SpreadSheet spreadSheet;
 3     private final Coordinates coord;
 4     private String formulaString = "";
 5     private Formula formula = null;
 6     private Value cachedValue = new Value();
 7     private final List<Cell> dependsOn = new ArrayList<Cell>();
 8     private final List<Cell> dependentOnThis =
 9                                new ArrayList<Cell>();
10
11      ...
12 }

Clients cannot adequately anticipate the effects of an operation.

One result of the dependencies within the model is that clients, such as the controllers in the user interface, cannot foresee all the changes that are effected by an operation they call. As a result, the controller of the MVC could not reliably notify the view about necessary repainting even without interference from other controllers. This fact reinforces the crucial designarrow.jpg 9.2.3 decision of updating the view by observing the model.

In the current example, the prototypical modification is setting a new formula on a cell. The overall approach is straightforward: Clear the old dependency information, and then set and parse the new input. Afterward, we can update the new dependencies by asking the formula for its references and recomputing the current cached value.

minixcel.model.spreadsheet.Cell

 1 public void setFormulaString(String formulaString) {
 2     clearDependsOn();
 3     this.formulaString = formulaString;
 4      ... special cases such as an empty input string
 5     formula = new Formula(spreadSheet.getFormulaFactory(),
 6                           formulaString);
 7     fillDependsOn();
 8      ... check for cycles
 9     recomputeValue();
10 }

The update process of a single cell now triggers updating the dependencies as well: The formula is evaluated and the result is stored.

minixcel.model.spreadsheet.Cell.recomputeValue

private void recomputeValue() {
     ...
    setCachedValue(new Value(formula.eval(
                               new SpreadSheetEnv(spreadSheet))));
     ... error handling on evaluation error
}

The cache value is therefore the “current” value of the cell. Whenever that changes, two stakeholders must be notified: the dependent cells within the spreadsheet and the observers outside of the spreadsheet. Both goals are accomplished in the method setCachedValue():

minixcel.model.spreadsheet.Cell

protected void setCachedValue(Value val) {
    if (val.equals(cachedValue))
        return;
    cachedValue = val;
    for (Cell c : dependentOnThis)
        c.recomputeValue();
    spreadSheet.fireCellChanged(this);
}

This brief exposition is sufficient to highlight the most important points with respect to model-view separation. Check out the online supplement for further details—for instance, on error handling for syntax errors in formulas and cyclic dependencies between cells.

9.4.3 Incremental Screen Updates

Many applications of model-view separation are essentially simple, with small models being displayed in small views. Yet, one often comes across the other extreme. Even a simple text viewer without any formatting must be careful to repaint only the portion of text determined by the scrollbars, and from that only the actually changing lines. Otherwise, the scrolling and editing process will become unbearably slow. The MiniXcel example is sufficiently complex to include a demonstration of the necessary processes.

Before we delve into the details, Fig. 9.14 gives an overview of the challenge.arrow.jpg 7.8 Put very briefly, it consists of the fact that even painting on the screen is event-driven: When a change notification arrives from the model, one never paints the corresponding screen section immediately. Instead, one asks to be called back for the job later on. In some more detail, the model on the left in Fig. 9.14 sends out some change notification to its observers. The view must then determine where it has painted the modified data. That area of the screen is then considered “damaged” and is reported to the window system. The window system gathers such damaged areas, subtracts any parts that are not visible anyway, coalesces adjacent areas, and maybe performs some other optimizations. In the end, it comes back to the view requesting a certain area to be repainted. At this point, the view determines the model elements overlapping this area and displays them on the screen.

Figure 9.14

Figure 9.14 Process of Incremental Screen Updates

A further reason for this rather complex procedure, besides the possibility of optimizations, is that other events, such as the moving and resizing of windows, can also require repainting, so that the right half of Fig. 9.14 would be necessary in any case. The extra effort of mapping model elements to screen areas in the left half is repaid by liberating the applications of optimizing the painting itself.

Let us track the process in Fig. 9.14 from left to right, using the concrete example of the MiniXcel SpreadSheetView. At the beginning, the view receives a change notification from the model. If the change concerns a single cell, that cell has to be repainted.

minixcel.ui.spreadsheet.SpreadSheetView.spreadSheetChanged

public void spreadSheetChanged(SpreadSheetChangeEvent evt) {
    switch (evt.type) {
    case CELL:
        redraw(evt.cell.getCoordinates());
        break;
     ...
    }
}

It will turn out later that cells need to be repainted on different occasions, such as to indicate selection or mouse hovering. We therefore implementdouble_arrow.jpg 9.4.4 the logic in a helper method, shown next. The method redraw()arrow.jpg 1.4.8 arrow.jpg 1.4.5 called on the mainArea of the view is provided by SWT and reports the area as damaged.

minixcel.ui.spreadsheet.SpreadSheetView

public void redraw(Coordinates coords) {
    Rectangle r = getCellBounds(coords);
    mainArea.redraw(r.x, r.y, r.width, r.height, false);
}

In a real implementation, the method getCellBounds() would determine the coordinates by the sizes of the preceding columns and rows. To keep the example simple, all columns have the same width and all rows have the same height in MiniXcel. This finishes the left half of Fig. 9.14. Now it is the window system’s turn to do some work.

minixcel.ui.spreadsheet.SpreadSheetView

protected Rectangle getCellBounds(Coordinates coords) {
    int x = (coords.col - viewPortColumn) * COL_WIDTH;
    int y = (coords.row - viewPortRow) * ROW_HEIGHT;
    return new Rectangle(x, y, COL_WIDTH, ROW_HEIGHT);
}

In the right half of Fig. 9.14, the MainArea is handed a paint request for a given rectangular area on the screen, in the form of a PaintEvent passed to the method shown next. This method determines the range of cells touched by the area (line 3). Then, it paints all cells in the area in the nested loops in lines 7 and 11. As an optimization, it does not recompute the area covered by each cell, as done for the first cell in line 5. Instead, it moves that area incrementally, using cells that are adjacent in the view (lines 9, 14, 16).

minixcel.ui.spreadsheet.MainArea.paintControl

 1 public void paintControl(PaintEvent e) {
 2      ... prepare colors
 3     Rectangle cells = view.computeCellsForArea(e.x, e.y, e.width,
 4                                                e.height);
 5     Rectangle topLeft = view.computeAreaForCell(cells.x, cells.y);
 6     Rectangle cellArea = Geometry.copy(topLeft);
 7     for (int row = cells.y; row < cells.y + cells.height; row++) {
 8         cellArea.height = SpreadSheetView.ROW_HEIGHT;
 9         cellArea.x = topLeft.x;
10         cellArea.width = SpreadSheetView.COL_WIDTH;
11         for (int col = cells.x;
12              col < cells.x + cells.width; col++) {
13             paintCell(col, row, cellArea, gc);
14             cellArea.x += cellArea.width;
15         }
16         cellArea.y += cellArea.height;
17     }
18 }

The actual painting code in paintCell() is then straightforward, if somewhat tedious. It has to take into account not only the cell content, but also the possible selection of the cell and a mouse cursor being inside, both of which concern view-level logic treated in the next section. Leaving all of that aside, the core of the method determines the current cell value, formats it as a string, and paints that string onto the screen (avoiding the creation of yet more empty cells):

minixcel.ui.spreadsheet.MainArea

private void paintCell(int col, int row,
                       Rectangle cellArea, GC gc) {
    if (view.model.hasCell(new Coordinates(col, row))) {
        cell = view.model.getCell(new Coordinates(col, row));
        Value val = cell.getValue();
        String displayText;
        displayText = String.format("%.2f", val.asDouble());
        gc.drawString(displayText, cellArea.x, cellArea.y, true);
    }
}

This final painting step finishes the update process shown in Fig. 9.14. In summary, incremental repainting achieves efficiency in user interface programming:arrow.jpg 2.1.3 The view receives detailed change notifications, via the “push” variant of the OBSERVER pattern, which it translates to minimal damaged areas on the screen, which get optimized by the window system, before the view repaints just the model elements actually touched by those areas.

9.4.4 View-Level Logic

arrow.jpg 9.2.5 We have seen in the discussion of the MVC pattern that widgets usually include behavior such as visual feedback that is independent of the model itself. MiniXcel provides two examples: selection of cells and feedback about the cell under the mouse. We include them in the discussion since this kind of behavior must be treated with the same rigor as the model: Users consider only applications that react consistently and immediately as trustworthy.

Treat selection as view-level state.

Most widgets encompass some form of selection. For instance, tables, lists,arrow.jpg 9.3.2 and trees allow users to select rows, which JFace maps to the underlying model element rendered in these rows. The interesting point about selection is that it introduces view-level state, which is orthogonal to the application’s core model-level state.

We will make our SpreadSheetView a good citizen of the community by implementing ISelectionProvider. That interface specifies that clients can query the current selection, set the current selection (with appropriate elements), and listen for changes in the selection. The last capability will also enable us to connect the entry field for a cell’s content to the spreadsheet (Fig. 9.13). For simplicity, we support only single selection and introduce a corresponding field into the SpreadSheetView.

minixcel.ui.spreadsheet.SpreadSheetView

Cell curSelection;

The result of querying the current selection is a generic ISelection. Viewers that map model elements to screen elements, such as tables and trees, usually return a more specific IStructuredSelection containing these elements. We do the same here with the single selected cell.

minixcel.ui.spreadsheet.SpreadSheetView.getSelection

public ISelection getSelection() {
    if (curSelection != null)
        return new StructuredSelection(curSelection);
    else
        return StructuredSelection.EMPTY;
}

Since the selection must be broadcast to observers and must be mirrored on the screen, we introduce a private setter for the field.

minixcel.ui.spreadsheet.SpreadSheetView

private void setSelectedCell(Cell cell) {
    if (curSelection != cell) {
        Cell oldSelection = curSelection;
        curSelection = cell;
        fireSelectionChanged();
         ... update screen from oldSelection to curSelection
    }
}

The remainder of the implementation of the OBSERVER pattern for selectionarrow.jpg 2.1 is straightforward. However, its presence reemphasizes the role of selection as proper view-level state.

Visual feedback introduces internal state.

The fact that painting is event-driven, so that a widget cannot paint visualarrow.jpg 7.8 feedback immediately, means that the widget must store the desired feedback as private state, determine the affected screen regions, and render the feedback in the callback (Fig. 9.14).

For MiniXcel, we wish to highlight the cell under the mouse cursor, so that users know which cell they are targeting in case they click to select it. The required state is a simple reference. However, since the state is purelyarrow.jpg 9.4.2 view-level, we are content with storing its coordinates; otherwise, moving over a yet unused cell would force the model to insert an empty Cell object.

minixcel.ui.spreadsheet.SpreadSheetView

Coordinates curCellUnderMouse;

Setting a new highlight is then similar to setting a new selected cell:

minixcel.ui.spreadsheet.SpreadSheetView

protected void setCellUnderMouse(Coordinates newCell) {
    if (!newCell.equals(curCellUnderMouse)) {
        Coordinates oldCellUnderMouse = curCellUnderMouse;
        curCellUnderMouse = newCell;
         ... update screen from old to new
    }
}

The desired reactions to mouse movements and clicks are implemented by the following simple listener. The computeCellAt() method returns the cell’s coordinates, also taking into account the current scrolling position. While selection then requires a real Cell object from the model, the targeting feedback remains at the view level.

minixcel.ui.spreadsheet.SpreadSheetView.mouseMove

public void mouseMove(MouseEvent e) {
    setCellUnderMouse(computeCellAt(e.x, e.y));
}
public void mouseDown(MouseEvent e) {
    setSelectedCell(model.getCell(computeCellAt(e.x, e.y)));
}

The painting event handler merges the visual and model states.

The technical core of visual feedback and view-level state, as shown previously, is not very different from the model-level state. When painting the widget, we have to merge the model- and view-level states into one consistent overall appearance. The following method achieves this by first painting the cell’s content (lines 4–5) and overlaying this with a frame, which is either a selection indication (lines 9–12), the targeting highlight (lines 13–17), or the usual cell frame (lines 19–23).

minixcel.ui.spreadsheet.MainArea

 1 private void paintCell(int col, int row,
 2                        Rectangle cellArea, GC gc) {
 3      ...
 4         displayText = String.format("%.2f", val.asDouble());
 5         gc.drawString(displayText, cellArea.x, cellArea.y, true);
 6     Rectangle frame = Geometry.copy(cellArea);
 7     frame.width-;
 8     frame.height-;
 9     if (view.curSelection != null && view.curSelection == cell) {
10         gc.setForeground(display.getSystemColor(
11                                     SWT.COLOR_DARK_BLUE));
12         gc.drawRectangle(frame);
13     } else if (view.curCellUnderMouse != null
14             && view.curCellUnderMouse.col == col
15             && view.curCellUnderMouse.row == row) {
16         gc.setForeground(display.getSystemColor(SWT.COLOR_BLACK));
17         gc.drawRectangle(frame);
18     } else {
19         gc.setForeground(display.getSystemColor(SWT.COLOR_GRAY));
20         int bot = frame.y + frame.height;
21         int right = frame.x + frame.width;
22         gc.drawLine(right, frame.y, right, bot);
23         gc.drawLine(frame.x, bot, right, bot);
24     }
25 }

According to this painting routine, the view-level state is always contained within the cells to which it refers. It is therefore sufficient to repaint these affected cells when the state changes. For the currently selected cell, the code is shown here. For the current cell under the mouse, it is analogous.

minixcel.ui.spreadsheet.SpreadSheetView

private void setSelectedCell(Cell cell) {
    if (curSelection != cell) {
         ...
        if (oldSelection != null)
            redraw(oldSelection.getCoordinates());
        if (curSelection != null)
            redraw(curSelection.getCoordinates());
    }
}

This code is made efficient through the incremental painting pipeline shown in Fig. 9.14 on page 500 and implemented in the code fragments shown earlier. Because the pipeline is geared toward painting the minimal necessary number of cells, it can also be used to paint single cells reliably and efficiently.

InformIT Promotional Mailings & Special Offers

I would like to receive exclusive offers and hear about products from InformIT and its family of brands. I can unsubscribe at any time.

Overview


Pearson Education, Inc., 221 River Street, Hoboken, New Jersey 07030, (Pearson) presents this site to provide information about products and services that can be purchased through this site.

This privacy notice provides an overview of our commitment to privacy and describes how we collect, protect, use and share personal information collected through this site. Please note that other Pearson websites and online products and services have their own separate privacy policies.

Collection and Use of Information


To conduct business and deliver products and services, Pearson collects and uses personal information in several ways in connection with this site, including:

Questions and Inquiries

For inquiries and questions, we collect the inquiry or question, together with name, contact details (email address, phone number and mailing address) and any other additional information voluntarily submitted to us through a Contact Us form or an email. We use this information to address the inquiry and respond to the question.

Online Store

For orders and purchases placed through our online store on this site, we collect order details, name, institution name and address (if applicable), email address, phone number, shipping and billing addresses, credit/debit card information, shipping options and any instructions. We use this information to complete transactions, fulfill orders, communicate with individuals placing orders or visiting the online store, and for related purposes.

Surveys

Pearson may offer opportunities to provide feedback or participate in surveys, including surveys evaluating Pearson products, services or sites. Participation is voluntary. Pearson collects information requested in the survey questions and uses the information to evaluate, support, maintain and improve products, services or sites, develop new products and services, conduct educational research and for other purposes specified in the survey.

Contests and Drawings

Occasionally, we may sponsor a contest or drawing. Participation is optional. Pearson collects name, contact information and other information specified on the entry form for the contest or drawing to conduct the contest or drawing. Pearson may collect additional personal information from the winners of a contest or drawing in order to award the prize and for tax reporting purposes, as required by law.

Newsletters

If you have elected to receive email newsletters or promotional mailings and special offers but want to unsubscribe, simply email information@informit.com.

Service Announcements

On rare occasions it is necessary to send out a strictly service related announcement. For instance, if our service is temporarily suspended for maintenance we might send users an email. Generally, users may not opt-out of these communications, though they can deactivate their account information. However, these communications are not promotional in nature.

Customer Service

We communicate with users on a regular basis to provide requested services and in regard to issues relating to their account we reply via email or phone in accordance with the users' wishes when a user submits their information through our Contact Us form.

Other Collection and Use of Information


Application and System Logs

Pearson automatically collects log data to help ensure the delivery, availability and security of this site. Log data may include technical information about how a user or visitor connected to this site, such as browser type, type of computer/device, operating system, internet service provider and IP address. We use this information for support purposes and to monitor the health of the site, identify problems, improve service, detect unauthorized access and fraudulent activity, prevent and respond to security incidents and appropriately scale computing resources.

Web Analytics

Pearson may use third party web trend analytical services, including Google Analytics, to collect visitor information, such as IP addresses, browser types, referring pages, pages visited and time spent on a particular site. While these analytical services collect and report information on an anonymous basis, they may use cookies to gather web trend information. The information gathered may enable Pearson (but not the third party web trend services) to link information with application and system log data. Pearson uses this information for system administration and to identify problems, improve service, detect unauthorized access and fraudulent activity, prevent and respond to security incidents, appropriately scale computing resources and otherwise support and deliver this site and its services.

Cookies and Related Technologies

This site uses cookies and similar technologies to personalize content, measure traffic patterns, control security, track use and access of information on this site, and provide interest-based messages and advertising. Users can manage and block the use of cookies through their browser. Disabling or blocking certain cookies may limit the functionality of this site.

Do Not Track

This site currently does not respond to Do Not Track signals.

Security


Pearson uses appropriate physical, administrative and technical security measures to protect personal information from unauthorized access, use and disclosure.

Children


This site is not directed to children under the age of 13.

Marketing


Pearson may send or direct marketing communications to users, provided that

  • Pearson will not use personal information collected or processed as a K-12 school service provider for the purpose of directed or targeted advertising.
  • Such marketing is consistent with applicable law and Pearson's legal obligations.
  • Pearson will not knowingly direct or send marketing communications to an individual who has expressed a preference not to receive marketing.
  • Where required by applicable law, express or implied consent to marketing exists and has not been withdrawn.

Pearson may provide personal information to a third party service provider on a restricted basis to provide marketing solely on behalf of Pearson or an affiliate or customer for whom Pearson is a service provider. Marketing preferences may be changed at any time.

Correcting/Updating Personal Information


If a user's personally identifiable information changes (such as your postal address or email address), we provide a way to correct or update that user's personal data provided to us. This can be done on the Account page. If a user no longer desires our service and desires to delete his or her account, please contact us at customer-service@informit.com and we will process the deletion of a user's account.

Choice/Opt-out


Users can always make an informed choice as to whether they should proceed with certain services offered by InformIT. If you choose to remove yourself from our mailing list(s) simply visit the following page and uncheck any communication you no longer want to receive: www.informit.com/u.aspx.

Sale of Personal Information


Pearson does not rent or sell personal information in exchange for any payment of money.

While Pearson does not sell personal information, as defined in Nevada law, Nevada residents may email a request for no sale of their personal information to NevadaDesignatedRequest@pearson.com.

Supplemental Privacy Statement for California Residents


California residents should read our Supplemental privacy statement for California residents in conjunction with this Privacy Notice. The Supplemental privacy statement for California residents explains Pearson's commitment to comply with California law and applies to personal information of California residents collected in connection with this site and the Services.

Sharing and Disclosure


Pearson may disclose personal information, as follows:

  • As required by law.
  • With the consent of the individual (or their parent, if the individual is a minor)
  • In response to a subpoena, court order or legal process, to the extent permitted or required by law
  • To protect the security and safety of individuals, data, assets and systems, consistent with applicable law
  • In connection the sale, joint venture or other transfer of some or all of its company or assets, subject to the provisions of this Privacy Notice
  • To investigate or address actual or suspected fraud or other illegal activities
  • To exercise its legal rights, including enforcement of the Terms of Use for this site or another contract
  • To affiliated Pearson companies and other companies and organizations who perform work for Pearson and are obligated to protect the privacy of personal information consistent with this Privacy Notice
  • To a school, organization, company or government agency, where Pearson collects or processes the personal information in a school setting or on behalf of such organization, company or government agency.

Links


This web site contains links to other sites. Please be aware that we are not responsible for the privacy practices of such other sites. We encourage our users to be aware when they leave our site and to read the privacy statements of each and every web site that collects Personal Information. This privacy statement applies solely to information collected by this web site.

Requests and Contact


Please contact us about this Privacy Notice or if you have any requests or questions relating to the privacy of your personal information.

Changes to this Privacy Notice


We may revise this Privacy Notice through an updated posting. We will identify the effective date of the revision in the posting. Often, updates are made to provide greater clarity or to comply with changes in regulatory requirements. If the updates involve material changes to the collection, protection, use or disclosure of Personal Information, Pearson will provide notice of the change through a conspicuous notice on this site or other appropriate way. Continued use of the site after the effective date of a posted revision evidences acceptance. Please contact us if you have questions or concerns about the Privacy Notice or any objection to any revisions.

Last Update: November 17, 2020