Adding Briefcase Support (Delphi)

In the previous tutorial you built a ToDo List application that pulls its data from a Relativity Server instance and passes any changes you make straight back to the server.

Nothing is persisted locally, so every time the user launches the app it has to fetch the data from the server again. If the network connection or the server is unreachable the user is left with an empty application, which is usually not acceptable.

In this tutorial we will add local storage using a feature of Data Abstract we call Briefcase support, and then look at how and where the data is saved.

What is Briefcase Support?

A briefcase is a local snapshot of the data that your application can use when it either cannot or should not connect to the Relativity Server. Storing data locally serves a few purposes:

  1. After the first run the app can start faster, because it loads the local data without having to talk to the server first.

  2. The application can ask the server only for the records that have changed and then merge that delta with the tables loaded from the local briefcase.

  3. The application can be used without a connection to the server – for example when the user is out of reach of Wi-Fi or a cellular network. Any changes the user makes are kept locally and, thanks to Data Abstract's conflict resolution, can be sent to the server later.

In Delphi a briefcase works directly with the TDAMemDataTable components that your DataModule already uses, so briefcase support drops in cleanly. The complete, runnable version of this code ships with Data Abstract as the Briefcase sample; the snippets on this page are taken from that sample, which loads two tables, tbl_Products and tbl_ProductGroups. When you add briefcase support to your own application you simply use your own tables (in the ToDo List app those would be the Priorities and Tasks tables).

The briefcase classes live in the uDABriefcase unit, so add it to your DataModule's uses clause.

Saving Data to a Briefcase

Saving happens in two small steps: first you say which tables should go into the briefcase, then you write the briefcase to disk.

The PutTablesToBriefcase method adds each table we want to persist by calling AddTable:

procedure TClientDataModule.PutTablesToBriefcase(briefcase: TDABaseBriefcase);
begin
  AddToLog('Putting tables to the briefcase...');
  briefcase.AddTable(tbl_ProductGroups, False);
  briefcase.AddTable(tbl_Products, False);
end;

SaveBriefcase creates the briefcase, calls PutTablesToBriefcase, stamps it with a data version and finally writes it out with WriteBriefcase:

procedure TClientDataModule.SaveBriefcase;
var
  lbriefcase: TDABaseBriefcase;
begin
  lbriefcase := TDAFolderBriefcase.Create(GetBriefcaseFileName,False,False);
  try
    if fInvalidateBriefcase then begin
      if AnsiSameText(BRIEFCASE_DATA_VERSION_INVALID, lbriefcase.PropValues[BRIEFCASE_DATA_VERSION_KEY]) then begin
        AddToLog('Briefcase already marked as INVALID.')
      end
      else
      begin
        lbriefcase.PropValues[BRIEFCASE_DATA_VERSION_KEY] := BRIEFCASE_DATA_VERSION_INVALID;
        AddToLog('Briefcase has been marked as INVALID.');
        lbriefcase.WriteBriefcase;
      end
    end
    else begin
      PutTablesToBriefcase(lbriefcase);
      lbriefcase.PropValues[BRIEFCASE_DATA_VERSION_KEY] := BRIEFCASE_DATA_VERSION;
      lbriefcase.PropValues[BRIEFCASE_UPDATE_DATE_KEY] := DateTimeToStr(Now);
      lbriefcase.WriteBriefcase;
      AddToLog('Briefcase has been updated.')
    end;
  finally
    lbriefcase.Free;
  end;
end;

The data version is important. Every briefcase stores a set of string properties (briefcase.PropValues), and here we write our own application version under BRIEFCASE_DATA_VERSION_KEY. When the app later loads the briefcase it compares that value against the version the application expects, and only uses the local data if the two match.

This gives you a simple upgrade path: whenever you change the database or the shape of the data in a way that would clash with what a user already has stored, just bump BRIEFCASE_DATA_VERSION. The next time the application runs it sees the mismatch, ignores the outdated briefcase and downloads fresh data from the server instead.

Data is downloaded from the server – and saved to the briefcase – in LoadFromServer:

procedure TClientDataModule.LoadFromServer;
begin
  AddToLog('Downloading data from the server.');
  tbl_ProductGroups.Close;
  tbl_Products.Close;
  RemoteDataAdapter.Fill([tbl_ProductGroups,tbl_Products]);
  AddToLog('Data has been downloaded from the server.');
  SaveBriefcase;
