ECO Example: Backup Utility

By: Wayne Niddery

Abstract: Created as an ECO learning vehicle for myself, it acts as a practical example demonstrating various abilities of ECO and that, even for such a modest project, ECO is an advantage and should never be considered “overkill”.

As part of learning to use ECO, I decided to create something I need anyway. As an independent developer I work on different projects over time and often at the same time. To protect myself and my clients, I like to back up my work to an offsite location.

Now there are certainly lots of existing backup products of various capabilities, and it’s even possible to use WinZip and batch files. But I decided I wanted something easy to use and specific for my needs, and it would be a good exercise.

In developing it, I also answered for myself a frequent question I hear: Isn’t ECO overkill for small projects? My answer is no. Despite this being a very modest project, I saved a great deal of time by not having to define relationships in code, manage lists of objects, and design and manage some kind of storage mechanism.

So first let’s define the requirements.

  1. We want to be able to define any number of backup “projects”, typically these would be one on one for each development project, but can be anything.
  2. Under each project, I want to define any number of folder specifications, and for each of those, indicate whether to recurse into its sub-folders.
  3. Also at the project level, I want to define any number of file masks. For development projects, this is of course going to typically include *.pas, *.dfm (or *.nfm), etc. but of course a backup project can include anything at all.
  4. The project will also need some information about where to store the backups it generates and what to name them. And of course we’re going to want to have some visible feedback as it does its work.

Note: for the ability to zip up the selected files I used open source library called SharpZipLib available from http://www.icsharpcode.net/OpenSource/SharpZipLib/. This library is distributed under a modified GPL license that allows the library to be used by executables without requiring the executable to be GPL.

Let’s start. Create a new ECO Winform Application project. Save the project with the name ECOBackup.

We will need persistence in order to store the project definitions we create with our utility, so we will define that first. Due to the nature of this project, simple XML storage will be ideal.

In the Project Manager, find the EcoBackupEcoSpace and open it. In the tool palette, under the Enterprise Core Objects node, there is a PersistenceMapperXML component that can be dropped on our ECO Space.

Keyboard shortcut: Focus the tool palette with Ctrl-Alt-P, then just type “pe” and the palette will display the ECO persistence components. Scroll down to the PersistenceMapperXML and hit Enter.

In the Object Inspector for the PersistenceMapperXML, we need to set a file name. This can just be “ECOBackup.xml”. You can give it a complete path if desired. At this point take a moment to Save All and compile the project. Then open Winform1 from the Project Manager. In the set of components at the bottom will be one called rhRoot. This is a reference handle used to provide access to the model from the form. In the Object Inspector for rhRoot you will see an EcoSpaceType property and you should now be able to click on the dropdown arrow and select the EcoSpace.

    Defining the Model

As mentioned above, this is a tiny model. There are only four classes, and only three of those will be persisted. We’ll start by defining the necessary attributes and associations to get us started, and we’ll add some extra bits to it as needed.

Click on the Model View tab of the project manager and expand the ECOBackup node. There you should see a “Package_1”, open this to get to the model surface.

From the tool palette, drag an ECO class on to the diagram and name it Project. This class will represent a single backup project, so we’ll define attributes needed at that level. The attributes that initially need to be added are a Name for the project, and what name to give the backup file being made. Since the goal is to be able to backup somewhere remotely, we will also need information about the destination computer: ServerName, ServerPath, ServerUserID, and ServerPassword. The Project class also needs two public methods: Preview and Backup.

Keyboard shortcut: Use Ctrl-W to add attributes and Ctrl-M to add operations to a class.

Hide image

According to our requirements, for a Project, we need to be able to define folders on the computer that we want backed up, and whether the backup should process a folder’s sub-folders. So we’ll add a second class to the project called Folder. Folder only needs two attributes: Path (String) and Recurse (Boolean).

In addition to Folder, we need to be able to define file masks to control which files are backed up, so a 3rd class needs to be created which we’ll call FileMask and it will contain just one string attribute named Mask.

Before our project can work, we need to define associations between them. For both the Folder and FileMask classes, there can be one to many of them for each Project, therefore we will add an association to each of these. If you create the associations by first clicking on Project and dragging to the other class, then for both, End 1 will be the Project. For the End 2 property then, we want to change the Multiplicity of each to 0..*. Strictly speaking this is enough, but to make the model better, we should recognize that both Folder and FileMask cannot logically exist apart from a Project, therefore they should be marked Composite in the End 2 Aggregation property. This will give the correct visual representation in the model as well.

