A Native TreeView (Part III)

Version: 1.00.10 - Last Update: Tuesday the 29th, September 2009 - 11:30:00

Previous ChapterComplex Controls Home (TOC)Next Chapter


FoxPro Rocks! This is all about how to create complex controls using native VFP only!

Intro

In part two we talked about some basics and how to apply them; we setup our first Grid using our brand new Node container (showing a Label that was updated dynamically from the underlying cursor). We finally ran our Test-Form and watched the RECNO() output echoed to the screen: Some strange things were going on – something we have to explore in more detail today…

Okay, this is what I found out while playing with our DynaView-Grid (IMHO that’s a pretty well-suited name for it) so far:

  1. Our Node container’s BackStyle property is accessed by VFP always TWICE in a row! Hm, sounds strange but this is something one can find all over the place when assigning _ACCESS() and/or _ASSIGN() methods to native VFP properties. So this seems to be more “by design” than a faulty behavior! Keep in mind that it is a good idea always to check what’s going on internally when applying access/assign hooks to native VFP properties!
  2. VFP re-renders all visible Nodes when changing the active Grid row. More precisely: VFP re-renders all visible Grid cells every time a Grid gets refreshed (if column's sparse = .T.) Now, this is VERY IMPORTANT (we have to remember this when implementing our TreeView solution): There are two directions changing the current Grid row: UP and DOWN. In other words: you can select (move to) a new row “above” or “below” your current active row. Go and try this on your own and carefully watch the record numbers echoed out. You will find that as long as you are moving down (selecting below) VFP will finish its re-rendering stopping on the newly selected record. As long as you are moving up (selecting above) VFP will finish its re-rendering stopping on the old record, which was selected before you started moving the record pointer upwards!

Why are these findings so important for us?

Well, I think the first point doesn’t need any further detailed explanation. There is no need to call our internal refresh hook more than once regardless of how often VFP needs to query the container’s BackStyle value.

The second finding implies two things:

  • Because VFP does not only refresh the two nodes apparently involved while changing the current record, but ALL VISIBLE ones, we have to keep our own refreshing code as compact as possible! Otherwise we’ll get hit by serious performance degradation one fine day.
  • Seen from an end-user point of view, there is a more or less comprehensive list of “living” Nodes displayed within our DynaView-Grid. But this is not a true picture of reality! In fact we only see a nicely painted picture of a Grid! There is only one “living” object inside our DynaView-Grid at a time! This object resides within the current/active Grid row, of course. All other Grid rows(cells) are represented by a picture only!!

Got the key? What do you think will happen if we change the horizontal positions of our embedded Node items within our BackStyle_Access() hook? This will become an issue, when rendering our TreeView, where we definitely have to indent the child items inside our Node container to reflect the hierarchical parent-child relations between our Nodes. Now think about an end-user selecting a new Node above! Got it? YES! We’ll end up with the “living” node object (under our mouse cursor) which was used by VFP to render the image of the old/previous node last time. This "old" Node almost always has a different indentation/layout than the new/current one! In such a scenario the last time our rendering code ran, it repositioned all internal Node items to reflect the outlook of the old Node. And this still is the actual internal state of our container that now is our active Node! You'd have to be mad to cook up an idea like that!

Let’s implement a workaround to fix the doubled BackStyle access. It's a piece of cake:
  • Add a property “nRecNo = 0” to the node container class.
  • Modify your BackStyle_Access() method like this:
WITH THIS
   IF RECNO() # .nRecNo
      .nRecNo = RECNO()
   ELSE
      ? "BackStyle_Access RecNo# =" + TRANSFORM(RECNO())
      .Refresh(.T.)
   ENDIF
ENDWITH
RETURN THIS.BackStyle

That’s all we have to do. Re-run your Test-Form and check the output. Well done! ;-)

Next let’s move the refresh code of our Label’s caption out of the Node container. I’ve put it there to get some fast results in part two of this thread. I think each child item of our Node container should refresh itself self-dependently (in terms of OOP encapsulation). Our Node is responsible for triggering the refresh only, telling its child objects to do so without telling them how to do it!