end;

Loading Data Back from a Briefcase

Reading the data back is a single call: Fill populates the listed tables straight from the briefcase.

procedure TClientDataModule.GetTablesFromBriefcase(briefcase: TDABaseBriefcase);
begin
  AddToLog('Loading tables from the briefcase...');

  briefcase.Fill([tbl_ProductGroups,tbl_Products],True);
end;

That is the whole idea. To see it in action, run the app once so the data is downloaded and saved to the briefcase, then stop Relativity Server and start the app again. It still launches and shows the data, because it was loaded from the local briefcase rather than the server.

Exploring Saving and Loading

The DataModule ties the pieces above together. This section walks through the mechanics so you understand what is going on.

Where the briefcase is stored

The location is returned by GetBriefcaseFileName. The sample stores the briefcase next to the application executable:

function TClientDataModule.GetBriefcaseFileName: string;
begin
  Result := ExtractFilePath(ParamStr(0))+ BRIEFCASE_NAME;
end;

What happens when loading

When the app starts it calls LoadData. This first checks whether a briefcase folder already exists; if it does not, the data is downloaded from the server:

procedure TClientDataModule.LoadData;
var
  lbriefcaseName: string;
  lbriefcase: TDAFolderBriefcase;
begin
  AddToLog('Initial loading...');
  AddToLog('Looking for the local briefcase first...');
  lbriefcaseName := GetBriefcaseFileName;
  AddToLog('Briefcase: '+ lbriefcaseName);

  if not DirectoryExists(lbriefcaseName) then
  begin
    AddToLog('No briefcase found. Downloading fresh data...');
    LoadFromServer;
  end
  else begin
    AddToLog('Briefcase has been found.');
    lbriefcase:= TDAFolderBriefcase.Create(lbriefcaseName,True,True);
    try
      if ValidateBriefcase(lbriefcase) then begin
        GetTablesFromBriefcase(lbriefcase);
      end
      else begin
        AddToLog('Clearing briefcase and downloading fresh data...');
        LoadFromServer;
      end;
    finally
      lbriefcase.Free;
    end;
  end;
  AddToLog('Data is ready.')
end;

If a briefcase is found, a TDAFolderBriefcase is created for it (the second and third True arguments tell it to read the data and the properties). ValidateBriefcase then compares the version stored in the briefcase against the version the application expects:

function TClientDataModule.ValidateBriefcase(
  briefcase: TDABaseBriefcase): Boolean;
var
  lbriefcaseVersion: string;
begin
  AddToLog('Validating briefcase ...');

  lbriefcaseVersion := briefcase.PropValues[BRIEFCASE_DATA_VERSION_KEY];
  Result := AnsiSameText(lbriefcaseVersion,BRIEFCASE_DATA_VERSION);
  // ...
end;

When the versions match, GetTablesFromBriefcase fills the tables from the local data and the server is never contacted. When they do not match, the briefcase is ignored and LoadFromServer downloads fresh data instead.

What happens when saving

SaveBriefcase (shown earlier) is called after data is downloaded from the server, and again when the user applies pending changes back to the server in SaveToServer. A good place to call it as well is when the application is closing, so any changes made during the session are kept locally.

Briefcase Types

Briefcases can be stored in two formats: as a folder or as a single file.

  • A briefcase folder (TDAFolderBriefcase) is stored as a package, with each table saved as a separate file. Individual tables can be read, written or added independently and more efficiently, without rewriting all the data.
  • A briefcase file (TDAFileBriefcase) keeps all the tables in one self-contained file (with the .daBriefcase extension), which is easier to hand around if your app is designed for the user to deal with briefcases as files.

Which one you use is largely a matter of taste. The sample – and the code above – uses a folder briefcase:

lbriefcase := TDAFolderBriefcase.Create(GetBriefcaseFileName, False, False);

To use a single file instead, create a TDAFileBriefcase:

lbriefcase := TDAFileBriefcase.Create(GetBriefcaseFileName, False, False);

Both classes derive from TDABaseBriefcase, so the rest of the code – AddTable, Fill, PropValues and WriteBriefcase – stays exactly the same.