JSON, Level: Intermediate, Version: FM 16 or later

Creating and Using Invisible Window IDs

Editor’s note: I first became aware of Paul Jansen when I licensed his FMTools in the late 1990s, and I finally had the pleasure of meeting him last June at dotFMP after 20 years of online and voice communication. It’s an honor and a privilege to welcome him to FileMaker Hacks as a guest author.

FileMaker is pretty flexible. As developers we are given options as to how to reference things; fields, scripts and layouts can be referenced by name or number/id. Whilst we know that referencing things by name is more fragile, it is still a very useful capability to have, but it is definitely more robust to reference by ID…

Windows are a bit of an odd one out; we can only reference them by name.

As windows are probably the most likely things to have their names changed during normal day to day usage of a FileMaker Database it would be really useful if we could keep track of them by an ID independent of the displayed window title. Another benefit of having access to an ID to identify a window is that we then have to option to very easily store and access window specific variables. Kevin has made use of window specific variables based upon the window name in several articles and I suggest that access to a numeric ID that was independent of the window title would be an improvement.

The idea for this article came from a product idea posted to the FileMaker Community requesting native support for window IDs (https://community.filemaker.com/ideas/1169). In the comments to this Idea, David Wikström was kind enough to share a technique he uses to combine a binary representation of a window ID number with the window title using invisible characters. David’s solution (which you can download from the Idea thread on the community site) uses a number of scripts and custom functions and stores data in several global variables using XML.

Since version 16, I like to use the native JSON functionality instead of XML and I was inspired by David’s work to change the storage mechanism to JSON. As I discovered when converting another utility solution (BrowserNav) to use JSON, it is possible to significantly reduce the number of global variables used for data storage as the native functions make it really easy to access the information we need. I also decided that I wanted to make adding window IDs as simple and lightweight as possible so that it can easily be added to an existing database.

So here is my solution…

Demo file: WindowID_JSON.fmp12 (requires FM 16 or later)

The window ID is really more like a layout number; it is a simple number based upon the creation order of the windows. So when you open a solution the the first window will have a window ID of 1. Open another window and it’s window ID will be 2 and so on. These window IDs are stored in a JSON array with the index position in the array being the window ID.

[
	null,
	{
		"id" : 1,
		"name" : "WindowID_JSON",
		"open" : "2019-01-11 1:09:27 PM",
		"portalFilterValue" : "p"
	},
	{
		"close" : "2019-01-11 2:17:33 PM",
		"id" : 2,
		"name" : "WindowID_JSON - 2",
		"open" : "2019-01-11 1:49:30 PM"
	},
	{
		"id" : 3,
		"name" : "WindowID_JSON",
		"open" : "2019-01-11 2:17:32 PM",
		"windowMessage" : "Example of window scoped variable text"
	}
]

So let’s start by reviewing the contents of the JSON array. The first entry in the array is null as there is no window zero and it is just easier to use the window ID as the array index pointer. The JSON for each window also includes a time stamp for when the window was opened and in the case of window 2, a time stamp when the window was closed. I decided not to reuse the window ID of closed windows as it avoids the possibility of contamination (a new window picking up data from the array that belonged to the previous owner of that window ID). It also makes it easier to count the items in the array and add 1 to get the window ID of the next new window.

The JSON for the first window contains the object “portalFilterValue” and the JSON for the third window includes “windowMessage”. These are examples of how this technique can be used to store window specific information. Whilst it would be nice to have variables that are limited to the scope of a window, it seems to me that this is a reasonable alternative that is available now.

Now let’s take a look at the windows represented by the above JSON which is stored in a global variable $$WINDOW.JSON.

The two windows shown above are the same layout in the same file. They appear to have exactly the same name whilst window one has the portal filtered to show only fruits beginning with ‘p’ and window two is displaying the contents of the “windowMessage” object. The final thing to note is that the window IDs are calculated from the window names. The window names are actually different and each includes an invisible binary ID…

Lifting the hood (or ‘looking under the bonnet’ if you are in the UK)

In order to manage the window IDs we need to make use of the OnWindowOpen and OnWindowClose script triggers which are set in File Options:

The script ‘WindowClose’ is currently very simple; a single Set Variable step to add the close timestamp to $$WINDOW.JSON.

‘WindowOpen’ on the other hand has quite a lot to do:

  1. Remove any invisible characters from the window title
  2. Calculate a new window ID – the number of items in the JSON array + 1
  3. Store the window information in a new array item in $$WINDOW.JSON
  4. Convert the window ID to binary number
  5. Transform the binary number into invisible characters and append it to the window title

Two recursive custom functions are needed to convert to and from binary (the release of 17 means that all developers have access to custom functions which is great):

  • Dec2Bin(integer)
  • Bin2Dec(binary)

There are also 3 custom functions specific to the technique:

  • Win.ID( WindowTitle) – extracts the invisible binary ID from the Window Name and converts it to an integer ( I tried to name the parameter ‘WindowName’ but FileMaker wouldn’t let me!)
  • Win.NewName(OldName;NewName) – This function is designed to be used by the ‘Set Window Title’ script step and ensures that the new name includes the invisible ID and also updates the variable $$WINDOW.JSON
  • Win.Name(WindowID) – constructs the full Window Name with the invisible binary ID appended from the integer WindowID using the name stored in $$WINDOW.JSON

David Wikström’s brilliant idea to convert the Window ID number to binary means that we only need two different invisible characters instead of 10.

So to convert our integer Window ID to an invisible binary number we use:

Substitute ( Dec2Bin ( $windowID )
 ; [ 0 ; Char ( 8203 ) ]
 ; [ 1 ; Char ( 65279 ) ]
)

This is then easily concatenated with the Window Name to produce what is referred to in the scripts as $WindowNameID. To extract the integer Window ID from this we use the Win.ID() custom function:

Bin2Dec (
	Substitute (
		Filter ( Get(WindowName) ; 
			Char ( 8203 ) & Char ( 65279 ) ) 
		; [ Char ( 8203 ) ; 0 ] 
		; [ Char ( 65279 ) ; 1 ]
	)
)

The Filter function removes everything except the two invisible characters which are then converted to a binary number using substitute and to an integer using the Bin2Dec custom function. This can then be used to access window specific information in the $$WINDOW.JSON array. For example:

JSONGetElement ( $$WINDOW.JSON ; "[" & Win.ID("") & "].windowMessage" )

With all the pieces in place Opening and closing windows will be managed automatically. There are a few other scenarios where we need to take a little care. When renaming windows, we need to use the following script step to ensure that the invisible ID is not lost and that the $$WINDOW.JSON variable is updated.

Set Window Title [ Current Window ; 
	New Title: Win.NewName ( $oldName ; $newName ) ]

We can also select windows based on the Window ID:

Select Window [ Name: Win.Name ( $windowID ) ; Current file ]

Finally there is one FileMaker anomaly to deal with. The GTRR script step does NOT trigger the OnWindowOpen script when set to show the related records in a new window, so we need to run this ourselves to ensure the newly created window is processed correctly.

Go to Related Record [ From table: “ChildTable” 
	; Using layout: “ChildTable” (ChildTable) ; New window ]
Perform Script [ “WindowOpen” ]

Do take a look at the sample file; I’d be delighted to receive feedback and suggestions for improvement and optimisation.

Finally, many thanks to Kevin Frank for the opportunity to post on filemakerhacks.com and to David Wikström for the initial inspiration to explore the invisible approach to window IDs.

My plan is to review the demo file (taking into account any feedback to this article) and submit it as a module over at http://modularfilemaker.org. I will add a comment to this article when the module has been submitted.


Afterthought: I wonder why FileMaker refers to “Window Name” everywhere except in the script step “Set Window Title”? Oddly the options for this script step also refer to “Window Name”!

14 thoughts on “Creating and Using Invisible Window IDs”

  1. Hi Paul,

    I was not aware that spawning a new window via Go To Related Record would bypass an OnWindowOpen trigger. I just tested to confirm. Thanks for a great article.

    Kevin

  2. Great idea! Thanks for sharing this with the community.

    This isn’t working in a loop, so there probably aren’t 6 or more digits worth of iterations to worry about. However, if devs start to add more tags than the few example given here, and/or in files that spawn a lot of windows (cards for UI, utility windows for processing, and so on…), and given that some users absolutely refuse to close their sessions at the end of the workday, I wonder if Insert Calculated Result might be subbed in for Set Variable, rather than risk degrading performance by manipulating large blocks of text with Set Variable.

    https://community.filemaker.com/message/788977

    1. Matthew, It would be great if you could expand on “This isn’t working in a loop” Do you mean that the OnWindowOpen trigger isn’t running?

  3. Ahem .. Set Window Title [Current Window; “Some Friendly Title” & ¶ & $secret_json]

    You did not hear this from me.

    1. Hi Eric,

      Unfortunately that trick doesn’t work cross-platform; on Windows the hard return is ignored.

      Kevin

  4. Thanks for the technique idea. I’ve spent some considerable time implementing this into a file we use. The biggest challenge was updating every single GTRR call in our database, since that for some reason is the only way to open a window without firing the OnWindowOpen script trigger.

    Now that I have invested the time in getting an implementation of this into our database, I’ve found that I can’t elicit the behavior I expected with window-scoped portal filters. For example, I had hoped to have two windows open on the same layout, and have a different portal filter in each window. However, it looks like Get ( WindowName ) really only reports the name of the active window, and since Win.ID calls that function it basically means that the portal filter seems to look for the json data of the currently active window, rather than keeping its reference to the json for its own window.

    It seems like the same behavior can be elicited in the sample file as well, which is why I guess the Refresh button is on that filtered portal layout. But that is not really a viable solution in my particular use case.

    In my use case, I’ve basically got a portal selector to click on/show an attachment in a filtered portal. the portal selector sets the $$WINDOW.JSON with the id of the attachment, and refreshes the single-row attachment display portal, which is filtering on the JSON value set by the selector. I have to use Refresh Portal to get the display portal to update to the current selection, but doing this forces an update of the portal in all windows on the same layout.

    For the life of me I can’t figure out any way around this behavior. Is there any possible way to work around this to have a truly window-scoped variable that is not subject to the currently active window?

    1. Dale,

      You are correct in that Refresh Portal does have an undesired result. However I believe this is an issue related to Refresh Portal rather than the Window specific variables technique in general.

      I have done some further testing and although Refresh Portal updates the portal in the non current window the incorrectly updated portal rows show the correct window ID on each row. It would appear that Refresh Portal refreshes all instances of the portal with the same data. I tried a duplicate layout, but the problem was not fixed.
      I also tried renaming the portal on the duplicate layout, but this did not help either.

      Did you try a Refresh Window with “flush cached Join Results” enabled?

      As far as I can tell the only way to avoid the problem is not to use Refresh Portal, unless you create a duplicate relationship and us Hide to show the portal to the first or second relationship.

      Sorry I can’t offer anything more helpful than using Refresh Window[“flush cached Join Results”] rather than Refresh Portal[portalName] , however I do think this is a Portal update issue rather than a failure of the Window specific variables.

      If anyone else has any insight, I’m sure they will let us know…

      1. Hi Paul,

        I have verified that I seem to be able to elucidate the desired behavior using Refresh Window[“flush cached Join Results”] over Refresh Portal. This does come at a cost of somewhat reduced performance over WAN, as the layout in question has quite a lot of moving parts; targeted refresh of individual objects was desired for that reason. But given my investment of time in getting this integrated, I would say that the trade-off is well worth it. So thank you very much for your advice!

        1. Great to hear you have it working. It is annoying that the solution reduces performance, but as you said, in this case the trade off is worth it.

          I am curious, are you displaying an interactive container in your portal?

          1. The container in question is optimized for images rather than interactive content.

            I’d like to point out that the performance hit versus Refresh Portal, while somewhat noticeable over WAN, is likely not going to be particularly noticeable over LAN (whenever we return to a time that people are working on-site lol). The layout in question has around a dozen portals of varying intensity of related information, so I think it’s more of a function of the amount of stuff going on with this particular layout. With less “involved” layouts the Refresh Window step would probably not incur quite the same penalty as in this particular scenario.

    2. Hi Dale

      Get (WindowName) does often require an initial window refresh:
      https://help.claris.com/en/pro-help/content/get-windowname.html

      The follow on from this is that some calculations do not know ‘real time’ their window’s name/id.

      However this might not be the direct cause of incorrect cross-window portal filtering.

      Initially I thought that due to this Refresh Window requirement, Get (WindowName) in the portal filter calculation was causing the following:

      1. “Refresh Portal” refreshes the portal in all windows of that layout as opposed to only the currently active one (because a refresh is required to know the window name)
      2. The portal filter is therefore unable to know its window’s name, using only the name of the window from which Refresh Portal was executed

      However I’ve found out that I only get the problem when the parent record is the same across the windows. It doesn’t even matter if the portal rows are the same, or if the primary key is the same. This occurs even without Get (WindowName) in the portal filter.

      So I believe the root of the problem is that Refresh Portal refreshes all portals that share the Parent record from which the script step was executed.
      Following on from that, the portal filter’s Get(WindowName) is that of the window from which Refresh Portal was executed.

      While this is still not ideal, as it may be that the same parent record needs to be open across multiple windows with different filters, for the most part it is OK for me, as my solution’s windows are parent record specific.

      Dale, do your windows share the same parent record? If so perhaps you could create temporary parent records to ensure independent portal filtering.

      If that still doesn’t work, perhaps we’re using different portal filtering methods. Let me know what you use if so.

      Salman

  5. Hi Paul, Thx for this… solves a vexing problem. One thing. Unless I’m mistaken, you may want to mention that in addition to using “Select Window” with the new Win.Name function, subsequent usage of “Close Window” script step also requires Win.Name. Otherwise the WindowClose trigger will not fire because the title will be wrong (Close Window kicks out an 112 Window Missing error). Cheers. -Scott

    1. Scott, Great to know the technique is proving useful. I’m not quite sure I understand the problem you are having. The close triggers in the demo seem to work ok for me. If you can provide more information, I’d be happy to take a look.

Leave a Reply to Kevin FrankCancel reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.