With this much defined in the model, we could go ahead and start defining the user interface. However, thinking ahead a bit, in order for Project to do its work, either Backup or Preview, it will need to process its Folders and FileMasks in order to generate a list of files to be backed up – which means we need something to store that information in - another class.

Drop a new class on the diagram and name it “FileEntry”. Add two attributes, FilePath (string) and FileSize (double). Just like Folder and FileMask, we need to add a one-to-many composite association between Project and FileEntry. An important difference between FileEntry and the other classes in our model is that there is no need to store a FileEntry permanently, it is only needed while Project performs its tasks, therefore we need to mark the Persistence property of FileEntry as transient so that ECO will not bother storing them.

Here’s our diagram so far:

Hide image

At this point we are ready to start developing our user interface. We will revisit the model later in order to add some additional details.

    Defining the User Interface

Go back to Winform1. We’ll start by setting up display for Projects and the ability to create or delete them. Drop a DataGrid and a couple of buttons on the form. For the grid, I found a size of roughly 225 wide by 275 high, and placed at the upper left to be about right. Set the CaptionText of the DataGrid to “Projects”. Position the two buttons under it. Name the DataGrid to “ProjectList” and the buttons to “AddProject” and “DeleteProject” respectively.

Here we’ll add our first ECO ExpressionHandle component. Name it to “ProjectsHandle” and set its RootHandle to rhRoot in order to connect it to the EcoSpace. The Expression property needs to be set to Project.allInstances, this can just be typed in or you can click on the ellipsis and use the expert to help you – selecting Project from the class node then allInstances from the Ocl Operations node.

Now we can connect the DataGrid and buttons. Set the DataSource property of the DataGrid to the new ProjectsHandle. For the buttons you will see a property called RootHandle on EcoListActions, set this also to the ProjectsHandle. You can now set the actions for these buttons. Since they will be operating on the list of Projects, find the EcoListAction on EcoListActions property and set this to Add and Delete respectively. ECO even changes the button captions for you. Finally, for the Delete button, we want to disable it if there are no projects to delete. This can be done using the property EnabledOcl on EcoListActions, set it to self->notEmpty. Because this button is tied to the ProjectsHandle expression handle, “self” in this OCL statement refers to the ProjectsHandle.

Drop one more button for now and place it somewhere below the Add and Delete buttons, name this “SaveButton”. Find the EcoAction on EcoGlobalActions property and set it to UpdateDatabase.

While not very pretty yet, you can now compile and run this project and you will be able to add and delete projects and edit them in the grid. You can click the Update DB button to save the project to file, and as you can see, the Delete button will correctly be disabled whenever there are no projects in the list.

It’s never too early to start making the UI nicer. I prefer to edit records in individual controls instead of grids. Since we will also need to manage other classes as well, we’ll use a Tab Control to divide up the functionality. Drop one down to the right and set its Dock property to Right, then size it to take up all the space right of the Datagrid. Add three tabs and label them “Project”, “Folders”, and “File Masks”, respectively.

On the Projects tab, we need an edit control for each of the six current Project attributes and corresponding labels. Now we could hook these edit controls directly to the ProjectsHandle handle and everything would work, however we will soon need to access the current project in code anyway and to do that we will need a CurrencyManager, therefore we’ll do that now.

.Net binding is designed to allow any list type component (DataGrid, Listbox) link to many kinds of data containers whether a DataSet, DataView, ArrayList, or an ECO ExpressionHandle. However, the interface that allows this has no concept of a current position in the list. This is done separately via a BindingContext. The ECO CurrencyManager wraps this functionality.

Drop a CurrencyManager on the form and name it to “ProjectsCM”. Set its RootHandle to ProjectsHandle and its BindingContext to the ProjectList Datagrid. We’ll now be able to use this CurrencyManager to hook up to the currently selected Project.

For each of the edit controls, in the DataBindings.Text property, you’ll now be able to select the desired attribute from the ProjectsCM handle. Don’t forget to set the PasswordChar property of the edit control hooked to the ServerPassword attribute!

Now that we have separate edit controls hooked up, we don’t need the Datagrid to be editable, or to show all attributes. The first thing to do is set its ReadOnly flag to True. Now click the ellipsis of the TableStyles property and in the dialog add a TableStyle. Click its GridColumStyles property ellipsis. Here you can add a Member, set its HeaderText to “Name” and select the Name attribute in the MappingName property. You can also set the column width here.

Here’s our form so far:

Hide image

Now let’s add functionality for Folders and File Masks.

On the Folders tab we need another Datagrid and a couple of buttons to add and delete folders. These can be laid out any way desired, each line of the grid will display a folder path. For the DataGrid, you can turn off the CaptionVisible property since we already know we’re on the Folders tab.

