Introducing Clarion FileManager

Hi all, I have written a two part series of exercises to introduce the Clarion FileManager and demonstrate how to use it, with some basic code examples.

See Introducing Clarion FileManager (Part 1) and (Part 2)

I would appreciate any feedback, comments, or bug/typo reports.

I am planning to do additional topics in future, including

  • CapeSoft FM3
  • WinEvent
  • String Functions
  • StringTheory
  • MyTable
  • Queues
  • Views
  • Filters
  • PostgreSQL
  • NetTalk API Server
  • API Consumer (NT and other)

If anyone would like to suggest other topics, or contribute some code examples or ideas, please do not hesitate to let me know, either in reply to this post, or via a direct message. Thanks in advance.

9 Likes

Looking at the below code it appeared odd to me to call .Init() / .Kill() for a File Manager. I’ve never done it and I am familiar with the code having written Init code to make ABC work with Legacy.

    Access:Customer.Init()      <-- ??? Init ?
    Access:Customer.Open()
    i = 0  ! Record counter
    LOOP UNTIL Access:Customer.Next() <> Level:Benign ! CUS loop
        !// Process each customer record
        i += 1 ! Count the records
    END ! CUS loop
    Access:Customer.Close()
    Access:Customer.Kill()      <-- ??? Kill ?

I think of Init() / Kill() as something done once by the generated BC file. I tested your code in School with the Majors file. It does run … But … if you build it Debug you will see an Assert() in the Init(). When I ran it a 2nd time it threw an Exception C000005 because the previous run .Kill() set the object &= Null.

In short IMO do not call Init() / Kill() on a File Manager. I see File Manager as a kind of a “Partial Class” which is useless without an `.Init()’ being written to &= reference to the FILE. Its a case that should be a Constructor that requires a (File) parameter.

image

image

This is my test code in School that runs from a Button.

Below is the Init/Kill code generated by the templates into the BC modules that runs once. For threaded files it’s done once each new thread with some tricky Hide:Access classes.

image

5 Likes

Your example shows this code to read all the records in the Customer file in KeyCusNumber sequence:

image

The SET() statement has 7 ways to read a File, so it can be confusing. One (File,Key) should never used, it often ends up in code when GET(File,Key) changes to SET() without making it SET(Key,Key).

Your code is using SET(Key,Key) which will Seek (read index pages) in Key order the Record that matches the Key’s component field “current values” or is greater than. You did a ClearKey() so the “current values” in the key fields will be set to Low (or High for descending -fields).

There is a simpler way of using SET( Key ) note the (Key) only appears once. This always starts with the First record in Key order. It does not do the work of Seeking a matching record, no index pages are read. There is no need to ClearKey() or Clear(Record) because those field values are not used.

In summary SET( Key ) is a simpler faster way to start with the First record than using SET(Key,Key) which does a seek reading the index and comparing.

The revised code to read all would be:

    Access:Customer.Open()
    !no need -->Access:Customer.ClearKey(CUS:KEYCUSTNUMBER) 
    SET( CUS:KEYCUSTNUMBER )  !<-- note just (Key) ... sets the order
    i = 0 ! Record counter
    LOOP UNTIL Access:Customer.Next() <> Level:Benign ! CUS loop
        !// Process each customer record
        i += 1 ! Count the records
        ud.Debug('Customer: ' & CUS:CustNumber & ' Company: ' & clip(CUS:Company))
    END ! CUS loop

The simplest is SET( File ) which processes in file record order, so nothing meaningful. I think of this as the most efficient way to read all the records, but I have no knowledge that IS the most efficient. You would have to understand how the specific DB stores records.

I would guess most SQL store records by Primary Key so SET( File:PrimaryKey ) may be most efficient. Would be interesting if anyone knows? FYI the question moved to this New Topic on MSSQL.

One other thing to look at is File Manager .UseFile().

2 Likes

11 posts were merged into an existing topic: How does MSSQL organize Record Data on disk in the database?

Thanks for the feedback, everyone.

I agree that .Init and .Kill will cause problems when run from a FORM. I mentioned that in the article when doing an example of a button on the form. You only use this when using standalone code, which I was calling from the Menu, not a form which is already using the table in question.

I wasn’t aware of the other SET option, which I will check out.

The help file says that .UseFile should be used before the file is opened.

The UseFile method notifies ABC Library objects that the managed file whose opening was delayed by the LazyOpen property is about to be used. UseFile returns a value indicating whether the file is ready for use. A return value of Level:Benign indicates the file is ready; any other return value indicates a problem. UseFile should always be called before a file is opened.

I added a button to the Customer browse, and used an action to call the standalone procedure as a new thread. I didn’t get any errors or exceptions in debug mode. maybe because it was a separate thread?

Brent Ozar is brilliant. He wrote a series of tests you can run on MSSQL Server to make sure it is running well. Saved me a lot of trouble.

2 Likes

I would bet money you should Never call File Manager .Init() and .Kill() in any typical code to access file records. They are special Constructor / Destructor methods run by the ABC generated DctInit / DctKill code just One Time.

Test all your code with a Debug build and you should see Asserts. Also run it 2+ times to catch the .Kill() destroying the FM file reference. The 2nd+ run should GPF.

  1. Open Build menu and “Set Configuration” to “Debug” not “Release”
  2. Open Project menu, “Project Options…”, Compiling tab and setting for Debug are on

image

image

Another way to look at it … the Source template has an Actions option to “Generate Open/Close Files Routines”. The generated code (below) does Not have Init/Kill. If this code was required the ABC templates would have generated it?

!Source template code for Action "Generate Open/Close Files Routines"
OpenFiles  ROUTINE
  Relate:Majors.SetOpenRelated()
  Relate:Majors.Open()   ! File Majors used by this procedure, so make sure it's RelationManager is open
  FilesOpened = True
!--------------------------------------
CloseFiles ROUTINE
  IF FilesOpened THEN
     Relate:Majors.Close
     FilesOpened = False
  END

To add some possible confusion I would point to Help “File Manager Overview” / “FileManager Conceptual Example”.

The Code is exactly like yours showing the Procedure do FM.Init() then .Open(), processing, then .Close() and .Kill(). But But But this is a complete PROGRAM not a PROCEDURE. The CODE is the first line so an .Init() is required. The RETURN will end it so should .Kill().

image

This example code includes the .Init method code. A rule of thumb might be: If you wrote the FM .Init code then you will know if you need to call it.

3 Likes

Thanks Carl. I have modified the posts and added in an explanation. I hate it when the help file is misleading.

1 Like

I have updated both posts. Part 1 no longer mentions .init and .kill methods. Part 2 shows the use of SET(key) and SET(key,key) and all the code and screen shots have been updated accordingly. The code all works and has been tested in debug mode as well, but only with TPS files. I will do another post later to cover using PostgreSQL.

Thanks once again for all your comments and support.

4 Likes

Donn,
I skimmed part 2 where you showed using the “Try*” i/o methods. Maybe I missed it, but did you explain how the i/o methods without the “Try” prefix will display various fatal error messages on-screen and prompt for a response and perhaps even terminate the program? Were you planning on discussing the ErrorClass and how you pretty much cannot determine what went wrong with the ABC i/o commands that fail unless you can squeeze that info out of the ErrorClass.

I don’t have any experience with the Error class yet, but I realise it is best to look for errors and deal with them rather than confront the user a dreadful message that terminates the program.

The blog article is only an introduction. Perhaps I can do another article later, once I can figure out how to create errors that can be detected by the program. Suggestions?