Modify the Node container’s refresh() method like so:

LPARAMETERS tlBackStyle AS Boolean
THIS.oCaption.Refresh()

Note that this is an intermediate redefinition. We will refine the refreshing behavior of our Node container later in the game again. Okay, now open your Label class and put in the following line of code into the refresh() method:

THIS.Caption = ALLTRIM(pCaption)

That’s it. Save all and re-run your Test-Form to check it and then time has come to release breaks: If you want to comment out the code that echoes out record numbers to VFP’s screen – just do it!

There is another Grid-related oddity we have to talk about next. To understand it, follow my instructions below and add programmatic centering to the Label class inside your Node container.

Open your Label class and update the refresh() method code as follows:

WITH THIS
   .Caption = ALLTRIM(pCaption)
   NOTE: .AutoSize should be set to TRUE
   .Top = INT((.Parent.Height - .Height)/2)
   .Left = INT((.Parent.Width - .Width)/2)
ENDWITH

To visualize the effect, change the label’s BackStyle property to opaque (1) and apply some funny backcolor (like yellow). Then save and close it. Next, open up your Node container class. Apply the same opaque BackStyle to it and set the background color to something complementary like green or red. Save your modifications and re-run the Test-Form. You should see something like shown in figure #1 below.

Figure #1: Our Label isn't centered! WHY?

Even better! Now go and resize the Grid’s column! Change the column width and the row height as well. Nothing happens as you can see in figure #2 shown below:

Figure #2: Column Resizing doesn't get reflected! WHY?

But wait! There is one thing we can say for sure: Our Node container definitely gets resized correctly. Otherwise we wouldn’t see each Grid cell completely flooded with our container’s red background color!

Okay, don’t drive yourself crazy! IMHO THIS IS A BUG! I played with it for a long time. I changed all Column properties that effect orientation without success. Even VFP’s debugger always shows exactly the same WIDTH- and HEIGHT- values for our Node container! They never change! It seems that they are deep-frozen right after object initialization.

Let’s change our Label’s repositioning so we do not have to use it's PARENT dimensions any longer. Modify the refresh() method of your Label class as follows:

WITH THIS
   .Caption = ALLTRIM(pCaption)
   *\\ this won't work
   *** .Top = INT((.Parent.Height - .Height)/2)
   *** .Left = INT((.Parent.Width - .Width)/2)
   *//
   *\\ let's try to get the correct dimensions
   *\\ right from our Grid/Column
   .Top = INT((.Parent.Parent.Parent.RowHeight - .Height)/2)
   .Left = INT((.Parent.Parent.Width - .Width)/2)
ENDWITH

Save this modification and re-run your Test-Form. WOW! Now it works as expected, like shown in figure #3

Figure #3: Gotcha!

Well, I must admit, I’m no big “PARENT” fan. I mean, referencing something outside my object’s boundaries at runtime using “THIS.PARENT.PARENT.PARENT.PARENT…” like above not only is hard to follow, but breaks encapsulation completely! We’ll have to discuss/find a better, more “OOP-ish” solution. One way to accomplish this is implementing an Access() method for both, the HEIGHT and the WIDTH property of our Node container. In this case we can compute the correct Node’s height and width (never touching its frozen internal values) on the fly. BUT, remember what I’ve said about assigning such hooks to NATIVE VFP properties!

  • You should be aware of all the side-effects that can hit you when doing so, like recurring (multiple) VFP-internal queries/assignments.
  • Before you finally decide to use Access/Assign hook methods, you better think twice if there isn’t a leaner solution! Keep in mind: adding an access hook turns your static class field (that’s what a property also is called) into a dynamic/computed one (also called a non parameterized function). If you are in “need for speed” (as we are in our case) you have to search for the perfect balance between code complexity (maintainability) and execution performance. Fortunately there is a pretty straight way that can help you to solve this: Count the calls to your access/assign hooks. Or even better: think first! How often do you need the information and how often does it change between your queries?