To power these controls, we need a new ExpressionHandle. Drop one down and name it “FoldersHandle”. Since we want this handle to present the set of folders linked to the currently selected Project, we need to set this expression’s RootHandle to the CurrencyManager, ProjectsCM. This is very much like creating a master/detail link between two datasets in Win32 Delphi applications. The expression property needs to access the list of folders. Since the FoldersHandle is tied to the current Project, this means that “self”, specified in the Expression property, will refer to that Project object. Because of the association between Project and Folder in the model, there will be a Role available in the Project object called Folders. Thus the Expression property needs to be set to self.Folders.

Now the grid can be hooked to the FoldersHandle via its DataSource property. On doing this, the grid will display all three members – the 3rd being the Project this object belongs to. We don’t need the Project to display here. This is solved simply by adding a TableStyle to the grid, adding a GridColumnStyle to that and adding two columns, setting these to the Path and Recurse attributes respectively. Set the column widths while there.

For the Delete button, set the RootHandle on EcoListActions to the FolderHandle, the EcoListAction on EcoListActions to Delete, and the Enabled on EcoListActions to self->not Empty.

For the Add button, we can’t use the EcoAction property because we need to prompt the user to enter a folder path, so we need to write our first code here!

Label the Add button appropriately, and then double click the Add button’s Click event. The following code will do what we need. After typing in this code, you will notice that LastBrowsePath has not been declared. We can use the Declare Field refactoring feature to do this (from the menu or Shift-Ctrl-D), set the type to string. This variable is not necessary, but simply adds a nice touch – each time the user selects a folder, it will start the browse dialog in the location that was last selected from.

var f: Folder;
    p: Project;
    dlg: FolderBrowserDialog;
begin
  dlg := FolderBrowserDialog.Create;
  try
    dlg.ShowNewFolderButton := False;
    dlg.SelectedPath := LastBrowsePath;
    if dlg.ShowDialog(Self) = System.Windows.Forms.DialogResult.OK  then
    begin
      f := Folder.Create(EcoSpace);
      f.Path := dlg.SelectedPath;
      p := ProjectsCM.CurrentElement(ProjectList).AsObject as Project;
      p.Folders.Add(f);
      LastBrowsePath := dlg.SelectedPath;
    end;
  finally
    dlg.Free;
  end;

The above code presents a folder browser to the user to allow a selection and, if selected, adds a new Folder object to the Project. The try/finally block isn’t really needed here since it will be garbage-collected anyway, but good habits die hard.

As you can see, creating a new object in code and adding it as a member of another object’s list is very simple in ECO. The only line that looks weird is the one getting access to the current Project object. We use the CurrencyManager object to get the current element from the ProjectList Datagrid. We need to use the AsObject property of that element to get an actual Project object, and finally it needs to be cast as a Project object in order to assign it to our variable.

The Folders tab is now functional, the project can be run and tested. The grid is left editable so the Recurse attribute can be edited to True or False (this could be enhanced to be done with a button or checkbox, to do so would also require another CurrencyManager for the Folders list). Different folders can be assigned to different projects and all is saved properly.

Hide image
Click to see full-sized image

The File Masks tab will be similar, but here we’ll just use a Listbox instead of a DataGrid. Again we need two buttons to add and delete File Masks, and we need a TextBox to allow entry of masks. Lay these out as desired, the ListBox does not need to be too wide.

Once again we need an ExpressionHandle, drop one down and name this one “MasksHandle”. Again its RootHandle will be ProjectsCM and the expression will be self.FileMasks.

For the Listbox, name it “MaskList” and set its Datasource to the MasksHandle. In addition, set its DisplayMember property to the Mask attribute. You can optionally set Sorted to True. Name the textbox to “MaskText”.

Using the same pattern as described for the Delete button on the Folder tab, the delete button for File Masks can be hooked up also using the MasksHandle.

Again like the Add button on the folders tab, set its caption and then create a Click event for it. The code needed for adding a File Mask is as follows:

var fm: FileMask;
    p: Project;
begin
  if MaskText.Text.Trim = '' then
  begin
    MessageBox.Show('Please enter file mask specification');
    Exit;
  end;
  if MaskList.FindStringExact(MaskText.Text) >= 0 then
  begin
    MessageBox.Show('File mask already added');
    Exit;
  end;
  fm := FileMask.Create(EcoSpace);
  fm.Mask := MaskText.Text;
  p := ProjectsCM.CurrentElement(ProjectList).AsObject as Project;
  p.FileMasks.Add(fm);
end;

Most of the above code is merely checking that there is something entered and that it isn’t duplicating an already added mask. Then it simply creates a new FileMask object, sets its Mask attribute, and adds it to the current Project object.

