Adding Briefcase Support (.NET)

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.

A note on data access: Briefcases store DataTable objects, so briefcase support is built on the DataSet-based data access (the RemoteDataAdapter) rather than on the strongly typed LINQ adapter used earlier in the ToDo List tutorial. The DataModule shown below keeps its data in a DataSet and works with the tables by name. 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, Products and ProductGroups. When you add briefcase support to your own application you simply use your own table names (in the ToDo List app those would be Priorities and Tasks).

The whole briefcase mechanism lives in the DataModule class, so the rest of your application does not need to change.

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 to the briefcase by calling AddTable:

private void PutTablesToBriefcase(Briefcase briefcase)
{
    this.AddToLog("Putting tables to the briefcase...");
    briefcase.AddTable(this.dataSet.Tables["Products"]);
    briefcase.AddTable(this.dataSet.Tables["ProductGroups"]);
}

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

public void SaveBriefcase()
{
    Briefcase briefcase = new FolderBriefcase(this.BriefcaseFileName);

    if (this.InvalidateBriefcase)
    {
        String v = this.SafeGetBriefcaseProperty(briefcase, BRIEFCASE_DATA_VERSION_KEY);
        if (String.Equals(BRIEFCASE_DATA_VERSION_INVALID, v, StringComparison.OrdinalIgnoreCase))
        {
            this.AddToLog("Briefcase already marked as INVALID.");
        }
        else
        {
            briefcase.Properties[BRIEFCASE_DATA_VERSION_KEY] = BRIEFCASE_DATA_VERSION_INVALID;
            this.AddToLog("Briefcase has been marked as INVALID.");
            briefcase.WriteBriefcase();
        }
    }
    else
    {
        this.PutTablesToBriefcase(briefcase);
        briefcase.Properties[BRIEFCASE_DATA_VERSION_KEY] = BRIEFCASE_DATA_VERSION;
        briefcase.Properties[BRIEFCASE_UPDATE_DATE_KEY] = DateTime.Now.ToString();
        briefcase.WriteBriefcase();
        this.AddToLog("Briefcase has been updated.");
    }
}

The data version is important. Every briefcase stores a set of string properties (briefcase.Properties), 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:

public void LoadFromServer()
{
    this.AddToLog("Downloading data from the server.");
    this.remoteDataAdapter.Fill(_dataSet, new string[] { "ProductGroups", "Products" });
    this.AddToLog("Data has been downloaded from the server.");
    this.SaveBriefcase();
}

Loading Data Back from a Briefcase

When data is read back from the briefcase we extract the same tables by name using FindTable, and add them to the module's DataSet:

private void GetTablesFromBriefcase(Briefcase briefcase)
{
    this.AddToLog("Loading tables from the briefcase...");

    this.dataSet.Tables.Clear();
    this.dataSet.Tables.Add(briefcase.FindTable("Products"));
    this.dataSet.Tables.Add(briefcase.FindTable("ProductGroups"));
}

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 exposed by the BriefcaseFileName property. The sample stores the briefcase next to the application executable:

public String BriefcaseFileName
{
    get
    {
        return Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), BRIEFCASE_NAME);
    }
}

What happens when loading

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

public void LoadData()
{
    this.AddToLog("Initial loading...");
    this.AddToLog("Looking for the local briefcase first...");

    String briefcaseName = this.BriefcaseFileName;
    this.AddToLog(String.Format("Briefcase: {0}", briefcaseName));
    if (!System.IO.Directory.Exists(briefcaseName))
    {
        this.AddToLog("No briefcase found. Downloading fresh data...");
        this.LoadFromServer();
    }
    else
    {
        this.AddToLog("Briefcase has been found.");
        Briefcase briefcase = new FolderBriefcase(briefcaseName);
        briefcase.ReadBriefcase();
        Boolean valid = this.ValidateBriefcase(briefcase);
        if (valid)
        {
            this.GetTablesFromBriefcase(briefcase);
        }
        else
        {
            this.AddToLog("Clearing briefcase and downloading fresh data...");
            this.LoadFromServer();
        }
    }
    this.AddToLog("Data is Ready.");
}

If a briefcase is found, a FolderBriefcase is created for it and ReadBriefcase loads its contents. ValidateBriefcase then compares the version stored in the briefcase against the version the application expects:

private Boolean ValidateBriefcase(Briefcase briefcase)
{
    this.AddToLog("Validating briefcase...");

    String briefcaseVersion = this.SafeGetBriefcaseProperty(briefcase, BRIEFCASE_DATA_VERSION_KEY);
    String appVersion = BRIEFCASE_DATA_VERSION;
    Boolean isValid = String.Equals(briefcaseVersion, appVersion, StringComparison.OrdinalIgnoreCase);
    // ...
    return isValid;
}

When the versions match, GetTablesFromBriefcase pulls the tables out of 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 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 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, created with FolderBriefcase:

Briefcase briefcase = new FolderBriefcase(this.BriefcaseFileName);

To use a single file instead, create the briefcase with FileBriefcase:

Briefcase briefcase = new FileBriefcase(this.BriefcaseFileName);

Both classes derive from Briefcase, so the rest of the code – AddTable, FindTable, Properties, ReadBriefcase and WriteBriefcase – stays exactly the same.