For example, if your data changes very often in the background but you only need it every now and then, you’ll better implement the dynamic-query way using an access method. Otherwise there would be an unnecessary overhead updating the static class field (the property) every time the background data changes. On the other hand, if you have to access some (almost static) data frequently, it would be of no use to re-compute the same value over and over again. This is the perfect scenario for querying a property directly.

Let’s apply those considerations:

  • There is no need to compute our Node’s width an height every time we have to know its dimensions coz they won’t change that often, if at all. That’s the best reason for using some static properties to hold the node’s height and width.
  • To bypass any problems that might arise when using access() hooks, let’s use some properties of our own (distinct from the native ones). Let’s call them “nNodeWidth” and “nNodeHeight”. If the Grid’s RowHeight and/or our Column’s Width will change, we can change our new property values on the fly accordingly. Our Node’s child objects (our “Node Items”) will query these two properties exclusively if they have to reposition themselves.

Open your Node container class and add the following properties:


nNodeWidth = 0
nNodeHeight = 0
nLeftOffset = 0

The latter will be used to hold an initial indentation/offset and to initialize a private variable we are going to use for lining up our Node items (more on that in part four).

Finally let’s extend our test cursor.

Up to now there isn’t much we’re able to display within our Node. It is time to change this. Before hacking something in, let’s rest and think about WHAT we would like to display. Figure #4 shows some RightClick menu of my German Visual Studio 2005 version. Don’t get confused by the “Bavarian” captions coz they are irrelevant ;-) Let’s use the screen shot to determine how many different kinds of Node items we should create instead.

Figure #4: Some RightClick Menu

Here comes our Node-Items wish list:

  • Icon
  • Caption
  • Divider line
  • Checkbox (Check mark)
  • Cool looking background on left side
  • Highlighting

Next we have to think about what information can be implemented statically (as a fixed property value of a Node item) and what information should be dynamically read from our cursor at runtime to keep our RightClick menu system as flexible as possible without any remarkable performance loss!

This is the moment we have to think about the different STATEs a Node can have. Fortunately these considerations apply to all kind of DynaView-Grids: Flat Lists (like or menu) as well as Hierarchical TreeViews!

Enumeration of Node States:

  • Disabled
  • Default (enabled)
  • Marked (checked)
  • Selected (hovered)
  • Focused

Wow! This seems to get complicated more and more! Yep, Right! But don’t get lost right now. I promise to you: There will be much more (even better) reasons to feel like so ;-)

I don’t want this post to get much longer. That's why I decided to defer the new complete table structure listing. Today let’s end with some pretty cool thing which also looks very nice. Follow the steps outlined below:

Run the following code fragment after having opened your test table exclusively:

ZAP
ALTER TABLE test ALTER COLUMN pCaption V(100)
= AFONT(allfonts)
FOR lnLoop = 1 TO ALEN(allfonts)
    INSERT INTO test (pCaption) VALUES (allfonts[m.lnLoop])
NEXT

Before running your Test-Form again, remove the ugly color assignments and make all backstyles transparent again. Comment out the code line that centers your caption item horizontally. And now let’s apply our first dynamic formatting.

Modify your Label’s refresh() method until it looks like shown below:

WITH THIS
   *\\ let's see what happens next :-)
   *\\ coz we've chanced the field type to "V" we don't
   *\\ need the ALLTRIM() any longer!
   STORE pCaption TO .FontName, .Caption
   *\\ let's try to get the correct dimensions
   *\\ right from our Grid/Column
   .Top = INT((.Parent.Parent.Parent.RowHeight - .Height)/2)
   *\\ do not center horizontally ATM
   *** .Left = INT((.Parent.Parent.Width - .Width)/2)
ENDWITH

Save it and run your Test-Form! Cool? Yep! No additional lines of code were needed! Show this to some of your neighbor VB.Net developers! ;-))

Figure #5: FoxPro Really Rocks!


Previous ChapterComplex Controls Home (TOC)Next Chapter

No comments:

Post a Comment