Hide image
Click to see full-sized image

At this point we can completely create and configure backup projects.

    Adding the Preview Feature

One of our main requirements was to be able to preview a backup as well as execute it and we have endowed our Project class with a Preview method. Now it’s time to implement it.

We need a place to display the preview – the list of actual files that would be backed up by the Backup method. We can add another tab to the Tab control and label it Preview. To this, add a DataGrid named “PreviewGrid”, and a button labeled “Preview”.

Again we need an ExpressionHandle to power this feature, add one and name it “FileEntriesHandle”. The RootHandle will once again be ProjectsCM, and the Expression will be self.FileEntries. Link the Datagrid up by setting the DataSource to FileEntriesHandle. Once again it will show the unneeded Project column in the grid. Get rid of this as before by adding a TableStyle and defining just two columns for the FilePath and FileSize attributes.

Create a Click event for the Preview button. The code needed here is:

var p: Project;
begin
  p := ProjectsCM.CurrentElement(ProjectList).AsObject as Project;
  p.Preview;
end;

As elsewhere, we need to get access to the current Project. Then we call the Preview method of the Project.

Of course we don’t yet have any functionality in the Preview method. We’ll fix this now. Go to the model and right click on the Preview method in the Project class. Here you can click on “Go to Definition”. This takes you to the interface declaration of Preview (actually the attribute line above it). Move down to the Preview method and you can use Ctrl-Shift-DownArrow to move to the implementation section. In this method, you only need type one line:

  BuildEntryList;

Now of course we need to define the BuildEntryList method. We’re placing this in a separate method because our Backup method is also going to need to call this. Following is the BuildEntryList method. Note that you need to add the System.IO namespace to the unit’s Uses clause.

procedure Project.BuildEntryList;
var fld: Folder;
    di: DirectoryInfo;

  // find requested files for passed directory
  procedure ProcessMask(d: DirectoryInfo);
  var mask: FileMask;
      fi: FileInfo;
      files: array of FileInfo;
      fe: FileEntry;
  begin
    // apply each mask in turn
    for mask in FileMasks do
    begin
      files := d.GetFiles(mask.Mask);
      for fi in files do
      begin
        fe := FileEntry.Create(self.AsIObject.ServiceProvider);
        fi.Refresh;
        fe.FileSize := fi.Length;
        fe.FilePath := fi.FullName;
        self.FileEntrys.Add(fe);
      end;
    end;
  end;

  // recurse folders from passed starting point
  procedure RecurseFolders(di: DirectoryInfo);
  var dirs: array of DirectoryInfo;
      di2: DirectoryInfo;
  begin
    dirs := di.GetDirectories;
    for di2 in dirs do
    begin
      ProcessMask(di2);
      RecurseFolders(di2);
    end;
  end;

begin
  FileEntrys.Clear;
  for fld in Folders do
  begin
    di := DirectoryInfo.Create(fld.Path);
    ProcessMask(di);
    if fld.Recurse then
    begin
      RecurseFolders(di);
    end;
  end;
end;

This method makes use of Delphi nested procedures and should make the code easier to understand. The mainline of the method starts by clearing any existing FileEntry objects from the Project. It then loops through the list of Folder classes that have been added by the user for this Project. For each, it calls ProcessMask to find and add any matching files in that folder. It then checks to see if it should recurse sub-folders, and if so, calls RecurseFolders.

RecurseFolders is a recursive procedure. It asks the passed DirectoryInfo object for a list of sub-folders. If any are returned, it loops through them, calling ProcessMask and then calling itself in order to continue the recursion. This will continue for any depth of sub-folders.

ProcessMask loops through each of the FileMask objects the user has added and, for each, asks the passed DirectoryInfo object for a list of matching files (much easier then the equivalent FindFirst/FindNext/FindClose technique needed in Win32!). For each file found, a FileEntry object is created and added to the Project’s FileEntrys list. Note the FileInfo object returned by the system must be refreshed in order for it to correctly report the size of the file.

With this complete, we have some actual functionality, run the application and, with folders and masks defined, go to the Preview tab and click the button!

Hide image
Click to see full-sized image

    Adding the Backup Feature

Before we can add the functionality, first we need to make a reference to the needed zip dll. As noted at the beginning of this article, you need to download and install SharpZipLib. We also need to reference the Indy components in order to use its FTP component. Right click on the References node in the Project Manager and click Add Reference.

Here in the list you should be able to find the needed Indy assemblies, these are IndyCore, IndyProtocols, and IndySystem. Highlight these 3 and click Add Reference.

You can then click the browse button to find the ICSharpCode.SharpZipLib.dll assembly, wherever you installed it.

Clicking OK on this dialog will add the four selected references to the project. You will then also be able to add these to the Uses clause.

Add a new button on the main form under the ProjectList datagrid and label it “Backup”. Name it as “BackupButton” and create a Click event for it. The Click event will contain the following code:

var p: Project;
begin
  BackupButton.Enabled := False;
  try
    p := ProjectsCM.CurrentElement(ProjectList).AsObject as Project;
    // clear the file list display
    p.FileEntrys.Clear;
    if PreviewGrid.Visible then
      PreviewGrid.Update;

    p.Backup;

  finally
    BackupButton.Enabled := True;
  end;

Here we use a try/finally block to disable the Backup button and re-enable it at completion, just to ensure it cannot be clicked again while a backup is in progress. As usual, we get the current Project object and clear out any FileEntry objects it may be holding. To make the display a little cleaner, we also update the PreviewGrid to show it empty prior to starting the backup. It will be regenerated by the backup process. Finally, we call the Backup method.

Find your way to the Project class’ Backup method. In that package unit, add the following references to the Uses clause: ICSharpCode.SharpZipLib.Zip, ICSharpCode.SharpZipLib.Core, IDFTP, and IDComponent.

Now we can add the code for the Backup method.

var fe: FileEntry;
    zip: ZipOutputStream;
    zentry: ZipEntry;
    infile: FileStream;
    memfile: MemoryStream;
    buffer: array of byte;
    ftp: TIdFtp;
begin
  BuildEntryList;

  if BackupName = '' then
    BackupName := BackupName.Format('{0}{1}.zip', self.Name,
      DateTime.Now.ToString('yyyyMMddhhmmss'));

  memfile := MemoryStream.Create;
  zip := ZipOutputStream.Create(memfile);

  // now process the entries
  for fe in FileEntrys do
  begin
    zentry := ZipEntry.Create(fe.FilePath);
    infile := &File.OpenRead(fe.FilePath);
    SetLength(buffer, infile.Length);
    infile.Read(buffer, 0, infile.Length);
    zip.PutNextEntry(zentry);
    zip.Write(buffer, 0, infile.Length);
  end;
  zip.Finish;
  zip.Flush;
  memfile.Position := 0;

  // upload to a server?
  if ServerName <> '' then
  begin // yes
    ftp := TIdFtp.Create;
    try
      ftp.Host := ServerName;
      ftp.Username := ServerUserID;
      ftp.Password := ServerPassword;
      ftp.Connect();
      ftp.ChangeDir(ServerPath);
      ftp.Put(memfile, BackupName);
    finally
      ftp.Free;
    end;
  end else
  begin // save locally
    infile := &File.OpenWrite(ServerPath + '\' + BackupName);
    memfile.WriteTo(infile);
    infile.Close;
  end;

  memfile.Free;
  zip.Close;

The first thing Backup does is call the BuildEntryList method we created earlier. This gives it an up-to-date list of files to be backed up. Then we make sure there’s a name for the backup file, if one was not specified then we generate one combining the Project name with the current timestamp. We then create a zip file using a memory stream and add each of the target files to it. Finally, if server information has been specified, we attempt to FTP the file to the specified location on the server, otherwise we assume the ServerPath attribute is a local file path and attempt to save the zip file to there.

We now have a fully functional backup utility. However, it has one major flaw: there is no feedback of any kind while it’s performing a backup, no way to see its progress. Typically, a backup utility is going to have a progress meter, and usually displays the file names as they are processed, so we’ll do that.

    Feedback

We’d like to get feedback from our Project object as it’s performing its work. However, under no circumstances do we want the model to know anything about our user interface, thus it cannot directly access our form. In order to get feedback, we need to observe our Project object. ECO implements the Observer (also know as Publish and Subscribe) pattern in all ECO classes you define and provides the necessary methods to let you set up such subscriptions. You can observe an object or a specific attribute (property) of an object.

We will add some new attributes to our Project class specifically to provide information about its progress. The attributes we will add are as follows:

BackupSize: Int64

ProgressPoint: Integer

ProgressState: string

Because these are only intended for use while the Project object is performing a backup, we do not want these attributes to be stored when saved to file. Therefore be sure to set the Persistence property of all three of these to Transient.

Now we need to add some code to make use of these attributes. Navigate to the BuildEntryList method and as the first line of its (main) code, add:

  ProgressState := 'Preparing File List';

We will want to report the status of the FTP process, so we need to provide a couple of event handlers to hook to. These will look as follows, use code-completion after entering the code in order to declare them in the class:

procedure Project.FTPStatus(ASender: TObject; const AStatus: TIDStatus; 
const AStatusText: string);
begin
  ProgressState := AStatusText;
end;

procedure Project.FTPWork(ASender: TObject; AWorkMode: TWorkMode; 
AWorkCount: integer);
begin
  ProgressState := 'Writing ' + AWorkCount.ToString('N0') + ' of '
    + BackupSize.ToString('N0');
end;

In the Backup method, there are a number of additional lines to be added, rather than listing each one, here is the complete backup method again with the new lines in bold.

procedure Project.Backup;
var fe: FileEntry;
    zip: ZipOutputStream;
    zentry: ZipEntry;
    infile: FileStream;
    memfile: MemoryStream;
    buffer: array of byte;
    ftp: TIdFtp;
begin
  BuildEntryList;

  if BackupName = '' then
    BackupName := BackupName.Format('{0}{1}.zip', self.Name,
      DateTime.Now.ToString('yyyyMMddhhmmss'));
  memfile := MemoryStream.Create;
  zip := ZipOutputStream.Create(memfile);

  ProgressState := 'Compressing Files';
  ProgressPoint := -1; // tell subscribers to initialize
  ProgressPoint := 0; // start

  // now process the entries
  for fe in FileEntrys do
  begin
    ProgressState := fe.FilePath;
    zentry := ZipEntry.Create(fe.FilePath);
    infile := &File.OpenRead(fe.FilePath);
    SetLength(buffer, infile.Length);
    infile.Read(buffer, 0, infile.Length);
    zip.PutNextEntry(zentry);
    zip.Write(buffer, 0, infile.Length);
    ProgressPoint := ProgressPoint + 1;
  end;
  zip.Finish;
  zip.Flush;

  BackupSize := zip.Length;
  ProgressState := 'Writing Backup File';

  memfile.Position := 0;
  // upload to a server?
  if ServerName <> '' then
  begin // yes
    ftp := TIdFtp.Create;
    try
      ftp.Host := ServerName;
      ftp.Username := ServerUserID;
      ftp.Password := ServerPassword;
      ftp.OnStatus := FTPStatus;
      ftp.OnWork := FTPWork;
      ftp.Connect();
      ftp.ChangeDir(ServerPath);
      ftp.Put(memfile, BackupName);
    finally
      ftp.Free;
    end;
  end else
  begin // save locally
    infile := &File.OpenWrite(ServerPath + '\' + BackupName);
    memfile.WriteTo(infile);
    infile.Close;
  end;

  memfile.Free;
  zip.Close;
  ProgressState := 'Backup complete';
end;

To take advantage of subscriptions, the observer must implement the ISubscriber interface. ISubscriber is defined in the Borland.Eco.Subscription namespace, so add this to the Uses clause of the form. We can then alter our form to implement the interface by changing the first line of the form definition to be:

TWinForm1 = class(System.Windows.Forms.Form, ISubscriber)

The ISubscriber interface requires two methods to be implemented, these are IsAlive and Receive. The IDE can help here, just scroll down to the bottom of the TWinform1 class and, in the public section, hit Ctrl-Space, Delphi will show you a list of overrideable methods with IsAlive and Receive at the top and in red. Highlight both of these (the list allows multi-select) and hit return, then Shift-Ctrl-C to create the implementation stubs.

The IsAlive method needs only one line – set the Result to True to indicate you want the subscription to continue.

Before implementing the Receive method, let’s set up the subscriptions. These will go in the click event of the Backup button, just before the call to Backup:

    p.AsIObject.Properties['ProgressState'].SubscribeToValue(self);
    p.AsIObject.Properties['ProgressPoint'].SubscribeToValue(self);

Underlying our Project (and other) objects, is an ECO object that implements the plumbing ECO needs to do its work for us. The AsIObject property gives us access to that underlying object and the Properties property gives access to the plumbing for our individual properties. The ability to subscribe exists at that level. The above lines set up a subscription for our form (self) on two of the attributes we added. When passing self, we are actually passing the ISubscriber interface we’ve implemented, and so ECO will now be able to call our Receive method whenever either of these Project attributes changes.

We now need to add a couple of items to our user interface to display progress, a label and a progress gauge. Name them “ProgressLabel” and “ProgressBar” respectively.

Below is the code for the Receive method. The first line gets the actual attribute that has changed. When the type of the object is a string, it must be ProgressState that has changed, if an integer, then it must be ProgressPoint. If an integer, we check if it is -1, the signal from Project that it is about to start the counting, this gives us an opportunity to initialize our progress gauge.

var o: TObject;
    p: Project;
begin
  o := (ElementChangedEventArgs(e).Element.AsObject);
  if o is string then
  begin
    ProgressLabel.Text := o.ToString;
    ProgressLabel.Refresh;
  end
  else if o is Integer then
  begin
    if Integer(o) = -1 then
    begin
      p := ProjectsCM.CurrentElement(ProjectList).AsObject as Project;
      ProgressBar.Value := 0;
      ProgressBar.Maximum := p.FileEntrys.Count;
    end else
    if Integer(o) <= ProgressBar.Maximum then
    begin
      ProgressBar.Value := Integer(o);
    end;
    ProgressBar.Refresh;
  end;

  Result := True;
end;

You should now be able to compile and run the application and, pressing the backup button on a project, you should see visible feedback.

    Doing it better…

This way of doing things is acceptable for our purposes; however, if your application needs to set up subscriptions to many different classes or attributes, then trying to sort it all out in a single Receive method is going to get ugly very quickly and becomes almost impossible as soon as you need to receive two attributes of the same type.

Fortunately, because subscriptions are done using interfaces, we can easily create a separate class to handle individual subscriptions, create as many of these as we need, and use event handlers to get the subscription events into our main form for processing. Because this class will be handy for any project needing such subscriptions, we’ll put it in its own unit. Create a new code-only unit (File | New | Other | Delphi for .Net Projects | New Files | Unit). Save this file as “ECOSubscribe.pas”. The unit should look like this:

unit ECOSubscribe;

interface

uses
  Borland.Eco.Subscription;

type
  TECOSubscriber = class(TObject, ISubscriber)
  private
    FOnReceive: EventHandler;
  published
  public
    function IsAlive: Boolean;
    function Receive(sender: TObject; e: EventArgs): Boolean;
    procedure set_OnReceive(const Value: EventHandler);
    property OnReceive: EventHandler add FOnReceive remove set_OnReceive;
  end;


implementation

{ TECOSubscriber }

function TECOSubscriber.IsAlive: Boolean;
begin
  Result := True;
end;

function TECOSubscriber.Receive(sender: TObject; e: EventArgs): Boolean;
begin
  if Assigned(FOnReceive) then
    FOnReceive(Self, e);
end;

procedure TECOSubscriber.set_OnReceive(const Value: EventHandler);
begin
  FOnReceive := Value;
end;

end.

This class acts as a relay, it is able to receive calls from a subscribed object and pass that call onto whoever attaches to its OnReceive event. We can create an instance of this class for each subscription we wish to make, and attach a different event handler to each one.

We can now improve our implementation in the main form. Add ECOSubscribe to the main form’s Uses clause. In the main form’s private section, add the following declarations:

    FProgressState: TECOSubscriber;
    FProgressPoint: TECOSubscriber;
    procedure ProgressStateChanged(sender: TObject; e: EventArgs);
    procedure ProgressPointChanged(sender: TObject; e: EventArgs);

Complete the procedures with Shift-Ctrl-C and add code to the implementations so they look as follows:

procedure TWinForm1.ProgressStateChanged(sender: TObject; e: EventArgs);
begin
  ProgressLabel.Text := 
    (ElementChangedEventArgs(e).Element.AsObject).ToString;
end;

procedure TWinForm1.ProgressPointChanged(sender: TObject; e: EventArgs);
var p: Project;
    point: integer;
begin
  point := Integer(ElementChangedEventArgs(e).Element.AsObject);
  if point = -1 then
  begin
    p := ProjectsCM.CurrentElement(ProjectList).AsObject as Project;
    ProgressBar.Value := 0;
    ProgressBar.Maximum := p.FileEntrys.Count;
  end else
  if point <= ProgressBar.Maximum then
    ProgressBar.Value := point;
end;

This is much nicer than our catch-all Receive method since we don’t have to guess what object is being sent or figure out how to distinguish from, say, two different string attributes.

We need to create instances of our ECOSubscribers and attach our event handlers to them. This is added to the main form’s Create event following the line setting the EcoSpace active:

  FProgressState := TECOSubscriber.Create;
  Include(FProgressState.OnReceive, ProgressStateChanged);
  FProgressPoint := TECOSubscriber.Create;
  Include(FProgressPoint.OnReceive, ProgressPointChanged);

Now we can change our subscriptions to use the ECOSubscribers instead of the form directly (this is in the Backup button’s Click event):

    p.AsIObject.Properties['ProgressState'].SubscribeToValue(FProgressState);
    p.AsIObject.Properties['ProgressPoint'].SubscribeToValue(FProgressPoint);

Finally, with that complete, we can remove Winform1.IsAlive and Winform1.Receive, and remove ISubscriber from the Winform1 class declaration.

You should be able to recompile and successfully run the project with the feedback working as before.

    Spit and Polish

While out application is now pretty complete, there are still a number of improvements that can be made. These improvements show additional ECO features and abilities.

    Focusing New Projects

The first thing we’ll correct is an annoyance: when you create a new Project, that Project does not become current, whatever Project was selected previously remains current. This is simply the way the controls work in .Net. To fix it, we need to attach to an event provided by the IBindingList interface of the ProjectsList datagrid and in that event handler, make the new Project current.

To do this, manually add the following event handler then use Shift-Ctrl-C to complete it (this assumes you have not renamed the default form name from WinForm1, if so then edit it appropriately):

procedure TWinForm1.ProjectsChanged(sender: TObject; e: ListChangedEventArgs);
begin
  // if new project added set focus to it
  if e.ListChangedType = ListChangedType.ItemAdded then
    ProjectsCM.Position := ProjectsHandle.GetList.Count - 1;
end;

Find the Load event for the form in the Object Inspector and create a handler for it. In that handler, add the following code:

  Include((ProjectsHandle as IBindingList).ListChanged, ProjectsChanged);

One last detail here: go into the TableStyle of the ProjectsList and set the column definition to be Read-only. Without this, it seems the first time a row is added, the ItemAdded event does not happen (it gets a Reset event instead).

Now when a new Project is added, it will become the current Project as well, ready to edit.

While we’re on new Projects, we can add a minor improvement: instead of leaving the Project Name blank, we can default it to something. To do this, return to the ECO model diagram. Click on the Name attribute of the Project class, and in the Object Inspector, set the Initial property to “<New Project>”. You can define default values for any of a class’ attributes this way.

    Auto-saving on Exit

It can be frustrating to set up a backup project, and then forget to click the Update DB button before exiting, so let’s check for any unsaved changes. The form has a Closing event, create an event handler for it. Here’s the body:

var res: System.Windows.Forms.DialogResult;
begin
  if EcoSpace.DirtyListService.HasDirtyObjects then
  begin
    res := MessageBox.Show('There are unsaved changes.'#10#13
      + 'Press Yes to save them, No to exit without saving, '
      + 'or Cancel to continue editing.', 'Unsaved Changes',
      MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question);
    if res = System.Windows.Forms.DialogResult.Yes then
      EcoSpace.UpdateDatabase
    else if res = System.Windows.Forms.DialogResult.Cancel then
      e.Cancel := True;
  end;
end;

    Allowing Sorting in the Preview Grid

To do this we need to determine if the user has clicked on a column header and, if so, which one. Then we need to manually change the OCL Expression of the FileEntries expression handle. Create an event handler for the PreviewGrid’s MouseUp event that looks like this:

var hit: DataGrid.HitTestInfo;
    fld: string;
begin
  hit := PreviewGrid.HitTest(e.X,e.Y);
  if hit.&Type = DataGrid.HitTestType.ColumnHeader then
  begin
    case hit.Column of
      0: fld := 'FilePath';
      1: fld := 'FileSize';
    end;
    if FileEntriesHandle.Expression.StartsWith('self.FileEntrys->orderBy') then
      FileEntriesHandle.Expression := 
  fld.Format('self.FileEntrys->orderDescending({0})', fld)
    else
      FileEntriesHandle.Expression :=
  fld.Format('self.FileEntrys->orderBy({0})', fld);
  end;
end;

    Adding a Last Backup Date

It would be convenient to be able to see when a project was last backed up, so we’ll add a new attribute to the Project called LastBackup (DateTime). Open the model diagram and add this to the Project definition. Save and compile the application.

Now add a new column to the TableStyle in the ProjectList datagrid. You should be able to map this to the new LastBackup field. Set this new column to Read-only. Set the header and adjust the size of both columns.

At the end of the Project.Backup method, add the following line:

   LastBackup := DateTime.Now;

    Final Words

There are certainly other improvements you can add to this project. For example, it is possible exceptions could be raised when attempting to read a file requested for backup (perhaps it is locked or you do not have permissions for that file). One way reporting for this could be handled would be to add a Boolean Error attribute to the FileEntry class along with an ErrorText string and, if there is an error while processing this file, set these attributes. On completion of the preview or backup, you can now query the FileEntry objects for those that have an error using another Expression Handle and display these in another grid.

The complete project can be downloaded from http://cc.borland.com/Item.aspx?id=24127.

Hopefully this article gives you a taste of how ECO can be an advantage even for small projects such as this.

Server Response from: ETNASC03